Python三大器

学习Python三大器——装饰器、迭代器、生成器,让你的代码更加Pythonic!

一. 装饰器(decorator

1. 函数的本质

在Python中,万物皆对象,函数也不例外。

当我们在Python中定义一个函数的时候,我们就是在新建一个变量,并在这个变量里保存了一个函数对象。

(1) 函数作为参数

函数作为一个对象,也可以像其他对象一样被当作参数,传入到其他函数里。

如:

1
2
3
4
5
6
7
8
9
10
11
def double(x):
return x*2

def triple(x):
return x*3

def calc_number(func, x):
print(func(x))

calc_number(double,3)
calc_number(triple,3)

在这里,calc_number函数是高阶函数(将函数作为参数的函数),doubletriple函数是回调函数(被作为参数传入函数的函数)。

由此可见,函数完全可以像其他的变量一样被相互传递。

(2) 函数作为返回值

函数本身也可以成为一个返回值。

如:

1
2
3
4
5
6
7
8
9
10
11
12
def get_multiple_func(n):

def multiple(x):
return n * x

return multiple

double = get_multiple_func(2)
triple = get_multiple_func(3)

print(double(3))
print(triple(3))

在这里,get_multiple_func它本身是一个函数,返回值也是一个函数,而它返回的这个函数做什么事情是和它得到的参数n相关的。multiple函数是闭包函数(一个函数要成为闭包,需要满足三个条件:1. 函数嵌套;2. 外函数返回内函数;3. 内函数引用外函数的变量),它能够“记住”外部作用域里的函数变量(即使外部函数已经返回),装饰器就是闭包的一个典型应用。

2. 装饰器

装饰器是一种特殊的函数,它接受一个函数作为参数,并返回一个新的函数(通常是包装过的原函数),用于在不修改原函数代码的情况下增强其功能。

简单来讲,装饰器就是一个参数是函数、返回值也是函数的函数

这是一个极简的“装饰器”:

1
2
3
4
5
6
7
8
9
10
11
12
def dec(f):

def inner():
pass

return inner

@dec
def double(x):
return x * 2

# double = dec(double)

在这里,@dec实际上是一个语法糖,它完全等价于double = dec(double)。(相比较于后者,使用@语法就像是给函数戴上了一顶帽子,能让我们清晰地看到这个函数被哪个装饰器装饰了,因此我们推荐使用前者来进行装饰)

也就是说,当我们将装饰器作用于double函数时,double函数就被替代成了dec函数返回的新函数inner

在绝大多数情况下,我们使用装饰器时,会将函数作为装饰器的返回值。并且返回的函数通常是对原函数的“包装”,它在包含原函数的功能的同时,还给原函数增添了新的功能,达到一种“装饰”的效果,“装饰器”因此而得名。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def timeit(f):

def wrapper(x):
start = time.time()
ret = f(x)
print(time.time()- start)
return ret

return wrapper

@timeit
def my_func(x):
time.sleep(x)

my_func(1)

timeit是一个装饰器,它接受的参数f是一个函数,返回值wrapper也是一个函数。

wrapper函数接受和my_func函数相同数目的参数,刚开始记了一个时间戳,然后调用timeit传进来的参数f,记录返回值,打印前后时间差,最后返回返回值。

显然,这个装饰器实现了一个打印函数运行时间的功能。并且对于不同的函数,你都可以用装饰器来简洁地完成这件事。

除了打印函数运行时间的功能外,我们还可以用装饰器来实现插入日志、性能测试、权限验证等功能,在不修改函数源代码的前提下,方便地去实现这些功能。😎

wrapper作为要代替func的新函数,那么它的参数数量也需要和func一致,可是我们并不清楚传入进来的func函数参数是什么样的,装饰器不应该只单独服务于某个函数。

因此,我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def timeit(f):

def wrapper(*args, **kwargs):
start = time.time()
ret = f(*args, **kwargs)
print(time.time()- start)
return ret

return wrapper

@timeit
def my_func(x):
time.sleep(x)

my_func(1)

这里的*args, **kwargs是什么鬼呢?

它们是用于处理可变数量参数的特殊语法,能够允许函数接受任意数量的位置参数( *args)和关键字参数( **kwargs),提供了极大的灵活性。

***操作符可以用于打包解包

  • 打包:
    • *:将若干个位置参数打包成元组
    • **:将若干个关键字参数打包成字典
  • 解包:
    • *:将可迭代对象(元组)拆分成若干个位置参数
    • **:将字典拆分成若干个关键字参数

这样,无论函数f需要多少个参数,在装饰器里都能够灵活应对,比如:

1
2
3
@timeit
def add(x, y):
return x + y

3. 带参数的装饰器

装饰器调用时可能会带上参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time

def timeit(iteration):

def inner(f):

def wrapper(x):
start = time.time()
for _ in range(iteration):
ret = f(x)
print(time.time()- start)
return ret

return wrapper

return inner

@timeit(1000) # 这里带上了参数!
def double(x):
return x * 2
# 相当于double = timeit(1000)(double)

double(2)

在这里,装饰器timeit接受的参数iteration是一个整型,用于定义执行次数,返回值是一个函数inner,而这个inner就是我们原本所说的那个参数是函数、返回值也是函数的装饰器。

所以它相当于就是在原装饰器的基础之上套了一个壳。参数可以是你指定的任意类型,而返回值是我们原来所说的那种装饰器。

4. 为什么要使用装饰器?

  1. 使用装饰器可以提升代码复用,避免重复冗余代码。如果我有多个函数需要测量执行时间,我可以直接将装饰器应用在这些函数上,而不是给多个函数加上一样的代码。这样的代码既冗余也不方便后面维护。
  2. 使用装饰器可以保证函数的逻辑清晰。如果一个本身功能就很复杂的函数,我还要通过修改内部代码来测量运行时间,这样会模糊函数自身的主逻辑。同时,软件开发的一个原则就是单一职责,也就是说,一个函数只应该承担一项责任。
  3. 通过装饰器,我们可以扩展别人的函数。想象我们正在使用一个第三方库的函数,但我要添加额外的行为,比如测量运行时间,那我就可以用装饰器去包装,而不是跑到库里面去修改。

装饰器 - 补充

可以使用多个@dec语句来装饰函数,即:

1
2
3
4
5
@A
@B
@C
def func():
pass

包装的顺序为从下往上,即func = A(B(C(func)))

让我们来做个汉堡🍔!

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
def 加面包():
def 包装():
print("🍞 面包") # 最外层
肉() # 中间的肉
print("🍞 面包") # 最外层
return 包装

def 加生菜():
def 包装():
print("🥬 生菜") # 外层
肉() # 内层
print("🥬 生菜") # 外层
return 包装

def 加肉饼():
def 包装():
print("🥩 肉饼") # 内层
馅() # 最内层(原函数)
print("🥩 肉饼") # 内层
return 包装

@加面包
@加生菜
@加肉饼
def 汉堡馅():
print("🧀 芝士酱")

汉堡馅()

二. 迭代器(iterator

1. 可迭代对象(iterable

1
2
3
my_str = '123'
for s in my_str:
print(s)
1
2
3
my_lst = [1, 2, 3]
for s in my_lst:
print(s)
1
2
3
my_int = 123
for s in my_int:
print(s)

对于以上三段代码,为什么for循环能作用于字符串和列表,而作用于整数就会报错呢?

注意到报错信息:

1
TypeError: 'int' object is not iterable

这里的iterable表示可迭代对象

在Python中,只有可迭代对象才能被for循环迭代。

可迭代对象必须实现一个叫做__iter__的魔法方法,字符串和列表都有这个方法,但是整数没有。

我们可以通过hasattr这个函数,查看对象是否有某个属性或者方法

1
2
3
4
5
6
my_str = '123'
my_lst = [1, 2, 3]
my_int = 123
print(hasattr(my_str, '__iter__')) # True
print(hasattr(my_lst, '__iter__')) # True
print(hasattr(my_int, '__iter__')) # False

可是,为什么对象必须有这个方法才能进行迭代呢?这和for循环的实现原理有关。

2. for循环的实现原理

流程图:

flowchart LR
    A([开始for循环]) --> B[调用__iter__方法<br>获取迭代器]
    B --> C[调用迭代器的<br>__next__方法]
    C --> D{是否能获取到下一个值?}
    D -->|是| E[获取下一个值]
    E --> F[执行循环体代码]
    F --> C
    D -->|否| G([结束for循环])

所以一个for循环:

1
2
for i in seq:
do_something_to(i)

实际上是这样工作的:

1
2
3
4
5
6
it = iter(seq)
while True:
try:
print(next(it))
except StopIteration:
break

这里的iternext函数等价于调用对象的__iter____next__方法。

for循环在循环前,会先调用可迭代对象的__iter__,拿到它的迭代器。然后不断调用迭代器的__next__方法获取下一个值,直到所有值都获取完毕,循环结束。

3. 迭代器(iterator

知道了for循环的工作原理,接下来就让我们正式了解迭代器这一概念。

调用可迭代对象的__iter__方法将会返回该对象对应的迭代器。

迭代器需要包含两个魔法方法:__iter____next__
__iter__方法将会返回迭代器本身(这一点似乎有点奇怪,待会会讲),__next__方法会返回迭代器的下一个值,当所有值都获取完毕时,调用__next__将会引发一个StopIteration的异常(for循环会捕捉这个异常,并将其作为循环结束的标志)。

下面是官方文档的解释:

可迭代对象和迭代器官方文档解释

简单来讲,可迭代对象是一个容器,它包含了迭代需要的数据,而迭代器则是实际执行迭代操作的单位。
也就是说,可迭代对象是负责指挥的“大哥”,迭代器是负责干活的“小弟”。

4. 迭代器的应用

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
class NodeIter:
def __init__(self, node):
self.curr_node = node

def __next__(self):
if self.curr_node is None:
raise StopIteration
node, self.curr_node = self.curr_node, self.curr_node.next
return node

class Node:
def __init__(self, name):
self.name = name
self.next = None

def __iter__(self):
return NodeIter(self)

node1 = Node("node1")
node2 = Node("node2")
node3 = Node("node3")
node1.next = node2
node2.next = node3

for node in node1:
print(node.name)

这个程序用迭代器实现了链表的遍历。在这里,Node是一个可迭代对象类,NodeIter是一个迭代器类。

可以看见,Node实现了一个__iter__方法,返回对应的迭代器。
NodeIter实现了一个__next__方法,返回下一个值(如果有)或者引发一个StopIteration的异常。

可是,为什么这里的NodeIter没有实现__iter__方法呢?

官方给出的解释是:

迭代器必须具有__iter__()方法用来返回该迭代器对象自身,因此迭代器必定也是可迭代对象,可被用于其他可迭代对象适用的大部分场合。

在迭代器中定义__iter__是为了让迭代器也成为可迭代对象,让每个迭代器也是可迭代的。

那为什么要这么规定呢?

基于上面的代码,有人可能会这么写:

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
class NodeIter:
def __init__(self, node):
self.curr_node = node

def __next__(self):
if self.curr_node is None:
raise StopIteration
node, self.curr_node = self.curr_node, self.curr_node.next
return node

class Node:
def __init__(self, name):
self.name = name
self.next = None

def __iter__(self):
return NodeIter(self)

node1 = Node("node1")
node2 = Node("node2")
node3 = Node("node3")
node1.next = node2
node2.next = node3

it = iter(node1) # 显式获取迭代器
first = next(it) # 获取第一个值

for node in it: # 遍历之后的值
print(node.name)

在这里,我们显式地获取了node1的迭代器,然后调用next()获取第一个值,从第二个开始遍历。

这看起来似乎很合理,但:

报错1

it虽然是一个迭代器,但由于它本身并不是一个可迭代对象,所以我们不能对它做for循环。但这件事是反直觉的!

这就是为什么官方规定一个可迭代对象必须也得是一个迭代器——它可以让迭代器更加灵活地应用于for循环和需要可迭代对象的函数(比如zip()map()filter()等)里

实现方法很简单:

1
2
3
4
5
6
7
8
9
10
11
12
class NodeIter:
def __init__(self, node):
self.curr_node = node

def __next__(self):
if self.curr_node is None:
raise StopIteration
node, self.curr_node = self.curr_node, self.curr_node.next
return node

def __iter__(self):
return self

定义一个__iter__函数,返回self即可。

在迭代器里定义__iter__这个方法是Python的最佳实践。当然你要是不定义,也没人拦着:)

5. 迭代器的特性

(1) 一次性

需要注意的是,迭代器是一次性的

也就是说迭代器一旦用完,就不能再重新使用了!

举个例子:

1
2
3
4
5
6
7
8
9
10
11
my_list = [1, 2, 3, 4, 5]

iterator = iter(my_list)

print("第一次遍历:")
for item in iterator:
print(item)

print("第二次遍历:")
for item in iterator:
print(item)

可以看到"第二次遍历":下是没有输出的,这是因为在前面一个for循环里,迭代器iterator的元素已经被消耗完了,第二次使用时它就会是空的。

或者是被函数遍历:

1
2
3
4
5
6
my_list = [1, 2, 3]

iterator = iter(my_list)

print(list(iterator)) # [1, 2, 3]
print(list(iterator)) # []

可以看到迭代器同样会被消耗。

如果你需要多次遍历同一可迭代对象,应该重新获取一个迭代器。

(2) 惰性

迭代器只有在每次调用__next__方法时,才会计算和存储下一个元素,而不是预先一次性计算和存储所有结果。

换句话说,迭代器是惰性加载的,按需计算,用多少就取多少。

使用迭代器,我们只要用很少的内存就可以存放和处理大量的数据!

迭代器 - 补充

在Python中,有很多返回值是迭代器的内置函数,比如:
zip()map()filter()reversed()

或许你曾经以为他们是列表

三. 生成器(generator

1. 什么是生成器?

在了解迭代器之后,你可能会觉得实现迭代器有点麻烦:需要定义一个类,实现__iter____next__方法,还要手动处理StopIteration异常。

那有没有什么更简单的方法推荐一下的呢?有的兄弟,有的。
它就是生成器

生成器是一种特殊的迭代器,它让你能用更简洁、更直观的方式创建迭代器。

生成器和迭代器具有许多相同的特性,它们都包含有__iter____next__方法,都能被for循环迭代,都具有“一次性”和“惰性”。

生成器有两种写法,分别是生成器函数生成器表达式

2. 生成器函数

下面是一个简单的例子:

1
2
3
4
5
6
7
8
def gen(num):
while num > 0:
yield num
num -= 1

g = gen(5)
for i in g:
print(i)

在这里,gen就是生成器函数,而g生成器对象

当Python在编译期间发现一个函数里有yield关键字时,就会把这个函数视作生成器函数,而不是普通的函数。调用生成器函数,会直接返回一个生成器对象,而不会去执行内部的具体语句来返回别的值。

只有对生成器对象调用next(),生成器对象才会进入函数体执行具体语句,它的运行规则是:
当函数执行到yield时,会在此暂停执行并返回一个值,下次调用next()时又从此处继续,直到再次遇到yield或是函数执行完毕。

生成器执行完毕(即return)将会引发StopIteration的异常。

可以看下面这个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def my_generator():
print("开始执行")
yield 1
print("继续执行")
yield 2
print("最后执行")
yield 3

# 调用生成器函数不会立即执行,而是返回一个生成器对象
gen = my_generator()
print(gen) # <generator object my_generator at 0x...>

# 每次调用next(),函数执行到下一个yield
print(next(gen)) # 开始执行 → 1
print(next(gen)) # 继续执行 → 2
print(next(gen)) # 最后执行 → 3
# print(next(gen)) # 这里会抛出StopIteration异常

3. 生成器表达式

生成器表达式与列表推导式语法类似,但使用圆括号而不是方括号。

不要跟我说它是元组推导式。。。

举个例子:

1
2
3
4
5
gen = (x * x for x in range(5))
print(gen) # <generator object <genexpr> at 0x...>

for i in gen:
print(i)

它等价于:

1
2
3
def generator_func():
for x in range(5):
yield x * x

4. 生成器的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Node:
def __init__(self, name):
self.name = name
self.next = None

def __iter__(self):
node = self
while node is not None:
yield node
node = node.next

node1 = Node("node1")
node2 = Node("node2")
node3 = Node("node3")
node1.next = node2
node2.next = node3

for node in node1:
print(node.name)

同样拿前面链表的例子,但现在我们直接将__iter__函数写成一个生成器。这比我们之前写的简洁了不少。

5. 高级用法

生成器还有一个高级用法,就是“发送”

yield *不仅仅是一个语句,还是一个表达式。对生成器调用send()方法,能够在恢复生成器执行的同时,向生成器内部发送一个值yield *表达式的结果就是你传进来的那个值。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def gen(num):
while num > 0:
tmp = yield num
if tmp is not None:
num = tmp
num -= 1

g = gen(5)

first = next(g)
print(f"first: {first}")

print(f"send: {g.send(10)}")

for i in g:
print(i)

在这里,g.send(10)使yield num成为了10,于是tmp被赋值为10,条件成立,num也在第五行被赋值为10,在第6行减1,下一次yield num产生的数据就是9了。

值得一提的是,在生成器中,next(g)完全等价于g.send(None)。因此,最后的for循环相当于是在不停地做g.send(None),此时if条件不满足,num会不断自减至0,输出就是从8到1。

请注意,向刚创建的生成器发送非None数据前,必须先用next(g)g.send(None)将其运行到第一个 yield处,否则会报错(毕竟这时候还没有遇到yield *表达式能够让你发送数据呢)。

如图:

报错2

总的来说,send()函数给了我们一个机制,能够让我们和生成器内部做交互。

生成器 - 补充

当生成器表达式作为函数的唯一参数时,可以省略外层括号。

例如:

1
2
3
result = sum(x**2 for x in range(5))
# 等价于:result = sum((x**2 for x in range(5)))
print(result) # 30

🎉完结撒花🎉


参考资料:

装饰器:

【python】装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了!
【Python 高级特性】装饰器:不修改代码,就能改变函数功能的强大特性

迭代器:

【python】对迭代器一知半解?看完这个视频就会了。涉及的每个概念,都给你讲清楚!
【Python】从迭代器到生成器:小内存也能处理大数据
『教程』几分钟听懂迭代器

生成器:

【python】生成器是什么?怎么用?能干啥?一期视频解决你所有疑问!
【Python 高级特性2】生成器:处理大量数据时,节省内存和时间

官方文档:

术语对照表 — Python 3.14.0 文档
术语对照表 — Python 3.14.0 文档
术语对照表 — Python 3.14.0 文档

感谢!(≧∀≦)ゞ