PythonCookbook第九章(元编程)【持续更新】
元编程的主要目标是创建函数和类,并用它们来操纵代码(比如修改、生成或者包装已有的代码)。Python中基于这个目的的主要特性包括装饰器、类装饰器以及元类。
9.1 给函数添加一个包装
问题
我们想给函数添加一个包装以添加额外的处理。
解决方案
写一个简单的装饰器
import time from functools import wraps def timethis(func): ''' :param func: Decorator that reports the execution time :return: func ''' @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 @timethis def countdown(n): while n > 0: n -= 1 if __name__ == '__main__': print(countdown(10000)) print(countdown(10000000))
讨论:
装饰器是一个函数,它可以接收一个函数作为输入并返回一个新的函数作为输出。
@timethis def countdown(n):
这个的意思就是countdown = timethis(countdown)
类里面的内置的@staticmethod, @classmethos, @property都是一样的逻辑
9.2编写装饰器时如何保存函数的元数据。
问题:
我们已经编写好一个装饰器,但是当将它用在一个函数上时,一些重要的元数据比如函数名、文档字符串、函数注释以及调用签名都丢失了。
解决方案:
functools.wraps
import time from functools import wraps def timethis(func): ''' :param func: Decorator that reports the execution time :return: func ''' @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 @timethis def countdown(n: int) -> int: '''this is countdown''' while n > 0: n -= 1 return n if __name__ == '__main__': print(countdown(10000)) print(countdown(10000000)) # 函数数据类型 print(countdown.__annotations__) # 函数名字 print(countdown.__name__) # 函数文档解释 print(countdown.__doc__)
countdown 0.0006299018859863281 0 countdown 0.5314240455627441 0 {'n': <class 'int'>, 'return': <class 'int'>} countdown this is countdown
讨论:
如果取消@wraps
函数的特性都没有了
countdown 0.0007231235504150391 0 countdown 0.5646867752075195 0 {} wrapper None
# 取回原函数 print(countdown.__wrapped__) from inspect import signature print(signature(countdown)) print(signature(countdown.__wrapped__))
可以通过被装饰函数的__wrapped__取回没有被装饰的函数
9.3 对装饰器进行解包装
问题:
取回没有包装过的函数
解决方案:
通过__wrapped__属性取回
讨论:
只要在装饰器利用了functools.wraps(func)对元数据进行了适当的拷贝,才能用__wrapped__属性取出。
多个装饰器的时候,看__wrapped__的效果。
from functools import wraps def decorator1(func): @wraps(func) def wrapper(*args): print('Decorator1') return func(*args) return wrapper def decorator2(func): @wraps(func) def wrapper(*args): print('Decorator2') return func(*args) return wrapper @decorator1 @decorator2 def add(x, y): return x+y if __name__ == '__main__': print(add(2,3)) print('=' * 10 ) # 取回的是decorator2函数 print(add.__wrapped__(3,4)) print('=' * 10) # 取回的是原来的函数 print(add.__wrapped__.__wrapped__(3, 4))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t3_3.py Decorator1 Decorator2 5 ========== Decorator2 7 ========== 7 Process finished with exit code 0
Python3.7已经修改了这个漏洞,不会直接穿越到元素的函数了。
但请注意并不是所有的装饰器都使用了@wraps,因此有些装饰器的行为可能与我们预期的有所区别,特别是,由内建的装饰器@staitcmethod和@classmethod创建的描述符对象并不遵循这个约定(相反,它们会把原始函数保存在__func__属性中)。
9.4定义一个可接收参数的装饰器
问题:
编写一个可接收掺乎的装饰器
解决方案:
编写一个装饰器工厂,书中用了logging模块,编写装饰器工厂,刚好我也重新复习下logging模块
from functools import wraps import logging import time def logged(level, name=None, message=None): def decorate(func): logname = name if name else func.__name__ # 获取一个log输出对象流 log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): # 输出log信息 # log.setLevel(logging.ERROR) # print(log.level) log.log(level, logmsg) return func(*args, **kwargs) return wrapper return decorate @logged(logging.DEBUG) def add(x, y): return x+y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!') if __name__ == '__main__': print(add(1, 2)) print('=' * 10) time.sleep(1) print(spam())
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t4_2.py 3 ========== spam Spam! None Process finished with exit code 0
通过装饰器工厂的主要作用就是可以传递参数给内部函数调用,这里传入的是logging的等级
讨论
@decorator(x, y, z)
def func(a, b):
...
其实底层运行的是
func = decorator(x,y,z)(func)
decorator(x,y,z)返回的必须是一个可调用对象。
9.5 定义一个属性可由用户修改的装饰器
问题
我们想编写一个装饰器来包装函数,但是可以让用户调整装饰器的属性,这样在运行时能够控制装饰器的行为
解决方案:
编写一个访问器函数,通过nonlocal关键字变量来修改装饰器内部的属性。之后把访问器函数作为函数属性附加到包装函数上。
我自己写的测试,根本不需要访问器函数,直接在包装函数外面定义包装函数的属性为函数。
from functools import wraps, partial import logging import time logging.basicConfig(level=logging.DEBUG) # 访问起函数,是一个简化版的装饰器工厂函数,使用了partial技巧 def attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return func def logged(level, name=None, message=None): def decorate(func): # print(func.__wrapped__) # print(func.__name__) logname = name if name else func.__name__ # 获取一个log输出对象流 log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): # 输出log信息 # log.setLevel(logging.ERROR) # print(log.level) log.log(level, logmsg) return func(*args, **kwargs) # 装饰器工厂会直接装饰器工厂函数调用两次,第一次传wrapper参数,第二次调用传func参数 # 通过这个装饰器工厂给函数赋值属性 @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg wrapper.get_level = lambda :level wrapper.name = 'sidian' return wrapper return decorate @logged(logging.DEBUG) def add(x, y): return x+y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!') if __name__ == '__main__': print(add.set_message('Hello World')) print(add(1, 2)) print('=' * 10) time.sleep(1) print(spam())
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t5_2.py DEBUG:add:Hello World None 3 ========== Spam! None CRITICAL:example:spam
其实我自己在写的过程中发现,不写访问起函数,
wrapper.get_level = lambda :level wrapper.name = 'sidian'
定义内部函数,然后给内部函数添加属性为函数也可以,相对来说就是多一步手工添加,但可以避免写访问器函数。
讨论:
装饰器都使用了@functoos.wrap的话,内层函数可以括约多个装饰器层进行传播。
9.6定义一个能接收可选参数的装饰器
问题:
我们想编写一个单独的装饰器,使其即可以像@decorator这样不带参数使用,也可以像@decorator(x,y,z)使用装饰器工厂这么用。
解决方案:
定义装饰器函数的传参方式里面有*,通过functools.partail返回一个函数。
from functools import wraps import logging import functools import time def logged(func=None, *, level=logging.WARNING, name=None, message=None): # 如果没有传入func,就返回这个partial定义好的函数,第二次执行这个函数,并自动传参被装饰的函数 if func is None: return functools.partial(logged, level=level, name=name, message=message) logname = name if name else func.__name__ # 获取一个log输出对象流 log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): # 输出log信息 # log.setLevel(logging.ERROR) # print(log.level) log.log(level, logmsg) return func(*args, **kwargs) return wrapper @logged def add(x, y): return x+y @logged(level=logging.CRITICAL, name='example') def spam(): print('Spam!') if __name__ == '__main__': print(add(1, 2)) print('=' * 10) time.sleep(1) print(spam())
讨论:
整个重点一点要了解到
@decorator(x, y, z)
def func(a, b):
...
其实底层运行的是
func = decorator(x,y,z)(func) 这个是重中之重
decorator(x,y,z)返回的必须是一个可调用对象。
9.7 利用装饰器对函数参数强制执行类型检查(感觉还是非常有用的)
问题
我们想为函数参数添加强制性的类型检查功能,将其作为一种断言或者调用者之间的契约。
解决方法
from inspect import signature from functools import wraps, partial def typeassert(*ty_args, **ty_kwargs): def decorate(func): # 如果为非debug模式,返回函数本身 if not __debug__: return func sig = signature(func) # print(sig) # (x, y, z=42) 函数的参数签名信息,<class 'inspect.Signature'>形式 bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments # print(bound_types) # 通过bind_partial可以给每个参数绑定对象,采用OrderDict形式OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)]) @wraps(func) def wrapper(*args, **kwargs): # 因为func如果设置由默认值,则sig对面里面保存着由默认值的信息。 bound_values = sig.bind(*args, **kwargs) # print(bound_values.arguments.items()) # 通过两个输入参数后,对比有序字典 for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate if __name__ == '__main__': @typeassert(int, z=int) def spam(x, y, z=[]): print(x, y, z) spam(1, 2, 3) spam(1, []) spam(1, 2, '')
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t7_2.py Traceback (most recent call last): File "/Users/shijianzhong/study/PythonCookbook/chapter_9/t7_2.py", line 38, in <module> spam(1, 2, '') File "/Users/shijianzhong/study/PythonCookbook/chapter_9/t7_2.py", line 26, in wrapper 'Argument {} must be {}'.format(name, bound_types[name]) TypeError: Argument z must be <class 'int'> 1 2 3 1 [] [] Process finished with exit code 1
讨论:
这里主要用到了inspect模块的一些功能
特别是sig = inspect.signature()
sig.bind与sig.bindpartail两个方法绑定输入的参数,通过arrgment.items字典形式输出。整个逻辑相对不是很复制,但需要了解很多内置模块的功能。
9.8在类中定义装饰器
问题:
我们想在类中定义一个装饰器,并将其作用于其他的函数或者方法上
解决方法:
from functools import wraps class A: # 实例装饰器方法 def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper # 类装饰器函数 @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper a = A() @a.decorator1 def spam(): ... @A.decorator2 def grok(): ... spam() grok()
讨论
我们的 propery类就用了这个
class Person: # 这个类属性是实例 first_name = property() # 调用实例方法的装饰器 @first_name.getter def first_name(self): return self._first_name @first_name.setter def first_name(self, value): self._first_name = value if __name__ == '__main__': p = Person() p.first_name = 'sh' print(p.first_name)
9.9把装饰器定义成类
问题:
我们想用装饰器来包装函数,但是希望得到的结果是一个可调用的实例。我们需要装饰器既能在类中工作,也可以在类外部使用。
解决方案:
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) # 定义__get__使的返回的对象可以在调用时,成本绑定对象instance的方法 def __get__(self, instance, owner): # print(instance, owner) if instance is None: print('123') return self else: # 将自身的__call__方法变成instance的方法 return types.MethodType(self, instance) @Profiled def add(x, y): return x+y class Spam: @Profiled def bar(self, x ): print(self, x) if __name__ == '__main__': print(add(2, 3)) print(add.ncalls) s = Spam() s.bar(1)
所以用类写装饰器相对非常的麻烦,一定要定义__get__才能给方法进行装饰,没什么特殊情况,还使通过普通函数写,因为函数默认内置定义好了__get__方法。
讨论:
from functools import wraps # 普通的函数装饰器,方便很多 def profiled(func): ncalls = 0 @wraps(func) def wrapper(*args, **kwargs): nonlocal ncalls ncalls += 1 return func(*args, **kwargs) # 必须属性赋值使函数,这样才有运行这个函数,或者最新的数据。 # 这里就像一个读取闭包函数的属性,必须通过函数来读取 wrapper.ncalls = lambda :ncalls return wrapper @profiled def add(x, y): return x+y class Spam: @profiled def bar(self, x ): print(self, x) if __name__ == '__main__': print(add(2, 3)) print(add(3 ,4)) print(add.ncalls()) s = Spam() s.bar(1)
9.10 把装饰器作用到类和静态方法上
问题:
我们想在类或者静态方法上应用装饰器
解决方案
直接写上装饰器就可以了,但必须在@classmethod和@staticmethod之前
from functools import wraps import time def timethis(func): ''' :param func: Decorator that reports the execution time :return: func ''' @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 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 if __name__ == '__main__': s = Spam() s.instance_method(1000000) Spam.class_method(1000000) Spam.static_method(100000)
讨论:
前面不要搞错位置,@classmethod和@staitcmethod并不会实际创建可直接调用的对象。相反,它们创建的是特殊的描述符对象。
9.11编写装饰器为被包装的函数添加参数
问题:
我们想编写一耳光装饰器为被包装的函数添加额外的参数。但是,添加的参数不能影响到该函数的已有的调用约定。
解决方案
通过函数那章学习到的keyword-only参数,额外的参数注入到函数的调用签名。
from functools import wraps def optional_debug(func): @wraps(func) # keyword-only参数传入 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) if __name__ == '__main__': spam(1, 2, 3, debug=True) print('='*20) spam(1, 2, 3)
讨论:
我前面的代码中,如果被装修的函数的参数里面与keyword-only中的参数一样,有可能名称冲突,为了避免,应该增加参数选择。
from functools import wraps import inspect def optional_debug(func): # 对函数的默认变量名参数的变量名进行查寻 args = inspect.getfullargspec(func).args # print(args) if 'debug' in args: raise TypeError('debug argument already defined') @wraps(func) # keyword-only参数传入 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) if __name__ == '__main__': spam(1, 2, 3, debug=True) print('='*20) spam(1, 2, 3)
from functools import wraps import inspect def optional_debug(func): # 对函数的默认变量名参数的变量名进行查寻 args = inspect.getfullargspec(func).args # print(args) if 'debug' in args: raise TypeError('debug argument already defined') @wraps(func) # keyword-only参数传入 def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) # 下面使修改被装饰函数的参数签名信息 sig = inspect.signature(func) params = list(sig.parameters.values()) # print(params) params.append(inspect.Parameter('debug', inspect.Parameter.KEYWORD_ONLY, default=False)) wrapper.__signature__ = sig.replace(parameters=params) return wrapper @optional_debug def spam(a, b, c): print(a, b, c) if __name__ == '__main__': spam(1, 2, 3, debug=True) print('='*20) spam(1, 2, 3) print(inspect.signature(spam))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t11_3.py [<Parameter "a">, <Parameter "b">, <Parameter "c">] Calling spam 1 2 3 ==================== 1 2 3 (a, b, c, *, debug=False) Process finished with exit code 0
9.12 利用装饰器给类打补丁