Python三大器
学习Python三大器——装饰器、迭代器、生成器,让你的代码更加Pythonic!
一. 装饰器(decorator)
1. 函数的本质
在Python中,万物皆对象,函数也不例外。
当我们在Python中定义一个函数的时候,我们就是在新建一个变量,并在这个变量里保存了一个函数对象。
(1) 函数作为参数
函数作为一个对象,也可以像其他对象一样被当作参数,传入到其他函数里。
如:
1 | def double(x): |
在这里,calc_number函数是高阶函数(将函数作为参数的函数),double和triple函数是回调函数(被作为参数传入函数的函数)。
由此可见,函数完全可以像其他的变量一样被相互传递。
(2) 函数作为返回值
函数本身也可以成为一个返回值。
如:
1 | def get_multiple_func(n): |
在这里,get_multiple_func它本身是一个函数,返回值也是一个函数,而它返回的这个函数做什么事情是和它得到的参数n相关的。multiple函数是闭包函数(一个函数要成为闭包,需要满足三个条件:1. 函数嵌套;2. 外函数返回内函数;3. 内函数引用外函数的变量),它能够“记住”外部作用域里的函数变量(即使外部函数已经返回),装饰器就是闭包的一个典型应用。
2. 装饰器
装饰器是一种特殊的函数,它接受一个函数作为参数,并返回一个新的函数(通常是包装过的原函数),用于在不修改原函数代码的情况下增强其功能。
简单来讲,装饰器就是一个参数是函数、返回值也是函数的函数。
这是一个极简的“装饰器”:
1 | def dec(f): |
在这里,@dec实际上是一个语法糖,它完全等价于double = dec(double)。(相比较于后者,使用@语法就像是给函数戴上了一顶帽子,能让我们清晰地看到这个函数被哪个装饰器装饰了,因此我们推荐使用前者来进行装饰)
也就是说,当我们将装饰器作用于double函数时,double函数就被替代成了dec函数返回的新函数inner
在绝大多数情况下,我们使用装饰器时,会将函数作为装饰器的返回值。并且返回的函数通常是对原函数的“包装”,它在包含原函数的功能的同时,还给原函数增添了新的功能,达到一种“装饰”的效果,“装饰器”因此而得名。
例如:
1 | import time |
timeit是一个装饰器,它接受的参数f是一个函数,返回值wrapper也是一个函数。
wrapper函数接受和my_func函数相同数目的参数,刚开始记了一个时间戳,然后调用timeit传进来的参数f,记录返回值,打印前后时间差,最后返回返回值。
显然,这个装饰器实现了一个打印函数运行时间的功能。并且对于不同的函数,你都可以用装饰器来简洁地完成这件事。
除了打印函数运行时间的功能外,我们还可以用装饰器来实现插入日志、性能测试、权限验证等功能,在不修改函数源代码的前提下,方便地去实现这些功能。😎
wrapper作为要代替func的新函数,那么它的参数数量也需要和func一致,可是我们并不清楚传入进来的func函数参数是什么样的,装饰器不应该只单独服务于某个函数。
因此,我们可以这么写:
1 | import time |
这里的*args, **kwargs是什么鬼呢?
它们是用于处理可变数量参数的特殊语法,能够允许函数接受任意数量的位置参数( *args)和关键字参数( **kwargs),提供了极大的灵活性。
*和**操作符可以用于打包和解包。
- 打包:
*:将若干个位置参数打包成元组**:将若干个关键字参数打包成字典
- 解包:
*:将可迭代对象(元组)拆分成若干个位置参数**:将字典拆分成若干个关键字参数
这样,无论函数f需要多少个参数,在装饰器里都能够灵活应对,比如:
1 |
|
3. 带参数的装饰器
装饰器调用时可能会带上参数,比如:
1 | import time |
在这里,装饰器timeit接受的参数iteration是一个整型,用于定义执行次数,返回值是一个函数inner,而这个inner就是我们原本所说的那个参数是函数、返回值也是函数的装饰器。
所以它相当于就是在原装饰器的基础之上套了一个壳。参数可以是你指定的任意类型,而返回值是我们原来所说的那种装饰器。
4. 为什么要使用装饰器?
- 使用装饰器可以提升代码复用,避免重复冗余代码。如果我有多个函数需要测量执行时间,我可以直接将装饰器应用在这些函数上,而不是给多个函数加上一样的代码。这样的代码既冗余也不方便后面维护。
- 使用装饰器可以保证函数的逻辑清晰。如果一个本身功能就很复杂的函数,我还要通过修改内部代码来测量运行时间,这样会模糊函数自身的主逻辑。同时,软件开发的一个原则就是单一职责,也就是说,一个函数只应该承担一项责任。
- 通过装饰器,我们可以扩展别人的函数。想象我们正在使用一个第三方库的函数,但我要添加额外的行为,比如测量运行时间,那我就可以用装饰器去包装,而不是跑到库里面去修改。
装饰器 - 补充
可以使用多个@dec语句来装饰函数,即:
1 |
|
包装的顺序为从下往上,即func = A(B(C(func)))。
让我们来做个汉堡🍔!
1 | def 加面包(肉): |
二. 迭代器(iterator)
1. 可迭代对象(iterable)
1 | my_str = '123' |
1 | my_lst = [1, 2, 3] |
1 | my_int = 123 |
对于以上三段代码,为什么for循环能作用于字符串和列表,而作用于整数就会报错呢?
注意到报错信息:
1 | TypeError: 'int' object is not iterable |
这里的iterable表示可迭代对象。
在Python中,只有可迭代对象才能被for循环迭代。
可迭代对象必须实现一个叫做__iter__的魔法方法,字符串和列表都有这个方法,但是整数没有。
我们可以通过hasattr这个函数,查看对象是否有某个属性或者方法
1 | my_str = '123' |
可是,为什么对象必须有这个方法才能进行迭代呢?这和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 | for i in seq: |
实际上是这样工作的:
1 | it = iter(seq) |
这里的iter和next函数等价于调用对象的__iter__和__next__方法。
for循环在循环前,会先调用可迭代对象的__iter__,拿到它的迭代器。然后不断调用迭代器的__next__方法获取下一个值,直到所有值都获取完毕,循环结束。
3. 迭代器(iterator)
知道了for循环的工作原理,接下来就让我们正式了解迭代器这一概念。
调用可迭代对象的__iter__方法将会返回该对象对应的迭代器。
迭代器需要包含两个魔法方法:__iter__和__next__
__iter__方法将会返回迭代器本身(这一点似乎有点奇怪,待会会讲),__next__方法会返回迭代器的下一个值,当所有值都获取完毕时,调用__next__将会引发一个StopIteration的异常(for循环会捕捉这个异常,并将其作为循环结束的标志)。
下面是官方文档的解释:

简单来讲,可迭代对象是一个容器,它包含了迭代需要的数据,而迭代器则是实际执行迭代操作的单位。
也就是说,可迭代对象是负责指挥的“大哥”,迭代器是负责干活的“小弟”。
4. 迭代器的应用
1 | class NodeIter: |
这个程序用迭代器实现了链表的遍历。在这里,Node是一个可迭代对象类,NodeIter是一个迭代器类。
可以看见,Node实现了一个__iter__方法,返回对应的迭代器。
NodeIter实现了一个__next__方法,返回下一个值(如果有)或者引发一个StopIteration的异常。
可是,为什么这里的NodeIter没有实现__iter__方法呢?
官方给出的解释是:
迭代器必须具有
__iter__()方法用来返回该迭代器对象自身,因此迭代器必定也是可迭代对象,可被用于其他可迭代对象适用的大部分场合。
在迭代器中定义__iter__是为了让迭代器也成为可迭代对象,让每个迭代器也是可迭代的。
那为什么要这么规定呢?
基于上面的代码,有人可能会这么写:
1 | class NodeIter: |
在这里,我们显式地获取了node1的迭代器,然后调用next()获取第一个值,从第二个开始遍历。
这看起来似乎很合理,但:

it虽然是一个迭代器,但由于它本身并不是一个可迭代对象,所以我们不能对它做for循环。但这件事是反直觉的!
这就是为什么官方规定一个可迭代对象必须也得是一个迭代器——它可以让迭代器更加灵活地应用于for循环和需要可迭代对象的函数(比如zip(),map(),filter()等)里。
实现方法很简单:
1 | class NodeIter: |
定义一个__iter__函数,返回self即可。
在迭代器里定义__iter__这个方法是Python的最佳实践。当然你要是不定义,也没人拦着:)
5. 迭代器的特性
(1) 一次性
需要注意的是,迭代器是一次性的。
也就是说迭代器一旦用完,就不能再重新使用了!
举个例子:
1 | my_list = [1, 2, 3, 4, 5] |
可以看到"第二次遍历":下是没有输出的,这是因为在前面一个for循环里,迭代器iterator的元素已经被消耗完了,第二次使用时它就会是空的。
或者是被函数遍历:
1 | my_list = [1, 2, 3] |
可以看到迭代器同样会被消耗。
如果你需要多次遍历同一可迭代对象,应该重新获取一个迭代器。
(2) 惰性
迭代器只有在每次调用__next__方法时,才会计算和存储下一个元素,而不是预先一次性计算和存储所有结果。
换句话说,迭代器是惰性加载的,按需计算,用多少就取多少。
使用迭代器,我们只要用很少的内存就可以存放和处理大量的数据!
三. 生成器(generator)
1. 什么是生成器?
在了解迭代器之后,你可能会觉得实现迭代器有点麻烦:需要定义一个类,实现__iter__和__next__方法,还要手动处理StopIteration异常。
那有没有什么更简单的方法推荐一下的呢?有的兄弟,有的。
它就是生成器。
生成器是一种特殊的迭代器,它让你能用更简洁、更直观的方式创建迭代器。
生成器和迭代器具有许多相同的特性,它们都包含有__iter__和__next__方法,都能被for循环迭代,都具有“一次性”和“惰性”。
生成器有两种写法,分别是生成器函数和生成器表达式。
2. 生成器函数
下面是一个简单的例子:
1 | def gen(num): |
在这里,gen就是生成器函数,而g是生成器对象。
当Python在编译期间发现一个函数里有yield关键字时,就会把这个函数视作生成器函数,而不是普通的函数。调用生成器函数,会直接返回一个生成器对象,而不会去执行内部的具体语句来返回别的值。
只有对生成器对象调用next(),生成器对象才会进入函数体执行具体语句,它的运行规则是:
当函数执行到yield时,会在此暂停执行并返回一个值,下次调用next()时又从此处继续,直到再次遇到yield或是函数执行完毕。
生成器执行完毕(即return)将会引发StopIteration的异常。
可以看下面这个实例:
1 | def my_generator(): |
3. 生成器表达式
生成器表达式与列表推导式语法类似,但使用圆括号而不是方括号。
不要跟我说它是元组推导式。。。
举个例子:
1 | gen = (x * x for x in range(5)) |
它等价于:
1 | def generator_func(): |
4. 生成器的应用
1 | class Node: |
同样拿前面链表的例子,但现在我们直接将__iter__函数写成一个生成器。这比我们之前写的简洁了不少。
5. 高级用法
生成器还有一个高级用法,就是“发送”。
yield *不仅仅是一个语句,还是一个表达式。对生成器调用send()方法,能够在恢复生成器执行的同时,向生成器内部发送一个值。yield *表达式的结果就是你传进来的那个值。
举个例子:
1 | def gen(num): |
在这里,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 *表达式能够让你发送数据呢)。
如图:

总的来说,send()函数给了我们一个机制,能够让我们和生成器内部做交互。
生成器 - 补充
当生成器表达式作为函数的唯一参数时,可以省略外层括号。
例如:
1 | result = sum(x**2 for x in range(5)) |
🎉完结撒花🎉
参考资料:
装饰器:
【python】装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了!
【Python 高级特性】装饰器:不修改代码,就能改变函数功能的强大特性迭代器:
【python】对迭代器一知半解?看完这个视频就会了。涉及的每个概念,都给你讲清楚!
【Python】从迭代器到生成器:小内存也能处理大数据
『教程』几分钟听懂迭代器生成器:
【python】生成器是什么?怎么用?能干啥?一期视频解决你所有疑问!
【Python 高级特性2】生成器:处理大量数据时,节省内存和时间官方文档:
术语对照表 — Python 3.14.0 文档
术语对照表 — Python 3.14.0 文档
术语对照表 — Python 3.14.0 文档
感谢!(≧∀≦)ゞ