装饰器
在你写自己的装饰器的时候记得要调用functools库的wraps装饰器,它能保留原始函数的元数据。
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
wraps是一个便捷函数,调用update_wrapper作为函数装饰器,后者更新一个wrapper函数以使其类似于wrapped函数。
functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)=partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
import functools
print(functools.WRAPPER_ASSIGNMENTS)
('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
print(functools.WRAPPER_UPDATES)
('__dict__',)
update_wrapper会把WRAPPER_ASSIGNMENTS这些属性直接赋值过去,然后会更新包装函数的WRAPPER_UPDATES也就是实例字典,添加了一个指向被包装函数的属性__wrapped__。
带参数的装饰器则是通过闭包的原理,在最外层多套了一层函数。
可自定义属性的装饰器则是在带参数的装饰器的基础上,给包装函数添加方法,方法里面通过nonlocal关键字对参数实现更改。
带可选参数的装饰器
from functools import wraps, partial
import logging
def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
if func is None:
return partial(logged, level=level, name=name, message=message)
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
利用装饰器强制函数上的类型检查,用到了inspect库。
将装饰器定义为类的一部分,就是把装饰器放到类里面,定义成实例方法还是类方法只是调用的时候会有区别。看上去有点奇怪,但有些标准库就有很多例子如@property,它其实是个类,里面定义了三个方法getter(), setter(), deleter()
,都是装饰器。
将装饰器定义为类,需要实现__init__和__call__方法,特别地,如果你想作用于类定义里面的函数,还需要额外实现__get__方法:
import types
from functools import wraps
class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)
要注意的点是wraps在__init__里的用法,还有__get__方法返回一个绑定方法(给这个方法传递self参数)。
为类和静态方法提供装饰器,要确保装饰器在 @classmethod
或 @staticmethod
之前,因为这两个并不会创建可直接调用的对象, 而是创建特殊的描述器对象,因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。
class Spam:
@timethis
def instance_method(self, n):
print(self, n)
while n > 0:
n -= 1
@classmethod
@timethis
def class_method(cls, n):
print(cls, n)
while n > 0:
n -= 1
@staticmethod
@timethis
def static_method(n):
print(n)
while n > 0:
n -= 1
装饰器为被包装函数增加参数,我惊了:
from functools import wraps
def optional_debug(func):
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
>>> @optional_debug
... def spam(a,b,c):
... print(a,b,c)
...
>>> spam(1,2,3)
1 2 3
>>> spam(1,2,3, debug=True)
Calling spam
1 2 3
>>>
使用装饰器扩充类的功能,通过装饰器重写方法,比继承来的更简洁更快。
装饰器的进阶终于讲完了这里贴个基础知识吧。
元类metaclass
《Understanding Python metaclasses》讲的很牛。
断断续续看了一个周才算真正弄懂,太长就不翻译了,随便放两张图吧!
*args和**kwargs的强制参数签名
对任何涉及到操作函数调用签名的问题,你都应该使用 inspect 模块中的签名特性。 我们最主要关注两个类:Signature 和 Parameter 。下面是一个创建函数前面的交互例子:
>>> from inspect import Signature, Parameter
>>> # Make a signature for a func(x, y=42, *, z=None)
>>> parms = [ Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
... Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
... Parameter('z', Parameter.KEYWORD_ONLY, default=None) ]
>>> sig = Signature(parms)
>>> print(sig)
(x, y=42, *, z=None)
上下文管理器
实现一个新的上下文管理器的最简单的方法就是使用 contexlib 模块中的 @contextmanager 装饰器。 下面是一个实现了代码块计时功能的上下文管理器例子:
import time
from contextlib import contextmanager
@contextmanager
def timethis(label):
start = time.time()
try:
yield
finally:
end = time.time()
print('{}: {}'.format(label, end - start))
# Example use
with timethis('counting'):
n = 10000000
while n > 0:
n -= 1
在函数 timethis() 中,yield 之前的代码会在上下文管理器中作为 __enter__() 方法执行, 所有在 yield 之后的代码会作为 __exit__() 方法执行。 如果出现了异常,异常会在yield语句那里抛出。
上面的装饰器适用于函数,如果是对象,那么你就需要单独实现 __enter__() 方法和 __exit__() 方法。
class timethis:
def __init__(self, label):
self.label = label
def __enter__(self):
self.start = time.time()
def __exit__(self, exc_ty, exc_val, exc_tb):
end = time.time()
print('{}: {}'.format(self.label, end - self.start))
exec执行代码
在局部变量域中会出问题:
# 全局命名空间下
>>> a = 13
>>> exec('b = a + 1')
>>> print(b)
14
# 局部下
>>> def test():
... a = 13
... exec('b = a + 1')
... print(b)
...
>>> test()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in test
NameError: global name 'b' is not defined
默认情况下,exec() 会在调用者局部和全局范围内执行代码。然而,在函数里面, 传递给 exec() 的局部范围是拷贝实际局部变量组成的一个字典。 因此,如果 exec() 如果执行了修改操作,这种修改后的结果对实际局部变量值是没有影响的。
为了修正这样的错误,你需要在调用 exec() 之前使用 locals() 函数来得到一个局部变量字典。 之后你就能从局部字典中获取修改过后的变量值了。例如:
>>> def test():
... a = 13
... loc = locals()
... exec('b = a + 1')
... b = loc['b']
... print(b)
...
>>> test()
14
作为 locals() 的一个替代方案,你可以使用你自己的字典,并将它传递给 exec() 。例如:
>>> def test4():
... a = 13
... loc = { 'a' : a }
... glb = { }
... exec('b = a + 1', glb, loc)
... b = loc['b']
... print(b)
...
>>> test4()
14