跳转至

第4章 函数与函数编程

4.1.1 函数的定义

  • 函数的作用
  • 是实现结构化编程的重要工具之一
  • 把需要重复使用的具有相对独立的功能部分定义为函数

  • 定义要点

  • 函数的核心组成部分包括函数名、函数的参数列表、函数的返回值
  • 函数的参数列表也称为函数的签名
  • 函数的文档字符串也是函数的组成部分(非必须),属性名为__doc__
  • 使用def关键字来定义函数
  • 函数使用return语句返回运行结果,如果没有return语句函数会返回None
1
2
3
def 函数名(参数列表):
    """文档字符串"""
    函数体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def add(x, y):
    """
    数值求和.
    Args:
        x: 第一个参数.
        y: 第二个参数.
    Returns: 两个参数之和.
    """
    s = x + y
    return s
1
print(add.__doc__)
1
2
3
4
5
    数值求和.
    Args:
        x: 第一个参数.
        y: 第二个参数.
    Returns: 两个参数之和.

  • 函数的副作用
  • 函数的副作用是指函数除了返回值之外,还对环境产生了附加的影响
  • 函数的副作用往往会为程序设计带来不必要的麻烦,因此应当尽可能减少
  • 尽可能遵循的原则
  • 在定义函数的时候尽可能让函数仅仅通过参数与返回值与环境产生联系

    • 函数只应当有一个入口,就是其参数;
    • 函数只应当有一个出口,就是return语句。
  • 函数可以定义在什么地方?

  • 模块
  • 其他函数之中
  • 类的方法之中
1
2
3
4
5
def fun():
    def inner_fun():
        print('This is inner fun!')
    print('This is fun!')
    inner_fun()

4.1.2 函数的调用

  • 参数传递
  • 通过函数的名字来调用函数
  • 调用其他函数的函数,称为主调函数
    • 如果函数中定义了参数,在主调函数中还应当传入对应的参数
  • 定义在参数列表中的参数,称为函数的__形参__(formal parameter)
  • 由主调函数传入的参数,称为__实参__(actual parameter)
  • 形参变量和实参变量的命名可以不同,在函数调用的过程中,实参会将其值传递给形参
1
2
3
n1 = 2
n2 = 3
add(n1, n2)
1
2
  • 形参与实参是相互独立的,改变形参的值并不会导致实参的值被改变
1
2
def fun(formal_p):
    formal_p = [8, 5, 7]
1
2
3
actual_p = [1, 4, 2]
fun(actual_p)
print(actual_p) 
1
[1, 4, 2]
  • 如果参数传递的是可变数据类型,形参传递给实参的实际上是可变数据的引用
  • 形参引用的改变不会导致实参引用发生变化,但是由于它们指向相同的数据,因此形参指向的数据被改变后实参所指向的数据当然也会被改变
1
2
def fun(formal_p):
    formal_p[0] = 0 
1
2
3
actual_p = [1, 4, 2]
fun(actual_p)
print(actual_p)
1
[0, 4, 2]
  • 递归调用
  • 函数定义之后,允许其他函数调事,也允许自己调用自己
  • 函数调用了自身称为__递归调用__,这样的函数称为__递归函数__
  • 递归函数中一定要有退出递归的条件,否则就会陷入无限的递归循环之中

  • 利用递归实现列表的深复制

1
2
def copy(seq):
    return [copy(o) if type(o) is list else o for o in seq]
1
2
3
4
5
lst = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10, 11]]]]]
lst_new = copy(lst)
lst_new[2][2][2][2][2] = 0
print(lst)
print(lst_new)
1
2
[1, 2, [3, 4, [5, 6, [7, 8, [9, 10, 11]]]]]
[1, 2, [3, 4, [5, 6, [7, 8, [9, 10, 0]]]]]

4.1.3 变量的作用域

  • 全局变量
  • 全局变量是直接在模块中定义的变量,其作用域为定义变量的位置直至模块结束
  • 定义在一个模块中的全局变量可以在其它模块中利用import语句导入使用
  • 全局变量应当定义在模块的头部,以利用管理和维护
  • 在模块中修改全局变量的值与在函数中修改参数的值相似

    • 模块中修改了其他模块中定义的全局变量的引用,则其他模块中所读取到的全局变量的值不会发生变化
    • 如果改变了引用所指向的数据,则其他模块中读取到的全局变量所指向的数据也会改变
  • 局部变量

  • 定义在函数中的局部变量其作用范围是其定义的位置开始至函数结束
  • 定义在类方法中的变量也是局部变量(参见面向对象编程部分)
  • 在推导式中(包括列表推导式、生成器推导式、集合推导式和字典推导式)中定义的变量,其作用范围仅限于推导式内部

  • global语句

  • 在函数中定义全局变量
    • 定义在函数中的全局变量,只有在函数执行之后才能被使用
    • 全局变量能够起到减少函数参数数量、简化代码的作用,但是它破坏了函数只有一个入口和一个出口的原则,因此应当尽可能减少使用
    • 要避免使用定义在函数中的全局变量
1
2
3
4
x = 1
def fun():
    global x
    x = 2
1
2
fun()
print(x)
1
2
  • 利用global将局部变量声明为全局变量
    • 在函数内部能够访问全局变量,但是不能修改全局变量的取值
    • 如果尝试在函数中修改全局变量的值,则相当于重新定义了一个同名的局部变量
    • 使用global可以将局部变量声明为同名的全局变量,就可以对其进行修改了
1
2
3
4
5
6
7
x = 0
def fun():
    x = 1
    print(x)

fun()
print(x)
1
2
1
0
1
2
3
4
5
6
7
8
x = 0
def fun():
    global x
    x = 1
    print(x)

fun()
print(x)
1
2
1
1

4.2 函数的参数

4.2.1 位置参数与关键字参数

  • 位置参数:实参列表中的参数将数值传递给形参列表中相同位置的参数。这种方式也是我们最常接触到的参数传递方式
  • 关键字参数:也称为命名参数,在参数传递时,实参利用形参变量的名将值传递给指定形参
1
2
def say(greeting, describe, target):
    print(f'{greeting} {describe} {target}!')
1
2
say('Hello', 'beautiful', 'Python')                           # 位置参数
say(target='Python', greeting='Hello', describe='beautiful')  # 关键字参数
1
2
Hello beautiful Python!
Hello beautiful Python!
  • 调用函数时也可以混合使用位置参数和关键字参数,但是要求所有位置参数必须出现在关键字参数之前
1
say('Hello', target='Python', describe='beautiful')
1
Hello beautiful Python!
1
say(greeting='Hello', 'beautiful', 'Python')
1
2
3
4
  File "<ipython-input-18-33a91a139779>", line 1
    say(greeting='Hello', 'beautiful', 'Python')
                         ^
SyntaxError: positional argument follows keyword argument
  • 使用位置参数时要求实参和形参的顺序必须一致
  • 使用关键字参数时,实参的顺序是无所谓的,只要正确指定形参名即可
  • 在定义函数时,可以指定一部分参数在调用时必须使用关键字参数

  • 强制关键字参数

  • 形参列表中可使用一个特殊的元素*,表示位于它左侧的参数在调用时可以使用位置参数也可以使用关键字参数;但位于它右侧的参数必须使用关键字参数,因此称为__强制关键字参数__
1
2
3
4
5
def say(greeting, describe, *, target):
    print(f'{greeting} {describe} {target}!')

say('Hello', 'beautiful', target='Python')
say('Hello',  'beautiful', 'Python')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Hello beautiful Python!



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-19-7a9dfada02dc> in <module>
      3 
      4 say('Hello', 'beautiful', target='Python')
----> 5 say('Hello',  'beautiful', 'Python')


TypeError: say() takes 2 positional arguments but 3 were given
  • 强制位置参数
  • 形参列表中可使用一个特殊元素/,表示位于它左侧的参数在调用时必须使用位置参数

  • 下例中,参数greeting必须使用位置参数,target必须使用关键字参数,describe可以使用位置参数也可以使用关键字参数

1
2
def say(greeting, /, describe, *, target):
    print(f'{greeting} {describe} {target}!')

4.2.2 可选参数

  • 定义函数的时候可以指定参数的默认值,有默认值的参数称为可选参数
  • 在调用的时候
    • 如果实参列表中没有给出可选参数的值,则函数体中会使用默认值进行计算
    • 如果实参列表中给出可选参数的值,则其默认值会被覆盖掉
1
2
3
4
5
def say(greeting, describe='beautiful', target='Python'):
    print(f'{greeting} {describe} {target}!')

say('Hello')
say('Hello', 'powerful')
1
2
Hello beautiful Python!
Hello powerful Python!
  • 在函数定义中可选参数必须出现在所有非可选参数之后
1
2
def say(greeting='Hello', describe, target):
    print(f'{greeting} {describe} {target}!')
1
2
3
4
  File "<ipython-input-23-3c50971decba>", line 1
    def say(greeting='Hello', describe, target):
           ^
SyntaxError: non-default argument follows default argument

4.2.3 可变参数

  • 可变参数是指在函数调用时,实参的数量是不确定的,可以根据需要任意变化
  • 两种可变参数
  • *修饰的参数用于收集实参中的位置参数
    • 可变的位置参数被构建为一个元组
  • **修饰的参数用于收集实参中的关键字参数
    • 可变的关键字参数则被构建为一个字典
1
2
3
def say(greeting='Hello', *describe):
    print(type(describe))
    print(f'{greeting} {" and ".join(describe)} Python!')    
1
say('Hello', 'beautiful', 'powerful', 'easy')
1
2
<class 'tuple'>
Hello beautiful and powerful and easy Python!
  • *修饰的参数可位于形参列表的任意位置,但是它右侧的参数在调用时必须使用关键字参数
1
2
def say(greeting, *describe, target):
    print(f'{greeting} {" and ".join(describe)} {target}!')
1
say('Hello', 'beautiful', 'powerful', 'easy', target='Python')
1
Hello beautiful and powerful and easy Python!
  • **修饰的参数必须位置于形参列表的最后
  • 可同时使用***来收集可变数量的位置参数和关键字参数
1
2
3
4
5
def say(greeting, *describe, **detail):
    print(type(describe), type(detail))
    print(f'{greeting} {" and ".join(describe)} Python!')
    details = [f"It's {k} is {v}." for k, v in detail.items()]
    print(' '.join(details))
1
say('Hello', 'beautiful', 'powerful', 'easy', syntax='elegant', code='simple')
1
2
3
<class 'tuple'> <class 'dict'>
Hello beautiful and powerful and easy Python!
It's syntax is elegant. It's code is simple.

4.2.4 参数分配

  • 在函数调用时,使用这***构造实参列表
  • *用于将序列或元组拆解开来分配给形参中的位置参数
  • **用于将字典拆解开来分配给形参中的关键字参数
1
2
3
4
5
6
7
8
def say(greeting, describe, target):
    print(f'{greeting} {describe} {target}!')

para_tuple = ('Hello', 'beautiful', 'Python')
say(*para_tuple)

para_dict = {'greeting': 'Hello', 'describe': 'beautiful', 'target': 'Python'}
say(**para_dict)
1
2
Hello beautiful Python!
Hello beautiful Python!

4.3 函数的类型注解*

4.3.1 类型注解

  • 函数的类型注解功能最早出现在Python 3.5中
  • 与变量的类型注解相同,函数的类型注解也只是一种提示信息,主要为了提高代码的可读性、自动化文档生成、为开发工具提供方便等

  • 函数的类型注解语法

def func(arg: type, optarg: type = default) -> return_type: - arg:形参名; - type:形参的类型; - optarg:可选参数; - default:默认值; - return_type:返回值的类型。

1
2
def add(x: int, y: int) -> int:
    return x + y
1
2
print(add(1, 1))
print(add(1.0, 1.0))
1
2
2
2.0
  • 添加了类型注解的函数,可以使用__annotations__属性来查看注解信息
1
print(add.__annotations__)
1
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
  • mypy
  • Python解释器不会利用函数的类型注解来检查参数的类型,但是它们可以被一些第三方编译工具利用
  • mypy就是一个静态类型版本的Python实现
    • 提供了一个基于CPython的编译器mypyc,它能够像静态语言那样先将Python代码编译为二进制文件,从而提高程序的运行效率
  • pip install mypy

将下面的代码保存为名为add.py的文件:

1
2
3
def add(x: int, y: int) -> int:
    return x + y 
add(1.0, 1.0)
1
2.0

利用mypy来执行,它显示出函数add的两个实参数值与形参的类型不匹配的错误。

1
2
3
4
$ mypy add.py
add.py:3: error: Argument 1 to "add" has incompatible type "float"; expected "int"
add.py:3: error: Argument 2 to "add" has incompatible type "float"; expected "int"
Found 2 errors in 1 file (checked 1 source file)

4.3.2 typing模块

  • 对于一些复杂的数据类型,如listtuplesetdict等,类型注解并不能完全反映变量的结构
  • 例:
  • 下面的函数中,参数nums注解为一个list,但是nums中究为什么类型的数值并不确定,需要添加更为细节的信息
1
2
3
def average(nums:list) -> float:
    n = len(nums)
    return sum(nums)/n
  • typing模块中提供了一些辅助类型,能够为类型添加更具体的注解信息

  • typing.List

  • 利用typing中的类型List可以将上例改写为如下形式,将参数nums注解为一个元素为float的列表
1
2
3
4
5
from typing import List
def average(nums:List[float]) -> float:
    n = len(nums)
    return sum(nums)/n
average([1.0, 4.0, 2.0, 8.0, 5.0, 7.0])
1
4.5
  • typing.List也可以嵌套使用,例如data: List[List[int]]=[[1, 4], [2, 8], [5, 7]]

  • typing.Tuple

  • 由于tuple是不可变类型,因此可以为其每个元素添加类型注解
1
2
3
4
from typing import Tuple
def create_student(name: str, age:int, gender:str) -> Tuple[str, int, str]:
    return (name, age, gender)
create_student('张三', 18, '男')
1
('张三', 18, '男')
  • typing.Sequence
  • 有时候函数的参数需要一个序列类型,listtuplestr都可以,则可以使用tying.Sequence
1
2
3
4
5
6
from typing import Sequence
def average(nums: Sequence[int]) -> float:
    n = len(nums)
    return sum(nums)/n
average([1, 4, 2, 8, 5, 7])
average((1, 4, 2, 8, 5, 7))
  • typing.Union
  • 当变量类型可以是多种类型中的一种时,可以使用typing.Union
1
2
3
4
5
6
from typing import Sequence, Union
def average(nums: Union[Sequence[int], Sequence[float]]) -> float:
    n = len(nums)
    return sum(nums)/n
average([1.0, 4.0, 2.0, 8.0, 5.0, 7.0])
average((1, 4, 2, 8, 5, 7))
  • typing.Any
  • typing.Any表示变量可以是任意类型,下面的函数中参数seq被标注为元素类型任意的序列,返回值类型也是如此
1
2
3
from typing import Sequence, Any
def reverse(seq: Sequence[Any]) -> Sequence[Any]:
    return seq[-1::-1]
  • typing.Optional
  • typing.Optional表示变量可以是指定的类型或者None
1
2
3
4
5
6
from typing import Union, Optional
def div(dividend: Union[int, float], divisor: Union[int, float]) -> Optional[float]:
    if divisor == 0:
        return None
    else: 
        return dividend/divisor
  • Optional[float]Union[float, None]等价

  • typing.NoReturn

  • typing.NoReturn表示函数中没有返回值,包括显式(Return)或隐式(函数默认返回None
1
2
3
from typing import NoReturn
def error(info: str) -> NoReturn:
    raise Exception(info)
  • typing模块中的其他常用类型
类型 描述
typing.Dict 字典
typing.Set 集合
typing.Generator 生成器
typing.Iterator 迭代器
typing.Callable 可调用类型

4.3.3 类型注解的使用

  • 类型注解在Python代码的运行过程中不起任何作用,使用与否不会对代码的功能产生影响。
  • 什么情况下应该使用类型注解呢?
  • 初学者暂时不必考虑使用类型注解;
  • 在很短的代码模块中使用注解的意义不大;
  • 若是为了开发供他人使用的工具包,最好使用类型注解;
  • 较大的软件项目中要使用类型注解。

4.4 函数对象

4.4.1 一等对象

  • 面向对象编程中的对象
  • 具有能够区别于其他所有对象或数值的唯一标识
  • 每个对象都有至少一个“类型”
  • 具有可供访问的属性和可供调用的方法

  • 一等对象(First-class object)

  • 一等对象可以是任何程序中的实体,而不一定是面向对象程序设计所指的“对象”
  • 特征

    • 可以被赋值给变量,或者作为数据结构的元素;
    • 可以作为参数传递给其他函数;
    • 可以作为函数的返回值;
    • 能够在程序运行期间动态创建。
  • Python中数值、列表、字典、集合等都是一等对象

  • 面向对象中的“对象”也是一等对象
  • 而函数则比较特殊,在不同的语言中差别很大
  • C语言和C++语言中的函数不是一等对象,因为它们不能在运行中动态创建,必须在程序设计时在代码中确定下来
  • Java和C#中本质上没有函数只有方法,所有的方法必须隶属于某一个类或对象
  • Python和Javascript中,函数是一等对象

4.4.2 Python函数的面向对象特征

  • 每个函数都具有一个唯一的标识
1
2
3
4
5
def fun1():
    pass

def fun2():
    pass
1
2
print(id(fun1), id(fun2))
print(id(fun1) == id(fun2))
1
2
4520867008 4520868304
False
  • Python函数的类型
1
type(fun1)
1
function
1
type(fun2)
1
function
  • Python函数的属性和方法
1
2
def fun():
     """This is doc string of the function"""
1
fun.__doc__
1
'This is doc string of the function'
1
fun.__dir__()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
['__repr__',
 '__call__',
 '__get__',
 '__new__',
 '__closure__',
 '__doc__',
 '__globals__',
 '__module__',
 '__code__',
 '__defaults__',
 '__kwdefaults__',
 '__annotations__',
 '__dict__',
 '__name__',
 '__qualname__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__init__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']

4.4.3 Python函数的一等对象特征

  • 函数赋值
  • Python中可以将函数赋值给一个变量,而被赋值的变量能够像函数一样被调用
  • 也作为一个数据结构的元素
1
2
def say(greeting, describe, target):
    print(f'{greeting} {describe} {target}!')
1
2
new_say = say
new_say('Hello', 'beautiful', 'python')
1
Hello beautiful python!
1
2
3
4
def fun1():
    print('fun1')
def fun2():
    print('fun2')
1
2
3
fun_list = [fun1, fun2]
for f in fun_list:
    f()
1
2
fun1
fun2
  • 高阶函数
  • 能够接收函数作为参数,或者返回值为函数的函数
1
2
3
4
5
6
7
8
def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

def compute(x, y, fun):
    return fun(x, y)
1
compute(1, 1, add)
1
2
1
compute(1, 1, subtract)
1
0
  • sorted
1
2
def sort_key(e):
    return e[1]
1
2
lst = [(1, 4), (2, 8), (5, 7)]
sorted(lst, key=sort_key)
1
[(1, 4), (5, 7), (2, 8)]
  • 匿名函数
  • lambda表达式
1
2
lst = [(1, 4), (2, 8), (5, 7)]
sorted(lst, key=lambda e: e[1])
1
[(1, 4), (5, 7), (2, 8)]
  • 特点
  • lambda表达式的函数体中不能赋值,也不能使用iffor等流程控制语句,以及return语句
  • lambda表达式在Python中基本上只用于作为函数参数使用,一般情况下仅包含一个表达式
  • lambda表达式实现复杂的函数功能会影响代码的可读性,这种情况下最好使用def定义一个一般的函数

  • 动态创建函数

  • 函数在程序运行过程中动态地创建出来
1
2
3
4
5
6
f_str = '''
def dynamic_fun():
    print('Hello dynamic function')

'''
exec(f_str)
1
dynamic_fun()
1
Hello dynamic function
  • 使用types.FunctionType动态创建函数
1
2
3
4
5
6
7
8
import types
fun_str = '''
def dynamic_fun():
    print('Hello dynamic Python!')
'''

code_obj = compile(fun_str, filename='', mode='exec')  # mode: 'exec'、'eval'或'single'
fun = types.FunctionType(code_obj.co_consts[0], globals())
1
fun()
1
Hello dynamic Python!

4.5 嵌套函数与闭包

4.5.1 嵌套函数

  • 嵌套函数是指一个函数(外部函数)中定义了另一个函数(内部函数)
  • 是一种动态创建函数的方法
  • 内部函数只能在外部函数中使用
  • 有一定程度的“封装”作用
1
2
3
4
5
def outer():
    def inner():
        print("This is from inner")
    print("This is from outer")
    inner()
1
outer()
1
2
This is from outer
This is from inner

nonlocal

  • 外部函数中定义的变量相当于其内部的全局变量,在内部函数中能够访问但不能修改
  • 使用nonlocal在内部函数中将变量称为非局部变量,就能够对其进行修改了
1
2
3
4
5
6
7
def outer():
    x = 0
    def inner():
        nonlocal x
        x = 1
    inner()
    print(x)
1
outer()
1
1
  • nonlocal只能声明外部函数中已定义的变量为非局部变量。若内部函数中声明了外部未定义的变量,则会抛出错误
1
2
3
4
5
def outer():
    def inner():
        nonlocal x
        x = 0
    inner()
1
2
3
4
  File "<ipython-input-6-a818351f88c3>", line 3
    nonlocal x
    ^
SyntaxError: no binding for nonlocal 'x' found

4.5.2 闭包

概念与原理

  • 闭包是指引用了自由变量的函数
  • 自由变量:是指被函数所使用,但是不在函数中定义,而是在函数运行的上下文中定义的变量
  • 闭包是引用了自由变量的函数,以及它们共同存在的上下文所组成的一个相对独立的实体

  • 构成闭包的三个条件

  • 定义了一个嵌套函数
  • 内部函数中引用了外部函数中定义的变量
  • 返回值是内部函数的引用

  • 利用闭包实现累积求均值

1
2
3
4
5
6
7
8
9
def create_averager():
    numbers = []
    def avg(nums):
        if isinstance(nums, list):
            numbers.extend(nums)
        else:
            numbers.append(nums)
        return sum(numbers)/len(numbers)
    return avg
1
averager = create_averager()
1
averager([3, 5])
1
4.0
1
averager(7)
1
5.0
1
averager([8, 9])
1
6.4
  • 闭包的运行原理
  • 普通函数调用的特点
    • 会在栈上压入函数的参数及函数中定义的局部变量,构建执行环境
    • 函数执行完毕返回时,运行环境会被销毁,参数与局部变量也将不复存在
    • 再次调用函数会生成新的环境
  • 闭包的不同之处
    • 外部函数在返回的时候,局部变量作为自由变量被内部函数所引用
    • 内部函数被作为闭包返回至外部环境
    • 闭包被销毁之前,它对外部函数中定义的自由变量的引用始终存在,因而自由变量不会被销毁
    • 自由变量成为一种特殊的存在,它即不是全局变量,也不是局部变量
  • 闭包的根本作用
    • 利用一种特殊的方式延伸了变量的作用范围

闭包的使用要点

  • 闭包并非由嵌套函数定义,而是通过调用外层函数创建出来
  • 每次调用都会创建出新的闭包
  • 不同的闭包之间是相互独立的,它们各自有着自己的自由变量
1
2
averager1 = create_averager()
averager2 = create_averager()
1
averager1([3, 5])
1
4.0
1
averager2([4, 6])
1
5.0
  • 利用闭包的__code__属性,可以查看局部变量和自由变量
1
averager.__code__.co_freevars
1
('numbers',)
1
averager.__code__.co_varnames
1
('nums',)
  • 自由变量的值,保存在__closure__属性之中,它是一个元素为cell对象的元组
1
averager.__closure__[0].cell_contents
1
[3, 5, 7, 8, 9]
  • 当嵌套函数的外部函数中定义了多个内部函数并返回它们,并且它们引用了相同的自由变量,那么返回的函数和自由变量处于同一个闭包之中,共享相同的自由变量
1
2
3
4
5
6
7
8
9
def create_averagers():
    numbers = []
    def avg1(num):
        numbers.append(num)
        return sum(numbers)/len(numbers)
    def avg2(num):
        numbers.append(num)
        return sum(numbers)/len(numbers)
    return avg1, avg2
1
averager1, averager2 = create_averagers()
1
averager1(3)
1
3.0
1
averager2(5)
1
4.0

实例

利用闭包实现任意一元函数

一元\(n\)次函数可写为: $$ f(x) = p_0x^n + p_1x^{n-1} + p_2 x^{n-2} + \cdots + p_{n-1}x + p_{n} $$ 给定一组参数\(p=(p_0,p_1,p_2,\cdots,p_n)\)就确定了一个一元函数。下面的例子中,外层函数接收定义一元函数的参数\(p\),返回一个由该参数确定的一元函数。

1
2
3
4
5
6
7
8
def create_univariate_func(*prams):
    def univariate_func(x):
        y = 0
        order = len(prams) - 1
        for i, p in enumerate(prams):
            y += p * x**(order - i)
        return y
    return univariate_func

当参数为\(p=(1,2,3)\)时,定义的一元函数为\(f(x) = x^2+2x+3\)

1
uni_func = create_univariate_func(1, 2, 3)
1
uni_func(1)
1
6
1
uni_func(2)
1
11
1
uni_func(3)
1
18

4.6 函数装饰器

  • 装饰器(Decorator)是用于修饰函数、类或者方法的一种特殊的工具,它能够在不 侵入代码的情况下增加或者改变修饰目标的功能
  • 利用函数实现
  • 利用类实现

4.6.1 简单函数装饰器

  • Python装饰器(Decorator)是对函数或方法的修饰
  • 利用装饰器,能够在不侵入代码的情况下为函数或方法动态地添加新的功能
  • 类型
  • 基于函数的装饰器
  • 基于类的装饰器

  • 函数装饰器的实现利用了函数的对象特征,以及嵌套函数或闭包的运行思想

  • 使用形式
1
2
3
@装饰器
def 函数():
    ... ...
  • 一个能够输出函数的运行时间的装饰器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import time

def run_time(fun):
    def wrapper():
        start = time.time()
        result = fun()
        end = time.time()
        print(f'函数{fun.__name__}的运行时间为{(end-start):.4}秒')
        return result
    return wrapper
  • 装饰器的名字就是定义的嵌套函数中外层函数的名字
  • 由于装饰器没有侵入定义函数的代码,因此在被修饰函数内部感知不到装饰器的存在
1
2
3
4
5
6
7
8
9
@run_time
def fun1():
    for _ in range(10):
        time.sleep(0.1)

@run_time
def fun2():
    for _ in range(10):
        time.sleep(0.2)
  • 装饰器同样不会侵入调用函数的代码,因此经过装饰器修饰的函数在调用时与普通函数没有任何区别
1
fun1()
1
函数fun1的动行时间为1.031秒
1
fun2()
1
函数fun2的动行时间为2.02秒

4.6.2 函数装饰器的工作原理

装饰器的实际执行过程

  • 函数装饰器只是一种令代码更加简洁的语法糖
  • 下面的例子实现了与装饰器完全相同的功能
1
2
3
4
5
6
7
def fun1():
    for _ in range(10):
        time.sleep(0.1)

def fun2():
    for _ in range(10):
        time.sleep(0.2)
1
2
f1 = run_time(fun1)
f1()
1
函数fun1的动行时间为1.033秒
1
2
f2 = run_time(fun2)
f2()
1
函数fun2的动行时间为2.034秒

两种方法是等价的, 装饰器的优势是完全不必改变任何函数定义或调用的代码逻辑,就能够为函数增加新的功能或行为

装饰器的运行时机

  • 装饰器的执行过程包括如下几个步骤
  • 定义装饰器
  • 定义函数
  • 装饰函数
  • 调用装饰之后的函数

  • 它们的运行顺序是怎样的呢?

将下面的代码保存为一个脚本文件run_chance.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def run_chance(fun):
    print(f'装饰函数 {fun.__name__}')
    return fun

@run_chance
def fun1():
    print('运行函数fun1')

@run_chance
def fun2():
    print('运行函数fun2')

if __name__ == '__main__': 
    fun1()
    fun2()

在终端运行python run_chance.py,输出结果为:

1
2
3
4
5
$ python run_chance.py
装饰函数 fun1
装饰函数 fun2
运行函数fun1
运行函数fun2

装饰器对函数的修饰不是在函数调用时执行,而是在函数定义时就执行的

  • 在其他模块中导入被装饰器修饰的函数时

1
from run_chance import *
输出结果为:
1
2
装饰函数 fun1
装饰函数 fun2

结果表明,装饰器在导入模块时就被执行了

装饰器与闭包的关系

  • 装饰器就是闭包
  • 装饰器外层函数的参数为函数
  • 返回为内层定义的函数
  • 内层函数中引用了外层中的形参变量

4.6.3 函数装饰器的优化

含参函数的装饰

  • 修饰函数包含参数,则需要内层函数参够处理这些参数
  • 利用函数的可变参数方便地实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import time

def run_time(fun):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fun(*args, **kwargs)
        end = time.time()
        print(f'函数{fun.__name__}的动行时间为{(end-start):.4}秒')
        return result
    return wrapper
1
2
3
4
5
6
7
8
9
@run_time
def fun1():
    for _ in range(10):
        time.sleep(0.1)

@run_time
def fun2(t):
    for _ in range(t):
        time.sleep(0.1)
1
fun1()
1
函数fun1的动行时间为1.032秒
1
fun2(10)
1
函数fun2的动行时间为1.034秒

被修饰函数的元信息

  • 经过修饰的函数与被修饰函数并非同一个函数
  • 函数的元信息丢失,例如__name__以及__doc__
1
fun1.__name__
1
'wrapper'
1
fun2.__name__
1
'wrapper'
  • 为了保留函数的元信息,需要在装饰器定义时手动地将被修饰函重要的元信息保存至内层函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import time

def run_time(fun):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fun(*args, **kwargs)
        end = time.time()
        print(f'函数{fun.__name__}的动行时间为{(end-start):.4}秒')
        return result
    wrapper.__name__ = fun.__name__
    wrapper.__doc__ = fun.__doc__
    return wrapper

@run_time
def fun():
    for _ in range(10):
        time.sleep(0.1)
1
fun.__name__
1
'fun'
  • 使用functools.wraps装饰器实现元信息的复制
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import time
from functools import wraps

def run_time(fun):
    @wraps(fun)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fun(*args, **kwargs)
        end = time.time()
        print(f'函数{fun.__name__}的动行时间为{(end-start):.4}秒')
        return result
    return wrapper

@run_time
def fun():
    for _ in range(10):
        time.sleep(0.1)
    print('fun')
1
fun.__name__
1
'fun'
  • 利用functools.update_wrapper函数实现元信息的复制
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import time
from functools import update_wrapper

def run_time(fun):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fun(*args, **kwargs)
        end = time.time()
        print(f'函数{fun.__name__}的动行时间为{(end-start):.4}秒')
        return result
    return update_wrapper(wrapper, fun)

@run_time
def fun():
    for _ in range(10):
        time.sleep(0.1)
    print('fun')
  • __wrapped__属性
  • 使用wraps装饰器或update_wrapper函数之后,可以通过被修饰函数的__wrapped__属性获得原始函数的引用
1
fun.__wrapped__.__name__
1
'fun'
1
fun.__wrapped__()
1
fun

4.6.4 装饰器的叠加*

叠加装饰器的定义与使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def dec1(fun):
    def wrapper(*args, **kwargs):
        print('dec1')
        return fun(*args, **kwargs)
    return wrapper

def dec2(fun):
    def wrapper(*args, **kwargs):
        print('dec2')
        return fun(*args, **kwargs)
    return wrapper

@dec1
@dec2
def fun():
    print('fun')
1
fun()
1
2
3
dec1
dec2
fun

叠加装饰器的原理

  • 函数叠加使用多个装饰器修饰时,其作用原理与单个装饰器完全相同
  • 只不过,外层装饰器的修饰对象并不是函数,而是内层装饰器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def dec1(fun):
    def wrapper(*args, **kwargs):
        print('dec1')
        return fun(*args, **kwargs)
    return wrapper

def dec2(fun):
    def wrapper(*args, **kwargs):
        print('dec2')
        return fun(*args, **kwargs)
    return wrapper

def fun():
    print('fun')
1
dec1(dec2(fun))()
1
2
3
dec1
dec2
fun

叠加装饰器的执行顺序

  • 被修饰函数调用之前的代码先执行
  • 执行顺序为先外层后内层
  • 接下来执行被修饰函数
  • 最后执行被修饰函数之后的代码
  • 执行顺序为先内层后外层
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import wraps

def dec1(fun):
    @wraps(fun)
    def wrapper(*args, **kwargs):
        print('dec1 before ...')
        result = fun(*args, **kwargs)
        print('dec1 after ...')
        return result
    return wrapper

def dec2(fun):
    @wraps(fun)
    def wrapper(*args, **kwargs):
        print('dec2 before ...')
        result = fun(*args, **kwargs)
        print('dec2 after ...')
        return result
    return wrapper

@dec1
@dec2
def fun():
    print('完成fun的功能')
1
fun()
1
2
3
4
5
dec1 before ...
dec2 before ...
完成fun的功能
dec2 before ...
dec1 after ...

4.6.5 含参装饰器

  • 装饰器实际上是一种高阶函数
  • 函数可以有参数,因此装饰器也可以有参数
  • 定义带参数的装饰器需要使用一个三层的嵌套函数
  • 最外层的函数称为装饰器工厂函数
  • 内部的两层函数是真正的装饰器定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def param_dec(arg1, arg2): 
    def dec(fun):
        def wrapper(*args, **kw):
            print(f"before actions based on {arg1} and {arg2} ...")
            result = fun(*args, **kw)
            print(f"after actions based on {arg1} and {arg2} ...")
            return result
        return wrapper
    return dec

@param_dec('arg1', 'arg2')
def fun():
    print('完成fun的功能')
1
fun()
1
2
3
before actions based on arg1 and arg2 ...
完成fun的功能
after actions based on arg1 and arg2 ...
  • 能够根据参数将函数运行时间的单位显示为秒、毫秒或者微秒的装饰器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from datetime import datetime
from functools import wraps

def run_time(unit):
    def dec(fun):
        @wraps(fun)
        def wrapper(*args, **kwargs):
            start = datetime.now()
            result = fun(*args, **kwargs)
            end = datetime.now()
            if unit == 'ms':
                time_str = f'{(end - start).total_seconds()*1000:.0f}毫秒'
            elif unit == 'us':
                time_str = f'{(end - start).total_seconds()*10**6:.0f}微秒'
            else:
                time_str = f'{(end - start).total_seconds():.0f}秒'
            print(f'函数{fun.__name__}的动行时间为{time_str}')
            return result
        return wrapper
    return dec
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@run_time('s')
def fun1():
    for _ in range(10):
        time.sleep(0.1)

@run_time('ms')
def fun2():
    for _ in range(10):
        time.sleep(0.1)

@run_time('us')
def fun3():
    for _ in range(10):
        time.sleep(0.1)
1
fun1()
1
函数fun1的动行时间为1秒
1
fun2()
1
函数fun2的动行时间为1034毫秒
1
fun3()
1
函数fun3的动行时间为1037261微秒

4.6.6 函数装饰器应用实例 *

  • 利用装饰器及函数参数的类型注解来实现类型检查
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from inspect import signature
from functools import wraps

def type_check(fun):
    @wraps(fun)
    def wrapper(*args, **kwargs):
        sig = signature(fun)          # 函数的签名
        param_dict = sig.parameters   # 函数的{参数: 类型}字典
        param_list = list(param_dict.values())
        for i, k in enumerate(args):  # 检查位置参数
            assert type(k) is param_list[i].annotation,\
                f'第{i+1}个参数的类型必须为{param_list[i].annotation}'
        for k, v in kwargs.items():   # 检查关键字参数
            assert type(v) is param_dict[k].annotation, \
                f'参数{k}的类型必须为{param_dict[k].annotation}'
        return fun(*args, **kwargs)
    return wrapper
1
2
3
@type_check
def fun(x: int, y: float):
    pass
1
fun(1, 2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-44-3f43dc183e44> in <module>
----> 1 fun(1, 2)


<ipython-input-42-2b636f173ed3> in wrapper(*args, **kwargs)
      9         param_list = list(param_dict.values())
     10         for i, k in enumerate(args):  # 检查位置参数
---> 11             assert type(k) is param_list[i].annotation,\
     12                 f'第{i+1}个参数的类型必须为{param_list[i].annotation}'
     13         for k, v in kwargs.items():   # 检查关键字参数


AssertionError: 第2个参数的类型必须为<class 'float'>
1
fun(1.0, 2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-45-be3945dfd467> in <module>
----> 1 fun(1.0, 2)


<ipython-input-42-2b636f173ed3> in wrapper(*args, **kwargs)
      9         param_list = list(param_dict.values())
     10         for i, k in enumerate(args):  # 检查位置参数
---> 11             assert type(k) is param_list[i].annotation,\
     12                 f'第{i+1}个参数的类型必须为{param_list[i].annotation}'
     13         for k, v in kwargs.items():   # 检查关键字参数


AssertionError: 第1个参数的类型必须为<class 'int'>
1
fun(1, y=2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-46-85690ed67ee6> in <module>
----> 1 fun(1, y=2)


<ipython-input-42-2b636f173ed3> in wrapper(*args, **kwargs)
     12                 f'第{i+1}个参数的类型必须为{param_list[i].annotation}'
     13         for k, v in kwargs.items():   # 检查关键字参数
---> 14             assert type(v) is param_dict[k].annotation, \
     15                 f'参数{k}的类型必须为{param_dict[k].annotation}'
     16         return fun(*args, **kwargs)


AssertionError: 参数y的类型必须为<class 'float'>
1
fun(1, 2.0)

4.6.7 重要的 Python 内置装饰器 *

lru_cache

  • 将被修饰函数的运行结果缓存起来,后续调用的时候如果函数参数与已缓存结果的参数相同,则不再执行函数,直接将缓存的结果返回
1
2
3
4
5
6
7
8
9
from functools import lru_cache

@lru_cache
def fun(i):
    print(f'计算 {i} 的平方')
    return i**2

for _ in range(5):
    print(fun(1))
1
2
3
4
5
6
计算 1 的平方
1
1
1
1
1
  • lru_cache的参数
  • maxsize:缓存的大小
  • typed:缓存结果会考虑参数的数据类型
1
2
3
4
5
from functools import lru_cache
@lru_cache(typed=True)
def fun(i):
    print(f'计算 {i} 的平方')
    return i**2
1
fun(1)
1
2
3
4
5
6
7
计算 1 的平方





1
1
fun(1.0)
1
2
3
4
5
6
7
计算 1.0 的平方





1.0
  • lru_cache的缓存是利用字典来实现的
  • 字典的键基于被修饰函数的参数来构建的
  • 求被修饰函数的参数必须是可哈希的(hashable)
1
2
3
4
5
from functools import lru_cache
@lru_cache(10)
def average(values):
    n = len(values)
    return sum(values)/n
1
average((1, 2, 3))
1
2.0
1
average([1, 2, 3])
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-55-5e145017fa0a> in <module>
----> 1 average([1, 2, 3])


TypeError: unhashable type: 'list'

singledispatch

  • 使用singledispatch装饰器可实现类似于函数重载的功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from functools import singledispatch

@singledispatch
def average(data):
    n = len(data)
    return sum(data)/n

@average.register(dict)   # 参数为字典时
def _(data):
    data = data.values()
    n = len(data)
    return sum(data)/n

@average.register(str)    # 参数为字符串时
def _(data):
    data = [float(i) for i in data.split(',')]
    n = len(data)
    return sum(data)/n
1
average([1, 2, 3])
1
2.0
1
average({'a': 1, 'b': 2, 'c': 3})
1
2.0
1
average('1, 2, 3')
1
2.0
  • singledispatch将被修饰函数转变成一个名为xxx.register的装饰器
  • 与使用xxx.register修饰的一组函数被绑定在一起,组成了一个泛函数(Generic Function)
  • 当调用函数的时候,会根据参数的类型确定被实际被调用的函数
  • xxx.register修饰的函数名字是无关紧要的,因此常使用_来替代

4.7 常用函数编程工具*

4.7.1 常用工具函数

  • functools.partial
  • 固定一个函数的部分参数值,返回一个以其余参数为参数的可调用对象

函数fun是以a、b、c为参数的一元二次函数\(ax^2+bx+c\)

1
2
def fun(a, b, c, x):
    return a*x**2 + b*x +c

利用partial将参数a、b、c固定为1、2、3,返回的\(f\)表示\(x^2+2x+3\)

1
2
from functools import partial
f = partial(fun, 1, 2, 3)
1
f(5)
1
38
  • mapfilter
  • map函数的作用是将一个函数作用于可迭代对象中的每一个元素之上,并返回由每个元素的运算结果构成的新的可迭代对象
  • filter的作用是对可迭代对象的元素进行过滤
1
2
3
4
lst = [1, 4, 2, 8, 5, 7]
lst_square = map(lambda x: x**2, lst)
list(lst_square)
[1, 16, 4, 64, 25, 49]
1
[1, 16, 4, 64, 25, 49]
1
2
lst_f = filter(lambda x: x%2==0, lst)
list(lst_f)
1
[4, 2, 8]
  • functools.reduce
  • 函数fun必须接受两个参数,并返回一个值
  • 它首先接受可迭代对象的前两个元素A和B,并计算fun(A, B),然后处理第三个元素C,计算fun(fun(A, B), C),以次类推直至将可迭代对象中的元素处理完毕
  • functools.reduce返回最后一次fun的执行结果
1
2
3
from functools import reduce
lst = [1, 4, 2, 8, 5, 7]
reduce(lambda r, e: r * e, lst)
1
2240
  • anyall
  • 检查一个可迭代对象的逻辑值,并返回TrueFalse
  • any在可迭代对象中的任意元素的逻辑值为真时返回True
  • all则在所有元素的逻辑值都为真时返回True
1
2
lst = [1, 4, 2, 8, 5, 7, 0]
any(lst)
1
True
1
all(lst)
1
False

4.7.2 operator模块

  • operator模块中包含了一系列对应于 Python 运算符的函数
  • 数学运算: addsubmultruediv,floordiv
  • 逻辑运算: not_truth
  • 比较运算: eqneltlegtge
  • 对象确认: is_is_not

4.7.3 itertools模块

函数 功能描述
count(start,step) 创建一个从start开始,步长为step的可迭代对象
cycle(iter) 创建一个将可迭代对象iter重复无限多次的可迭代对象
repeat(elem, [n]) 创建一个将元素elem重复n次的可迭代对象,n为空时重复无限多次
chain(iterA, iterB, ...) 将多个可迭代对象拼接起来生成一个新的可迭代对象
islice(iter, [start], stop, [step]) 截取iter的部分元素构成新的可迭代对象
tee(iter, [n]) 将可迭代对象iter复制n次,n默认值为2
starmap(func, iter) iter为以元组为元素的可迭代对象,starmapiter的元素作为函数func的参数,返回func的执行结果构成的可迭代对象
filterfalse(predicate, iter) filter函数相反,返回所有让 predicate 返回False 的元素
takewhile(predicate, iter) 返回一直让 predicate返回 True 的元素,一旦 predicate 返回 False就终止迭代
dropwhile(predicate, iter) takewhile相反,当predicate返回True 的时候丢弃元素,返回可迭代对象的其余元素
compress(data, selectors) 返回dataselectorsTrue的相应位置的元素
groupby(iter,key_func=None) key_funciter中元素的处理结果为key,对iter中的元素进行分组