廖雪峰Python教程学习笔记(2)

5. 函数

Python 不仅能非常灵活地定义函数,而且本身内置了很多有用的函数,可以直接调用。
函数是最基本的一种代码抽象的方式。

5.1 调用函数

内置函数的官方文档地址:https://docs.python.org/3/library/functions.html

abs() 函数的调用

# 调用 abs() 函数, 返回一个指定值:整数或浮点数的绝对值
print(abs(100))
print(abs(-20))
print(abs(12.34))
print(abs(-56.789))

# 错误演示一:
# print(abs(1, 2)) # 报错:参数个数不对
'''
Traceback (most recent call last):
  File "func_call.py", line 8, in <module>
    print(abs(1, 2))
TypeError: abs() takes exactly one argument (2 given)
'''

# 错误演示二:
# print(abs('hello')) # 报错:错误的参数类型
'''
Traceback (most recent call last):
  File "func_call.py", line 17, in <module>
    print(abs('hello'))
TypeError: bad operand type for abs(): 'str'
'''

max() 函数的调用

# 调用 max() 函数,它可以接收任意多个参数,返回最大的那个
print(max(1, 2, 3)) # 3
print(max(1, 0, -3, 7, 9, 12)) # 12

数据类型转换函数的调用

# 把 str 转换成整数
print(int('123')) # 123
# 把浮点数转换成整数
print(int(12.34)) # 12
# 把 str 转换成浮点数
print(float('12.34')) # 12.34
# 把浮点数转换成 str
print(str(12.3)) # 12.3
# 把整数转换成 str
print(str(1234)) # 1234
# 把 str 转成布尔
print(bool('')) # False
print(bool('a')) # True

之前我们接触的 chr() 函数,ord() 函数,len() 函数,range()函数, set() 函数都属于内置的函数。如果不记得,可以去复习一下。

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋值给一个变量,相当于给这个函数起了一个“别名”。
上面这句话需要好好理解一下。

a = abs # 变量 a 指向 abs 函数
print(a(-1)) # 1,这里是通过 a 调用 abs 函数

5.2 定义函数

在 Python 中,定义一个函数要使用 def语句,依次写出函数名、括号、括号中的参数(如果有参数的话)和冒号:,然后在缩进代码块中编写函数体,函数的返回值用 return 语句返回。

# 自定义一个求绝对值的my_abs函数
def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

print(my_abs(-99)) # 99

把 func_def.py 文件中定义的 my_abs()函数导入 Python 交互式模式,需要在 func_def.py 所在的目录,打开 Python 交互式模式,输入 from func_def import my_abs 并回车就可以了。

>>> from func_def import my_abs
>>> my_abs(-9)
9

可以在 Python 交互式模式下直接定义 my_abs() 函数:

>>> def my_abs(x):
...     if x >= 0:
...         return x
...     else:
...         return -x
...
>>> my_abs(-10)
10

需要注意的是,函数定义结束后需要按两次回车重新回到>>>提示符。

函数执行完毕也没有return语句时,自动return None 的代码演示:

# 没有返回值的函数
def show1(x):
    print(x)

def show2(x):
    print(x)
    return

def show3(x):
    print(x)
    return None

show1("no return")
show2("no return")
show3("no return")

空函数

如果想定义一个什么都不做的空函数,要用到 pass 语句:

# 定义一个空函数
def nop():
    pass

pass 是起占位符的作用,这能保证代码正常运行起来。如果没有 pass,那么会报错:SyntaxError: unexpected EOF while parsing。
pass 语句还可以用在其他语句里。

对自定义的 my_abs() 函数的优化

# print(my_abs('A')) # 报错
'''
报错信息:
Traceback (most recent call last):
  File "func_def.py", line 14, in <module>
    print(my_abs('A'))
  File "func_def.py", line 3, in my_abs
    if x >= 0:
TypeError: '>=' not supported between instances of 'str' and 'int'
'''
# 而使用内置函数 abs() 
# print(abs('A')) # 报错
'''
报错信息:
Traceback (most recent call last):
  File "func_def.py", line 25, in <module>
    print(abs('A'))
TypeError: bad operand type for abs(): 'str'
'''
# 可以看到,我们自定义的 my_abs() 函数,和内置函数 abs() 报错信息不一样:
# 自定义的不能检查出参数错误,内置的可以检查出参数错误。
# 优化自定义的 my_abs() 函数,对参数类型做检查,只允许整数和浮点数的参数
def my_abs2(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type for my_abs2(): %s' % type(x).__name__)
    if x >= 0:
        return x
    else:
        return -x

print(my_abs2(-1)) # 1
# print(my_abs2('a'))

isinstance(object, classinfo) 函数也是一个内置函数,classinfo 可以是给定的类型,也可以类型的元组。

返回多个值

在 Python 中,函数可以返回多个值。事实上,Python 的函数返回多个值就是返回一个 tuple。在语法上,返回一个 tuple,可以省略括号。

import math

def move(x, y, step, angle = 0):
    nx = x + step * math.cos(angle)
    ny = y + step * math.sin(angle)
    return nx, ny

x, y = move(100, 100, 60, math.pi / 6)
print(x, y) # 151.96152422706632 130.0

# 但实际上,Python 函数返回的仍然是单一值
r = move(100, 100, 60, math.pi / 6)
print(r) # (151.96152422706632, 130.0)
# 可以看到,返回值实际上是一个 tuple

5.3 函数的参数

位置参数

位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。

# 写一个计算 x^2 的函数
# def power(x):
#     return x * x

# print(power(5))
# print(power(15))
# 写一个计算 x^n 的函数
def power(x, n):
    s = 1
    while n > 0:
        n -= 1
        s *= x
    return s

print(power(5, 2))
print(power(15, 2))
print(power(2, 10))

上面参数列表中的 x 和 n,就是位置参数。它们都是必须的,顺序和个数必须与函数定义时一致。

但是,对于原来的 2 次方写法,power(x, n) 却多出了一个位置参数,这样导致不得不修改之前的代码,怎么办呢?我们需要默认参数出场了。

默认参数

def power(x, n = 2):
    s = 1
    while n > 0:
        n -= 1
        s *= x
    return s

print(power(5))
print(power(15))
print(power(2, 10))

第二个参数的默认值被设置为 2。

默认参数可以简化函数的调用。

设置默认参数时,需要注意:

  • 必选参数在前,默认参数在后,否则 Python 的解释器会报错;
  • 当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

当有多个默认参数时,当按顺序提供默认参数时,可以省略参数名;当不按顺序提供部分默认参数时,就必须把参数名写上。

默认参数必须指向不可变对象!

def add_end(L = []):
    L.append('END')
    return L

print(add_end([1, 2, 3])) # [1, 2, 3, 'END']
print(add_end(['x', 'y', 'z'])) # ['x', 'y', 'z', 'END']
print(add_end()) # ['END']
print(add_end()) # ['END', 'END']
print(add_end()) # ['END', 'END', 'END']

# 描述一下,出现的大坑:第一次调用 add_end() 的输出是 ok 的,但是第二次,第三次的输出却是不对的。
# 原因是:Python 函数在定义的时候,默认参数 L 的值就被计算出来了,即 [], 因为默认参数 L 也是一个变量,
# 它指向对象 [],每次调用该函数,如果改变了 L 的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的 [] 了。

# 解决办法:默认参数必须指向不变对象!
def add_end2(L = None):
    if L is None:
        L = []
    L.append('END')
    return L

print(add_end2()) # ['END']
print(add_end2()) # ['END']
print(add_end2()) # ['END']

可变参数

可变参数就是说传入的参数个数是可变的,可以是 1 个,2 个或者任意个,还可以是 0 个。

# 给定一组数字a,b,c……,请计算a^2 + b^2 + c^2 + ……
# 这种写法需要传入一个 list 或者 tuple
def calc(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n ** 2
    return sum

print(calc([1, 2, 3])) # 14

# 使用可变参数
def calc2(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n ** 2
    return sum

print(calc2(1, 2, 3)) # 14
t = (1, 3, 5, 7)
print(calc2(*t)) # 84

可以看到,定义可变参数,仅仅是在参数加了一个 * 号而已。
如果目前有一个 list 或者 tuple,该怎样调用可变参数呢?使用 *tt 这个 tuple 的所有元素作为可变参数传进去。

关键字参数

可变参数允许传入 0 个或者任意个参数,这些可变参数在函数调用时自动组装为一个 tuple
而关键字参数允许传入 0 个或者任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict

def person(name, age, **kw):
    # print(isinstance(kw, dict)) # True
    print('name:', name, 'age:', age, 'other:', kw)

# 可以不传递关键字参数
person('Michael', 30) # name: Michael age: 30 other: {}
# 传递关键字参数
person('Bob', 35, city='Beijing') # name: Bob age: 35 other: {'city': 'Beijing'}

# 直接传一个 dict
extra = {'city' : 'Beijing', 'job' : 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job']) # name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

#简写
person('Jack', 24, **extra) # name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra 表示把 extra 这个 dict 的所有 key-value 用关键字参数传入到函数的 **kw 参数,kw 将获得一个 dict ,注意 kw 获得的 dictextra 的一份拷贝,对 kw 的改动不会影响到函数外的 extra

命名关键字参数

如果要限制关键字参数的名字,就可以用命名关键字参数。
命名关键字参数必须传入参数名。

# 使用命名关键字参数: 命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数
def person2(name, age, *, city, job):
    print(name, age, city, job)

person2('Jack', 24, city='Beijing', job='Engineer') # Jack 24 Beijing Engineer
# 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了
def person3(name, age, *args, city, job):
    print(name, age, args, city, job)

person3('zhichao',32, *[1, 2, 3], city='shanghai', job='Engineer') # zhichao 32 (1, 2, 3) shanghai Engineer

# 命名关键字参数必须传入参数名
# person2('Jack', 24, 'Beijing', 'Engineer') # 报错: 命名关键字参数不传参数名,就被当成了位置参数。
'''
Traceback (most recent call last):
  File "func_args.py", line 110, in <module>
    person2('Jack', 24, 'Beijing', 'Engineer')
TypeError: person2() takes 2 positional arguments but 4 were given
'''

# 命名关键字参数可以有缺省值,从而简化调用
def person4(name, age, *, city='Beijing', job):
    print(name, age, city, job)

# 由于命名关键字参数有默认值,调用时,可以不传入 city 参数
person4('Bill', 32, job='Driver') # Bill 32 Beijing Driver

参数组合

在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这 5 种参数都可以组合使用。
但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

def f1(a, b, c=0, *args, **kw):
    print('a=', a, 'b=', b, 'c=', c, 'args=', args, 'kw=', kw)

def f2(a, b, c=0, *, d, **kw):
    print('a=', a, 'b=', b, 'c=', c, 'd=', d, 'kw=', kw)

f1(1, 2) # a= 1 b= 2 c= 0 args= () kw= {}
f1(1, 2, c=3) # a= 1 b= 2 c= 3 args= () kw= {}
f1(1, 2, 3, 'a', 'b') # a= 1 b= 2 c= 3 args= ('a', 'b') kw= {}
f1(1, 2, 3, 'a', 'b', x=99) # a= 1 b= 2 c= 3 args= ('a', 'b') kw= {'x': 99}
f2(1, 2, d=9, ext=None) # a= 1 b= 2 c= 0 d= 9 kw= {'ext': None}
# 通过一个 tuple 和 dict,调用上述函数
args = (1, 2, 3, 4)
kw = {'d' : 99, 'x' : '#'}
f1(*args, **kw) # a= 1 b= 2 c= 3 args= (4,) kw= {'d': 99, 'x': '#'}
args = {1, 2, 3}
kw = {'d' : 88, 'x' : '#'}
f2(*args, **kw) # a= 1 b= 2 c= 3 d= 88 kw= {'x': '#'}

对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。这点也是神奇了。

5.4 递归函数

如果一个函数在内部调用自身,那么这个函数就是递归函数。
使用递归函数要防止栈溢出。

栈知识点补充:
在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。但是,栈的大小不是无限的。

所以,递归调用的次数过多,会导致栈溢出。
可以看这段演示代码:

# 计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示
def fact(n):
    if n == 1:
        return 1
    return n * fact(n - 1)

print(fact(1))
print(fact(5))
print(fact(100))
# print(fact(1000)) # 报错
'''
Traceback (most recent call last):
  File "func_recur.py", line 10, in <module>
    print(fact(1000))
  File "func_recur.py", line 5, in fact
    return n * fact(n - 1)
  File "func_recur.py", line 5, in fact
    return n * fact(n - 1)
  File "func_recur.py", line 5, in fact
    return n * fact(n - 1)
  [Previous line repeated 995 more times]
  File "func_recur.py", line 3, in fact
    if n == 1:
RecursionError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化。尾递归就是从最后开始计算,每递归一次就算出相应的结果。

尾调用很重要的特性就是它可以不在调用栈上面添加一个新的堆栈帧,而是更新它。

# 尾调用的代码:
def foo(data):
   return b(data)

def b(data):
 pass

若一个函数在尾位置调用本身(或是一个尾调用本身的其他函数等),则称这种情况为尾递归,是递归的一种特殊情形。而形式上只要是最后一个 return 语句返回的是一个完整函数,它就是尾递归。这里注意:尾调用不一定是递归调用,但是尾递归一定是尾调用。

修改为尾递归的形式:

# 使用尾递归
def fact2(n):
  return fact_iter(n, 1)

def fact_iter(num, product):
  if num == 1:
    return product
  return fact_iter(num - 1, num * product) # 这里仅返回递归函数本身

# fact2(1000) # 报错
'''
Traceback (most recent call last):
  File "func_recur.py", line 35, in <module>
    fact2(1000)
  File "func_recur.py", line 28, in fact2
    return fact_iter(n, 1)
  File "func_recur.py", line 33, in fact_iter
    return fact_iter(num - 1, num * product) # 这里仅返回递归函数本身
  File "func_recur.py", line 33, in fact_iter
    return fact_iter(num - 1, num * product) # 这里仅返回递归函数本身
  File "func_recur.py", line 33, in fact_iter
    return fact_iter(num - 1, num * product) # 这里仅返回递归函数本身
  [Previous line repeated 994 more times]
  File "func_recur.py", line 31, in fact_iter
    if num == 1:
RecursionError: maximum recursion depth exceeded in comparison
'''

但是,发现依然会出现栈溢出的错误。这是因为大多数编程语言没有针对尾递归做优化,Python 解释器也没有做优化。

发布了78 篇原创文章 · 获赞 46 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/willway_wang/article/details/104220562