# 函数
# 创建
- Python 中,使用关键字
def
可以定义一个函数,一般语法为:def function_name(args): statement_block
- 使用
function_name(args)
的语法,即可调用一个函数。
# 参数
C 语言中,
- 定义函数时,需要声明一组输入参数的名称,称为形参列表。
- 调用函数时,需要传入一组输入参数的值,称为实参列表。
- 实参列表必须与形参列表一致,也就是包含同样数量的参数、参数的数据类型相同。但 Python 没有这些限制,参数比较灵活。
Python 中,形参有几种格式:
- 位置参数(positional argument)
- :只有参数名,没有默认值。因此调用函数时,必须给每个位置参数赋值。
- 例:
>>> def fun1(a, b): ... return a, b ... >>> fun1() TypeError: fun1() missing 2 required positional arguments: 'a' and 'b' >>> fun1(1) TypeError: fun1() missing 1 required positional argument: 'b' >>> fun1(1, 2) (1, 2)
- 关键字参数(keyword argument)
- :有参数名、默认值,格式为
arg=value
。因此调用函数时,可以不给关键字参数赋值,此时会采用默认值。 - 例:
>>> def fun2(a, b=2): # 这里 a 属于位置参数,b 属于关键字参数 ... return a, b ... >>> fun2(1) # 这里没有给 b 赋值,因此会采用 b 的默认值 (1, 2) >>> fun2(1, 2) # 形参 a、b 都可以通过 value 或 key=value 两种形式赋值 (1, 2) >>> fun2(a=1, b=2) (1, 2)
- :有参数名、默认值,格式为
- 元组参数
- :用一个星号
*
作为前缀。 - 定义函数时,最多只能声明一个元组参数。
- 调用函数时,如果传入的实参个数,多于形参个数,
- 如果没声明元组参数,则抛出异常。例:
>>> fun1(1, 2, 3) TypeError: fun1() takes 2 positional arguments but 3 were given
- 如果声明了元组参数,则将多余的几个实参,打包为一个 tuple ,赋值给元组参数。例:
>>> def fun3(a, b=2, *args): ... return a, b, args ... >>> fun3(1, 2) # 此时没有传入多余的实参,因此 args 元组是空的 (1, 2, ()) >>> fun3(1, 2, 3) # 此时传入了多余的实参,因此被放在 args 元组中 (1, 2, (3,))
- 如果没声明元组参数,则抛出异常。例:
- :用一个星号
- 字典参数
- :用两个星号
**
作为前缀。 - 定义函数时,最多只能声明一个字典参数,功能与元组参数类似。
- 元组参数只能接收多余的位置参数(打包为一个 tuple ),不能接收多余的关键字参数。
- 字典参数只能接收多余的关键字参数(打包为一个 dict ),不能接收多余的位置参数。
- 例:
>>> def fun4(a, b, **kwargs): ... return a, b, kwargs ... >>> fun4(1, 2) (1, 2, {}) >>> fun4(1, 2, c=3) (1, 2, {'c': 3}) >>> fun4(1, 2, 3) # 不能接收多余的位置参数 TypeError: fun4() takes 2 positional arguments but 3 were given
- 定义函数、调用函数时,可以在函数头中,同时填入上述四种参数,但必须从左到右依次存放:例:
function_name(位置参数,关键字参数,元组参数,字典参数)
>>> def fun1(a=1, b): # 所有关键字参数,必须位于所有位置参数的右侧,否则会抛出异常 SyntaxError: non-default argument follows default argument
- :用两个星号
- 万能参数
- 定义函数时,如果只声明一个元组参数、一个字典参数,则允许接收任何类型、任何数量的实参。
>>> def fun5(*args, **kwargs): ... print(args, kwargs) ... >>> fun5() # 如果不传入参数,则元组参数、字典参数默认为空 () {} >>> fun5(1, 2, a=3, b=4) (1, 2) {'a': 3, 'b': 4}
- 定义函数时,如果只声明一个元组参数、一个字典参数,则允许接收任何类型、任何数量的实参。
- 位置参数(positional argument)
Python 中,实参有几种格式:
- 位置参数
- :只输入参数的值。
- 例:
fun5(1, 2) # 1 是第一个实参,因此会赋值给第一个形参。2 是第二个实参,因此会赋值给第二个形参。以此类推
- 关键字参数
- :先指定参数的名称,然后给参数赋值。
- 例:
fun5(x=1, y=2) # 1 会赋值给名为 x 的形参,2 会赋值给名为 y 的形参 fun5(y=2, x=1) # 多个关键字参数之间,可以打乱顺序。因为指定了参数的名称,能知道赋值给哪个形参
- 拆包
- 调用函数时,如果一个实参属于可迭代对象,则可用一个星号
*
,获取该可迭代对象的所有元素,从而将该实参拆分成多个实参。>>> fun5(*range(2)) (0, 1) {} >>> fun5(*range(2), *range(2)) # 可以对多个实参,进行拆包 (0, 1, 0, 1) {}
- 调用函数时,如果一个实参属于 dict 类型,则可用两个星号
**
,获取该 dict 的所有元素,从而将该实参拆分成多个实参。>>> fun5(1, 2, {'c': 3}) # 如果不拆包,则会将 {'c': 3} 作为一个位置参数传入 (1, 2, {'c': 3}) {} >>> fun5(1, 2, **{'c': 3}) (1, 2) {'c': 3} >>> fun5(*(1, 2), **{'c':3, 'd':4}) (1, 2) {'c': 3, 'd': 4}
- 如果一个实参,拆包之后没有得到任何元素,则相当于该实参不存在。
>>> print(()) () >>> print(*()) # 这里相当于调用 print() >>> print(**{})
- 调用函数时,如果一个实参属于可迭代对象,则可用一个星号
- 位置参数
# 返回值
- return 语句用于终止函数的执行,并返回一个值。
- 一个函数可以存在多个 return 语句。执行任一 return 语句,都会导致函数立即终止。
- 一个函数可以不存在 return 语句。当函数执行完最后一行代码之后,会隐式执行
return None
。 - 一个 return 语句可以返回一个任意类型的对象。
- 一个 return 语句可以返回多个值,用逗号分隔,它们会组成一个 tuple 返回。例:
>>> def fun1(a, b): ... return a, b ... >>> fun1(1, 2) (1, 2)
# 调用
Python 属于脚本语言,大部分语句,要等到被 Python 解释器执行时,才能发现是否有错误(比如变量未定义)。
- 因此,一个函数即使能成功定义、创建,也不一定能成功调用。
- 有的语句有明显的语法错误,可以在执行之前就发现、报错。
在函数内使用外部变量时,可能遇到以下问题:
- 如果函数内使用了一些标识符,则当 Python 解释器执行该函数中的代码时,才能发现这些标识符是否有效。
>>> def fun1(): ... return a ... >>> fun1() NameError: name 'a' is not defined
- 如果函数内使用了一个外部变量,则当 Python 解释器执行到该函数时,才会尝试读取该变量的值。
>>> def fun1(): ... return a ... >>> a = 1 >>> fun1() 1 >>> a = 2 # 函数外的变量被修改了,函数内会读取到该变量的新值 >>> fun1() 2
- 如果将外部变量,用作函数某个形参的默认值,则会在创建函数时,就读取该变量的值并保存。
>>> a = 1 >>> def fun1(b=a): ... return b ... >>> fun1() 1 >>> a = 2 # 此时函数外的变量被修改了,函数内依然读取到该变量的旧值 >>> fun1() 1 >>> fun1.__defaults__ # 因为在创建函数时,会将输入参数的默认值,保存到函数的内置属性 __defaults__ 中 (1,)
- 如果函数内使用了一些标识符,则当 Python 解释器执行该函数中的代码时,才能发现这些标识符是否有效。
给函数传入实参时,实参的值,可能被函数体修改。
- 这是因为,Python 将实参赋值给形参时,采用引用传递,而不是值传递。如果实参属于可变类型的对象,则可以被修改。
>>> def fun1(a): ... a.append(1) ... >>> a = [] >>> fun1(a) >>> a [1]
- 为了避免实参被修改,可以将它拷贝一份,再传入函数。
>>> fun1(a.copy())
- 这是因为,Python 将实参赋值给形参时,采用引用传递,而不是值传递。如果实参属于可变类型的对象,则可以被修改。
函数被调用时,如何判断,当前传入了哪些实参?
- 默认情况下,如果没有传入某个位置参数,则会引发异常。如果没有传入某个关键字参数,则会采用默认值。
>>> def fun1(a, b=None): ... pass ... >>> fun1() TypeError: fun1() missing 1 required positional argument: 'a'
- 可以将关键字参数的默认值,设置为一个用户不可能传入的值,从而判断用户是否传入了该参数。
>>> _no_value = object() >>> def fun1(a, b=_no_value): ... if b is _no_value: ... raise TypeError("missing argument: 'b'") ... >>> fun1() TypeError: fun1() missing 1 required positional argument: 'a' >>> fun1(1) TypeError: missing argument: 'b'
- 默认情况下,如果没有传入某个位置参数,则会引发异常。如果没有传入某个关键字参数,则会采用默认值。
# 匿名函数
Python 允许用关键字
lambda
,在单行代码中,定义一个简短的函数。常用于计算一个表达式的值。- 格式:
lambda 形参 : 返回值
- 例:
>>> lambda a,b : a+b <function <lambda> at 0x0000027A73FB9E50> >>> _(1,2) 3
- lambda 函数没有函数名,因此称为匿名函数。应该只使用一次,否则代码不容易被人阅读。
- lambda 函数像普通函数,有独立的作用域、命名空间。
- lambda 函数的左侧,是形参列表,可以使用关键字参数。
>>> lambda a,b=0: a+b <function <lambda> at 0x000001748948E040>
- lambda 函数的右侧,只能用于 return 返回值,不能赋值。
>>> lambda a,b : a=b SyntaxError: cannot assign to lambda
- 格式:
Python 的 lambda 函数,类似于 C++ 的内联函数。
- C 语言中,可以通过宏定义实现一些简单的函数功能。
- C++ 中,将宏定义升级成了内联函数这种语法。
- 用关键字 inline 声明内联函数。
- 调用内联函数时,编译器会将调用该函数的语句,替换成函数内的语句块。如果函数体较大,编译器可能不会这么做。
- 优点:每次调用函数时,存在一定开销,需要保存现场、把变量入栈、跳转执行函数体、跳转回原来代码处、恢复现场。调用内联函数,比普通函数的开销更小。
- 缺点:编译出来的可执行文件,体积会变大。
# 闭包函数
高阶函数
- Python 允许一个函数的输入参数是 callable 类型,或者 return 返回值是 callable 类型。
- 这种函数称为高阶函数。因为它输入、输出的是一个函数,是其它函数的管理者,地位更高。
嵌套函数
- Python 允许在一个函数内,定义另一个函数。
- 这种语法称为嵌套函数,前一个函数称为父函数,后一个函数称为子函数。
闭包函数(closure)
- 闭包函数有三个特征:
- 在一个函数内,嵌套定义另一个函数。
- 将子函数,作为父函数的返回值。
- 子函数,能读取父函数中定义的局部变量。
- 例:
>>> def fun1(): ... a = 1 ... def fun2(): ... print(a) ... return fun2 ... >>> fun1() <function fun1.<locals>.fun2 at 0x0000017489BB9E50> >>> fun1()() 1
- 这里变量 a 称为自由变量。因为父函数已经运行结束,该变量却依然存活,能被子函数访问。换句话说,该变量脱离了原本的作用域,自由存在。
- Python 的装饰器语法中,就会将被装饰的函数,变成闭包函数。
- 闭包函数有三个特征:
# 装饰器
装饰函数:是让一个函数担任装饰器(Decorator),装饰另一个函数。在不改变后者函数体的情况下,给后者附加一些功能。
例:定义一个装饰函数,用于打印日志
>>> def debug(func, *args, **kwargs): ... print('[DEBUG]: calling {}()'.format(func.__name__)) ... return func(*args, **kwargs) ... >>> debug(int, 1.0) [DEBUG]: calling int() 1
上例的缺点:需要所有调用
int(x)
的代码,改成debug(int, x)
的形式,挺麻烦。因此,建议改进为以下代码:>>> def debug(func): ... def wrapper(*args, **kwargs): ... print('[DEBUG]: calling {}()'.format(func.__name__)) ... return func(*args, **kwargs) ... return wrapper ... >>> debug(int) <function debug.<locals>.wrapper at 0x0000020A564C9E50> >>> int = debug(int) # 将 int 这个函数名,替换成装饰之后的函数名 wrapper >>> int.__name__ 'wrapper' >>> int(1.0) [DEBUG]: calling int() 1
上例的缺点:函数名被替换了,
__name__
、__doc__
等函数属性都丢失了。因此,建议改进为以下代码:>>> from functools import wraps >>> def debug(func): ... @wraps(func) # 这里调用装饰器 @wraps ,拷贝原函数的 __name__、__doc__ 等函数属性 ... def wrapper(*args, **kwargs): ... print('[DEBUG]: calling {}()'.format(func.__name__)) ... return func(*args, **kwargs) ... return wrapper ... >>> debug(int) <function int at 0x0000022693909E50> >>> debug(int).__name__ 'int' >>> debug(int).__wrapped__ # 使用装饰器 @wraps 时,会将装饰之前的原函数,记录在 __wrapped__ 变量中 <class 'int'>
为了更简便地启用装饰器,Python 提供了一种语法:定义一个函数时,可以在函数头之上添加一行
@function_name
,表示用另一个函数,作为装饰器,装饰当前函数。>>> @debug ... def fun1(x): ... return x ... >>> fun1('hello') [DEBUG]: calling fun1() 'hello'
进一步地,如果想给装饰器本身传入参数,则需要在装饰器外层,再定义一层函数:
>>> from functools import wraps >>> def debug(level=0): # 外层函数,用于生成装饰器 ... print('debug level: {}'.format(level)) # 外层函数中的语句,会在用 @ 指定装饰器时执行 ... def decorator(func): ... @wraps(func) ... def wrapper(*args, **kwargs): ... print('[DEBUG]: calling {}()'.format(func.__name__)) ... return func(*args, **kwargs) ... return wrapper ... return decorator # 返回最终生效的装饰器 ... >>> @debug(level=1) ... def fun1(x): ... return x ... debug level: 1 >>> fun1('hello') [DEBUG]: calling fun1() 'hello'
上例的装饰器,会拦截一个函数的输入参数、输出参数,从而可以进行修改。但是,如果不需要修改参数,则可以定义一个简单的装饰器。
>>> def wrapper(obj): ... obj.debug_level = 1 # 添加一个属性 ... return obj ... >>> @wrapper # 相当于 fun1 = wrapper(fun1) ... def fun1(): ... pass ... >>> fun1.debug_level 1 >>> @wrapper # 相当于 Test = wrapper(Test) ... class Test(): ... pass ... >>> Test.debug_level 1
如何装饰某个类的方法?需要增加处理 self 参数。
>>> def debug(method): ... @wraps(method) ... def wrapper(self, *args, **kwargs): ... print('[DEBUG]: calling {}()'.format(method.__name__)) ... return method(self, *args, **kwargs) ... return wrapper ... >>> class Test: ... @debug ... def fun1(self, x): ... return x ... >>> Test().fun1(1) [DEBUG]: calling fun1() 1
如何用类,定义一个装饰器?见下例:
>>> import types >>> from functools import wraps >>> class Debug: ... def __init__(self, func): # 必须定义 __init__() 方法,接收被装饰的函数 func ... self.num_calls = 0 ... wraps(func)(self) # 将原函数 func 的 __name__、__doc__ 等属性,拷贝到 self ... def __call__(self, *args, **kwargs): # 必须定义 __call__() 方法,使得当前类名,可以像函数名称一样调用 ... self.num_calls += 1 ... print('[DEBUG]: calling {}()'.format(self.__wrapped__.__name__)) ... return self.__wrapped__(*args, **kwargs) ... def __get__(self, instance, instance_type): # 定义 __get__() ,使得当前装饰器,可以装饰实例方法 ... if instance is None: ... return self ... else: ... return types.MethodType(self, instance) ... >>> @Debug ... def fun1(x): ... return x ... >>> fun1('hello') [DEBUG]: calling fun1() 'hello' >>> fun1 # 此时 fun1 是从 Debug 类创建的对象 <__main__.Debug object at 0x000002269383F430> >>> fun1.num_calls # 可以访问该对象的成员 1 >>> class Test: ... @Debug ... def method1(self, x): ... return x ... >>> Test().method1('hello') [DEBUG]: calling method1() 'hello'