在函数上添加包装器
问题:
你想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)
解决方案:
如果你想使用额外的代码包装一个函数,可以定义一个装饰器函数,例如:
1 import time 2 from functools import wraps 3 4 5 def timethis(func): 6 @wraps(func) 7 def wrapper(*args,**kwargs): 8 start = time.time() 9 result = func(*args,**kwargs) 10 end = time.time() 11 print(func.__name__,end-start) 12 return result 13 return wrapper 14 15 @timethis #等同于timethis(countdown) 16 def countdown(n): 17 while n > 0: 18 n -= 1 19 20 countdown(100000) 21 countdown(1000000)
以上代码的执行结果为:
countdown 0.008307218551635742
countdown 0.10066509246826172
创建装饰器时保留函数元信息
问题:
你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了
解决方案:
任何时候你定义装饰器的时候,都应该使用functools 库中的@wraps 装饰器来注解底层包装函数。例如:
1 import time 2 from functools import wraps 3 4 def timethis(func): 5 @wraps(func) 6 def wrapper(*args, **kwargs): 7 start = time.time() 8 result = func(*args, **kwargs) 9 end = time.time() 10 print(func.__name__, end-start) 11 return result 12 return wrapper 13 14 @timethis 15 def countdown(n:int): 16 """ 17 装饰器内部调用的函数 18 :param n: 19 :return: 20 """ 21 while n>0: 22 n -= 1 23 24 return 'countdown' 25 26 countdown(100000) 27 print('name:', countdown.__name__) 28 print('doc:', countdown.__doc__) 29 print('annotations:', countdown.__annotations__) 30 31 ''' 32 @wraps 有一个重要特征是它能让你通过属性wrapped 直接访问被包装函数。例如: 33 ''' 34 print('wrapped:', countdown.__wrapped__(100000)) 35 36 ''' 37 wrapped 属性还能让被装饰函数正确暴露底层的参数签名信息。例如: 38 ''' 39 from inspect import signature 40 print('signatrue:', signature(countdown))
以上代码的执行结果为:
countdown 0.008561134338378906 name: countdown doc: 装饰器内部调用的函数 :param n: :return: annotations: {'n': <class 'int'>} wrapped: countdown signatrue: (n:int)
解除一个装饰器
问题:
一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数
解决方案:
假设装饰器是通过@wraps来实现的,那么你可以通过访问__wrapped__ 属性来访问原始函数:
1 from functools import wraps 2 3 4 def decorator(func): 5 @wraps(func) 6 def wrapper(x, y): 7 result = func(x, y) 8 return result 9 return wrapper 10 11 @decorator 12 def add(x, y): 13 return x + y 14 15 orig_add = add.__wrapped__ 16 print('orig_add:', orig_add(3, 5))
以上代码的执行结果为:
orig_add: 8
定义一个带参数的装饰器
问题:
你想定义一个可以接受参数的装饰器
解决方案:
我们用一个例子详细阐述下接受参数的处理过程。假设你想写一个装饰器,给函数添加日志功能,当时允许用户指定日志的级别和其他的选项。下面是这个装饰器的定义和使用示例:
1 from functools import wraps 2 import logging 3 4 def logged(level, name=None, message=None): 5 """ 6 Add logging to a function. level is the logging 7 level, name is the logger name, and message is the 8 log message. If name and message aren't specified, 9 they default to the function's module and name. 10 """ 11 def decorate(func): 12 logname = name if name else func.__module__ 13 log = logging.getLogger(logname) 14 logmsg = message if message else func.__name__ 15 16 @wraps(func) 17 def wrapper(*args, **kwargs): 18 log.log(level, logmsg) 19 return func(*args, **kwargs) 20 return wrapper 21 return decorate 22 23 @logging(logging.DEBUG) #等同于logging(loggin.DEBUG)(add)(x,y) 24 def add(x, y): 25 return x +y 26 27 28 @logged(logging.CRITICAL, 'example') 29 def spam(): 30 print('Spam!')
可自定义属性的装饰器
问题:
你想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为
解决方案:
引入一个访问函数,使用nolocal 来修改内部变量。然后这个访问函数被作为一个属性赋值给包装函数
1 def logged(level, name=None, message=None): 2 ''' 3 Add logging to a function. level is the logging 4 level, name is the logger name, and message is the 5 log message. If name and message aren't specified, 6 they default 7 ''' 8 def decorate(func): 9 logname = name if name else func.__module__ 10 log = logging.getLogger(logname) 11 logmsg = message if message else func.__name__ 12 13 @wraps(func) 14 def wrapper(*args, **kwargs): 15 log.log(level, logmsg) 16 return func(*args, **kwargs) 17 18 @attch_wrapper(wrapper) 19 def set_level(newlevel): 20 nonlocal level 21 level = newlevel 22 23 @attch_wrapper(wrapper) 24 def set_message(newmsg): 25 nonlocal logmsg 26 logmsg = newmsg 27 28 return wrapper 29 30 return decorate 31 32 @logged(logging.DEBUG) 33 def add(x, y): 34 return x + y 35 36 @logged(logging.CRITICAL, 'example') 37 def spam(): 38 print('Spam!') 39 40 import logging 41 logging.basicConfig(level=logging.DEBUG) 42 print(add(10, 11))
以上代码的执行结果为:
21 DEBUG:__main__:add
带可选参数的装饰器
问题:
你想写一个装饰器,既可以不传参数给它,比如@decorator ,也可以传递可选参数给它,比如@decorator(x,y,z)
解决方案:
下面是日志装饰器的一个修改版本:
1 from functools import wraps, partial 2 import logging 3 4 5 def logged(func=None, *, level=logging.DEBUG, name=None, message=None): 6 if func is None: 7 return partial(logged, level=level, name=name, message=message) 8 9 logname = name if name else func.__module__ 10 log = logging.getLogger(logname) 11 logmsg = message if message else func.__name__ 12 13 @wraps(func) 14 def wrapper(*args, **kwargs): 15 log.log(level, logmsg) 16 return func(*args, **kwargs) 17 18 return wrapper 19 20 @logged 21 def add(x, y): 22 return x + y 23 24 @logged(level=logging.CRITICAL, name='example') 25 def spam(): 26 print('Spam!') 27 28 print(add(10, 20)) 29 print('*'*40) 30 spam()
以上代码的执行结果为:
30 spam **************************************** Spam!
总结:
这里提到的这个问题就是通常所说的编程一致性问题。当我们使用装饰器的时候,大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。其实从技术上来讲,我们可以定义一个所有参数都是可选的装饰器,就像下面这样:
@logged def add(x, y): """ 等同于 add = logged(add) 这时候,被装饰函数会被当做第一个参数直接传递给logged 装饰器。因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值 """ return x + y @logged(level=logging.CRITICAL, name='example') def spam(): """ 等同于 spam = logged(level=logging.CRITICAL, name='example')(spam) 初始调用logged() 函数时,被包装函数并没有传递进来。因此在装饰器内,它必 须是可选的。这个反过来会迫使其他参数必须使用关键字来指定。并且,但这些参数 被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。为了这样做,我 们使用了一个技巧,就是利用functools.partial 。它会返回一个未完全初始化自 身,除了被包装函数外其他参数都已经确定下来了。 """ print('Spam!')
利用装饰器强制函数上的类型检查
问题:
作为某种编程规约,你想在对函数参数进行强制类型检查
解决方案:
在演示实际代码前,先说明我们的目标:能对函数参数类型进行断言,类似下面这样:
1 from inspect import signature 2 from functools import wraps 3 4 5 def typeassert(*ty_args, **ty_kwargs): 6 def decorate(func): 7 if not __debug__: 8 return func 9 10 sig = signature(func) 11 bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments 12 13 @wraps(func) 14 def wrapper(*args, **kwargs): 15 bound_values = sig.bind(*args, **kwargs) 16 for name, value in bound_values.arguments.items(): 17 if name in bound_types: 18 if not isinstance(value, bound_types[name]): 19 raise TypeError( 20 'Argument {} must be {}'.format(name, bound_types[name]) 21 ) 22 return func(*args, **kwargs) 23 return wrapper 24 return decorate 25 26 27 @typeassert(int, z=int) 28 def spam(x, y, z=42): 29 print(x, y, z) 30 31 spam(1, 2, 3) 32 spam(1, 'hello') 33 ''' 34 报错 35 spam(1, 'hello', 'world') 36 TypeError: Argument z must be <class 'int'> 37 '''
以上代码的执行结果为:
1 2 3 1 hello 42
其次, 这里还对被包装函数的参数签名进行了检查, 我们使用了inspect.signature() 函数。简单来讲,它运行你提取一个可调用对象的参数签名信息。例如:
1 from inspect import signature 2 3 4 def spam(x, y, z=42): 5 return x + y 6 7 sig = signature(spam) 8 print('sig:', sig) 9 print('sig parameters:', sig.parameters) 10 print('sig paramenters z:', sig.parameters['z'].name) 11 print('sig paramenters name:', sig.parameters['z'].default) 12 print('sig paramenters kind:', sig.parameters['z'].kind)
以上代码的执行结果为:
sig: (x, y, z=42) sig parameters: OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)]) sig paramenters z: z sig paramenters name: 42 sig paramenters kind: POSITIONAL_OR_KEYWORD
将装饰器定义为类的一部分
问题:
你想在类中定义装饰器,并将其作用在其他函数或方法上
解决方案:
在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法。下面我们用例子来阐述它们的不同:
1 from functools import wraps 2 3 4 class A: 5 def decorator1(self, func): 6 @wraps(func) 7 def wrapper(*args, **kwargs): 8 print('Decorator 1') 9 return func(*args, **kwargs) 10 return wrapper 11 12 @classmethod 13 def decorator2(cls, func): 14 @wraps(func) 15 def wrapper(*args, **kwargs): 16 print('Decorator 2') 17 return func(*args, **kwargs) 18 return wrapper 19 20 a = A() 21 22 @a.decorator1 23 def spam(): 24 pass 25 26 @A.decorator2 27 def grok(): 28 pass
为类和静态方法提供装饰器
问题:
你想给类或静态方法提供装饰器
解决方案:
给类或静态方法提供装饰器是很简单的,不过要确保装饰器在@classmethod 或@staticmethod 之前。例如:
1 import time 2 from functools import wraps 3 4 5 def timethis(func): 6 @wraps(func) 7 def wrapped(*args, **kwargs): 8 start = time.time() 9 result = func(*args, **kwargs) 10 end = time.time() 11 print(end-start) 12 return result 13 return wrapped 14 15 16 class Spam: 17 @timethis 18 def instnce_method(self, n): 19 print(self, n) 20 while n > 0: 21 n -= 1 22 23 @classmethod 24 @timethis 25 def class_method(cls, n): 26 print(cls, n) 27 while n > 0: 28 n -= 1 29 30 s = Spam() 31 s.instnce_method(100000) 32 Spam.class_method(100000)
以上代码的执行结果为:
<__main__.Spam object at 0x101445630> 100000 0.011115074157714844 <class '__main__.Spam'> 100000 0.009546995162963867
装饰器为被包装函数增加参数
问题:
你想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则
解决方案:
可以使用关键字参数来给被包装函数增加额外参数。考虑下面的装饰器:
1 from functools import wraps 2 3 4 def optional_debug(func): 5 @wraps(func) 6 def wrapper(*args,debug=False, **kwargs): 7 if debug: 8 print('Calling', func.__name__) 9 return func(*args, **kwargs) 10 11 return wrapper 12 13 @optional_debug 14 def spam(a, b, c): 15 print(a, b, c) 16 17 spam(1, 2, 3) 18 spam(1, 2, 3, debug=True)
以上代码的执行结果为:
1 2 3
Calling spam
1 2 3
使用装饰器扩充类的功能
问题:
你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式
解决方案:
这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法getattribute 的类装饰器,可以打印日志:
1 def log_getattribute(cls): 2 orig_getattribute = cls.__getattribute__ 3 4 def new_getattribute(self, name): 5 print('getting:', name) 6 return orig_getattribute(self, name) 7 8 cls.__getattribute__ = new_getattribute 9 return cls 10 11 12 @log_getattribute 13 class A: 14 def __init__(self, x): 15 self.x = x 16 17 def spam(self): 18 pass 19 20 a = A(50) 21 print(a.x) 22 a.spam()
以上代码的执行结果为:
getting: x 50 getting: spam
使用元类控制实例的创建
问题:
你想通过改变实例创建方式来实现单例、缓存或其他类似的特性
解决方案:
你可以定义一个元类并自己实现call () 方法。为了演示,假设你不想任何人创建这个类的实例:
1 class NoInstances(type): 2 def __call__(self, *args, **kwargs): 3 raise TypeError("Can't instantiate directly") 4 5 6 class Spam(metaclass=NoInstances): 7 @staticmethod 8 def grok(x): 9 print('不允许实例化~') 10 print('Spam.grok') 11 12 Spam.grok(50) 13 14 ''' 15 s = Spam() 16 s.grok() 17 实例化这种不能调用,报错~ 18 ''' 19 20 #实现单例模式 21 class Singleton(type): 22 def __init__(self, *args, **kwargs): 23 self.__instance = None 24 super().__init__(*args, **kwargs) 25 26 def __call__(self, *args, **kwargs): 27 if self.__instance is None: 28 self._instance = super().__call__(*args, **kwargs) 29 return self.__instance 30 else: 31 return self.__instance 32 33 34 class Spam(metaclass=Singleton): 35 def __init__(self): 36 print('Creating Spam') 37 38 a = Spam() 39 b = Spam() 40 c = Spam() 41 print(a is b and b is c)
以上代码的执行结果为:
不允许实例化~
Spam.grok
Creating Spam
Creating Spam
Creating Spam
True
定义有可选参数的元类
问题:
你想定义一个元类,允许类定义时提供可选参数,这样可以控制或配置类型的创建过程
解决方案:
在定义类的时候,Python 允许我们使用‘‘metaclass‘‘关键字参数来指定特定的元类。例如使用抽象基类:
1 from abc import ABCMeta, abstractclassmethod 2 3 4 class IStream(metaclass=ABCMeta): 5 @abstractclassmethod 6 def read(self, maxsize=None): 7 pass 8 9 @abstractclassmethod 10 def write(self, date): 11 pass 12 13 14 class MyMeta(type): 15 @classmethod 16 def __prepare__(cls, name, bases, *, debug=False, synchronize=False): 17 pass 18 super().__prepare__(name, bases) 19 20 def __new__(cls, name, bases, ns, *, debug=False, synchronize=False): 21 pass 22 return super().__new__(cls, name, bases, ns) 23 24 def __init__(cls, name, bases, ns, *, debug=False, synchronize=False): 25 pass 26 super().__init__(name, bases, ns) 27 28 29 class Spam(metaclass=MyMeta, debug=True, synchronize=True): 30 pass
*args 和**kwargs 的强制参数签名
问题:
你有一个函数或方法,它使用*args 和**kwargs 作为参数,这样使得它比较通用,但有时候你想检查传递进来的参数是不是某个你想要的类型
解决方案:
对任何涉及到操作函数调用签名的问题,你都应该使用inspect 模块中的签名特性。我们最主要关注两个类:Signature 和Parameter 。下面是一个创建函数前面的交互例子:
1 from inspect import Signature, Parameter 2 3 4 parms = [ 5 Parameter('x', Parameter.POSITIONAL_OR_KEYWORD), 6 Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42), 7 Parameter('z', Parameter.KEYWORD_ONLY, default=None) 8 ] 9 10 sig = Signature(parms) 11 print(sig) 12 13 14 def func(*args, **kwargs): 15 bound_values = sig.bind(*args, **kwargs) 16 for name, value in bound_values.arguments.items(): 17 print(name, value) 18 19 func(1, 2, z=3) 20 print(''.center(30, '*')) 21 func(1) 22 print(''.center(30, '*')) 23 func(1, z=3) 24 print(''.center(30, '*')) 25 func(y=2, x=1) 26 print(''.center(30, '-')) 27 ''' 28 报错鸟,为啥,因为只能传递三个参数呀,你四不四傻~ 29 ''' 30 #func(1, 2, 3, 4) 31 32 33 34 ''' 35 另外一种写法 36 ''' 37 print('另外一种写法'.center(30, '*')) 38 39 from inspect import Signature, Parameter,signature 40 def make_sig(*names): 41 parms = [Parameter(name,Parameter.POSITIONAL_OR_KEYWORD) for name in names] 42 return Signature(parms) 43 44 45 class Structure: 46 __signature__ = make_sig() 47 48 def __init__(self, *args, **kwargs): 49 bound_values = self.__signature__.bind(*args, **kwargs) 50 for name, value in bound_values.arguments.items(): 51 setattr(self, name, value) 52 53 54 class Stock(Structure): 55 __signature__ = make_sig('name', 'shares', 'price') 56 57 58 class Point(Structure): 59 __signature__ = make_sig('x', 'y') 60 61 62 print(signature(Stock)) 63 s1 = Stock('ACME', 100, 490.1) 64 ''' 65 报错 66 ''' 67 #s2 = Stock('ACME', 100) 68 #s3 = Stock('ACME', 100, 490.1, shares=50)
以上代码的执行结果为:
(x, y=42, *, z=None) x 1 y 2 z 3 ****************************** x 1 ****************************** x 1 z 3 ****************************** x 1 y 2 ------------------------------ ************另外一种写法************ (name, shares, price)
避免重复的属性方法
问题:
你在类中需要重复的定义一些执行相同逻辑的属性方法,比如进行类型检查,怎样去简化这些重复代码呢?
解决方案:
考虑下一个简单的类,它的属性由属性方法包装:
1 class Person: 2 def __init__(self, name, age): 3 self.name = name 4 self.age = age 5 6 @property 7 def name(self): 8 return self._name 9 10 @name.setter 11 def name(self, value): 12 if not isinstance(value, str): 13 raise TypeError('name must be a string') 14 self._name = value 15 16 @property 17 def age(self): 18 return self._age 19 20 @name.setter 21 def age(self, value): 22 if not isinstance(value, int): 23 raise TypeError('age must be an int') 24 self._age = value 25 26 27 #针对上面的问题,我们需要改良一下 28 def typed_property(name, expected_type): 29 storage_name = '_' + name 30 31 @property 32 def prop(self): 33 return getattr(self, storage_name) 34 35 @prop.setter 36 def prop(self, value): 37 if not isinstance(value, expected_type): 38 raise TypeError('{} must be a {}'.format(name, expected_type)) 39 setattr(self, storage_name, value) 40 41 return prop 42 43 44 #测试实例 45 class Person: 46 name = typed_property('name', str) 47 age = typed_property('age', int) 48 49 def __init__(self, name, age): 50 self.name = name 51 self.age = age 52 53 ''' 54 报错 55 p = Person('这尼玛的是大佬', '18') 56 ''' 57 58 #正常显示输出 59 p = Person('这尼玛的是大佬', 18)
在局部变量域中执行代码
问题:
你想在使用范围内执行某个代码片段,并且希望在执行后所有的结果都不可见
解决方案:
为了理解这个问题,先试试一个简单场景。首先,在全局命名空间内执行一个代码片段:
1 a = 13 2 exec('b = a + 10') 3 print('b的结果为为:', b) 4 5 ''' 6 #执行下面的函数会报错NameError: name 'c' is not defined 7 def test(): 8 a = 13 9 exec('c = a + 5') 10 print(c) 11 test() 12 ''' 13 14 #解决方法,local()可以获取函数内部的所有变量并以字典的方式返回, 15 # 变量名称是字典的key,变量的赋值是value,通过获取local中的c的局部变量在赋值给变量即可 16 def test(): 17 a = 13 18 loc = locals() 19 print('before:', loc) 20 exec('c = a + 5') 21 c = loc['c'] 22 print('c:', c) 23 print('after:', loc) 24 test()
以上代码的执行结果为:
b的结果为为: 23 before: {'a': 13} c: 18 after: {'a': 13, 'loc': {...}, 'c': 18}