# 函数

# 创建

  • 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}
        
  • 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 将实参赋值给形参时,采用引用传递,而不是值传递。如果实参属于可变类型的对象,则可以被修改。
      >>> def fun1(a):
      ...     a.append(1)
      ...
      >>> a = []
      >>> fun1(a)
      >>> a
      [1]
      
    • 为了避免实参被修改,可以将它拷贝一份,再传入函数。
      >>> fun1(a.copy())
      
  • 函数被调用时,如何判断,当前传入了哪些实参?

    • 默认情况下,如果没有传入某个位置参数,则会引发异常。如果没有传入某个关键字参数,则会采用默认值。
      >>> 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'