《Fluent Python》借来5天了。最近两天没顾上看书,作业有点多。今天把这一章开个头。之前大概看过《Python高级编程》了解了一点装饰器,有些太基础的就不写了。
7.2 Python何时执行装饰器 Page 156
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python加载模块时)。
# registration.py
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@regigter
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
把registration.py
当作脚本运行得到的输出如下:
$ python3 registration.py
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
running main()
registry -> [<function f1 at 0x...>, <function f2 at 0x...>]
running f1()
running f2()
running f3()
如果导入registration.py
模块(不作为脚本运行),输出如下:
>>> import registration
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
>>> registraion.registry
[<function f1 at 0x...>, <function f2 at 0x...>]
这个示例突出了Python程序员所说的 导入时 和 运行时 的区别。
7.4 变量作用域规则 Page 159
下面的示例,我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量a
,是函数的参数;另一个是变量b
,这个函数没有定义它。
>>> def f1(a):
print(a)
print(b)
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: name 'b' is not defined
出现错误并不奇怪。如果先给b
赋值,然后再调用f1
,那就不会出错。
>>> b = 6
>>> f1(3)
3
6
下面是一个令人吃惊的示例。
>>> b = 6
>>> def f2(a):
print(a)
print(b)
b = 9
>>> f2(3)
3
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
f2(3)
File "<pyshell#5>", line 3, in f2
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
事实上,Python在编译该函数的定义体时,它判断b
是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python会尝试从本地环境获取b。后面调用f2(3)
时,f2的定义体会获取并打印局部变量a
的值,但是尝试获取局部变量b
的值时,发现b
没有绑定值。
这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。这比JavaScript的行为好多了,JavaScript也不要求声明变量, 但是如果忘记把变量声明为局部变量(使用var
),可能会在不知情的情况下获取全局变量。
如果在函数中赋值时想让解释器把b
当成全局变量,要使用global
声明:
>>> b = 6
>>> def f3(a):
global b
print(a)
print(b)
b = 9
>>> f3(3)
3
6
>>> b
9
比较字节码
dis
模块为反汇编Python函数字节码提供了简单的方式。
>>> def f1(a):
print(a)
print(b)
>>> def f2(a):
print(a)
print(b)
b = 9
>>> from dis import dis
>>> dis(f1)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
>>> dis(f2)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
4 16 LOAD_CONST 1 (9)
18 STORE_FAST 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
LOAD_GLOBAL
意思是加载全局名称,LOAD_FAST
意思是加载本地名称。
运行字节码的CPython VM是栈机器,因此LOAD
和POP
操作引用的是栈。深入说明Python操作码不在本书范畴之内,不过dis
模块的文档对其做了说明。
7.5 闭包 Page 161
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外的非全局变量。
这个概念难以掌握,最好通过示例理解。
假如有个名为avg
的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
初学者可能会像下例那样使用类实现:
class Averager:
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)