Python CookBook 学习笔记
Table of Contents
第九章 元编程¶
软件开发领域中最经典的口头禅就是 “don’t repeat yourself”. 也就是说,任何时候当你的程序中存在高度重复(或者是通过剪切复制)的代码时,都应该想想是否有更好的解决方案。在Python 当中,通常都可以通过元编程来解决这类问题。简而言之,元编程就是关于创建操作源代码(比如修改、生成或包装原来的代码) 的函数和类。主要技术是使用装饰器、类装饰器和元类。不过还有一些其他技术,包括签名对象、使用 exec() 执行代码以及对内部函数和类的反射技术等。本章的主要目的是向大家介绍这些元编程技术,并且给出实例来演示它们是怎样定制化你的源代码行为的。
比较重要的小节:
- 9.5 可自定义属性的装饰器
- 9.9 将装饰器定义为类
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
@timethis
def countdown(n):
'''
Counts down
'''
while n > 0:
n -= 1
countdown(100000)
countdown(10000000)
讨论:在上面的 wrapper() 函数中, 装饰器内部定义了一个使用 *args
和 **kwargs
来接受任意参数的函数。 在这个函数里面调用了原始函数并将其结果返回,不过可以添加其他额外的代码(比如计时)。 然后这个新的函数包装器被作为结果返回来代替原始函数。
需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。 使用 *args
和 **kwargs
目的就是确保任何参数都能适用。 而返回结果值基本都是调用原始函数 func(*args, **kwargs)
的返回结果,其中 func 就是原始函数。@wraps(func)
复制原始函数的元数据,比如函数名等
9.2 创建装饰器时保留函数元信息¶
问题:你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。
解决方案:任何时候定义装饰器,都应该使用 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
# 下面我们使用这个被包装后的函数并检查它的元信息:
@timethis
def countdown(n):
'''
Counts down
'''
while n > 0:
n -= 1
countdown(100000)
countdown.__name__
countdown.__doc__
countdown.__annotations__
讨论:@wraps 有一个重要特征是它能让你通过属性 __wrapped__
直接访问被包装函数。
countdown.__wrapped__(100000)
__wrapped__
属性还能让被装饰函数正确暴露底层的参数签名信息。
from inspect import signature
print(signature(countdown))
9.3 解除一个装饰器¶
问题:一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。
解决方案:假设装饰器是通过 @wraps (参考9.2小节)来实现的,那么你可以通过访问 __wrapped__
属性来访问原始函数:
@timethis
def add(x, y):
return x + y
orig_add = add.__wrapped__
orig_add(3, 4)
讨论:直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。 但是这里的方案仅仅适用于在包装器中正确使用了 @wraps 或者直接设置了 __wrapped__
属性的情况。
如果有多个包装器,那么访问 __wrapped__
属性的行为是不可预知的,应该避免这样做。 在 Python3.3 中,它会略过所有的包装层,但在 Python 3.4 中却不一样
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def add(x, y):
return x + y
下面我们在Python3.3下测试:
add(2, 3)
add.__wrapped__(2, 3)
下面我们在Python3.4下测试:
add(2, 3)
add.__wrapped__(2, 3)
并不是所有的装饰器都使用了 @wraps ,因此这里的方案并不全部适用,内置的装饰器 @staticmethod 和 @classmethod 就没有遵循这个约定 (它们把原始函数存储在属性 func 中)
from functools import wraps
import logging
def logged(level, name=None, message=None):
"""
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
"""
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
add(2, 3)
spam()
最外层的函数 logged() 接受参数并将它们作用在内部的装饰器函数上面。 内层的函数 decorate() 接受一个函数作为参数,然后在函数上面放置一个包装器。带参数的装饰器就是在传统的装饰器外层再加一层包装而已。
讨论:
@logged(logging.CRITICAL, 'example')
def func(a, b):
pass
func(1, 2)
装饰器处理过程跟下面的调用是等效的,这很重要
def func(a, b):
pass
func = decorator(x, y, z)(func)
9.5 可自定义属性的装饰器¶
问题:你想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为。
解决方案:引入一个访问函数,使用 nonlocal 来修改内部变量。 然后这个访问函数被作为一个属性赋值给包装函数。
from functools import wraps, partial
import logging
# Utility decorator to attach a function as an attribute of obj
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):
'''
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
'''
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
# Attach setter functions
@attach_wrapper(wrapper)
def set_level(newlevel):
nonlocal level
level = newlevel
@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
# 下面是交互环境下的使用例子:
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> add(2, 3)
DEBUG:__main__:add
5
>>> # Change the log message
>>> add.set_message('Add called')
>>> add(2, 3)
DEBUG:__main__:Add called
5
>>> # Change the log level
>>> add.set_level(logging.WARNING)
>>> add(2, 3)
WARNING:__main__:Add called
5
讨论:这一小节的关键点在于访问函数(如 set_message() 和 set_level() ),被作为属性赋给包装器。每个访问函数允许使用 nonlocal 来修改函数内部的变量。
另一个值得注意的地方是访问函数会在多层装饰器间传播(如果你的装饰器都使用了 @functools.wraps 注解)。即使装饰器以相反的方向排放,效果也是一样的
@timethis
@logged(logging.DEBUG)
def countdown(n):
while n > 0:
n -= 1
# 你会发现访问函数依旧有效
>>> countdown(10000000)
DEBUG:__main__:countdown
countdown 0.8198461532592773
>>> countdown.set_level(logging.WARNING)
>>> countdown.set_message("Counting down to zero")
>>> countdown(10000000)
WARNING:__main__:Counting down to zero
countdown 0.8225970268249512
@logged(logging.DEBUG)
@timethis
def countdown(n):
while n > 0:
n -= 1
还能通过使用lambda表达式代码来让访问函数返回不同的设定值:
@attach_wrapper(wrapper)
def get_level():
return level
# Alternative
wrapper.get_level = lambda: level
一个比较难理解的地方就是对于访问函数的首次使用。例如,你可能会考虑另外一个方法直接访问函数的属性,如下:
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.log.log(wrapper.level, wrapper.logmsg)
return func(*args, **kwargs)
# Attach adjustable attributes
wrapper.level = level
wrapper.logmsg = logmsg
wrapper.log = log
这个方法也可能正常工作,但前提是它必须是最外层的装饰器才行。 如果它的上面还有另外的装饰器(比如上面提到的 @timethis 例子),那么它会隐藏底层属性,使得修改它们没有任何作用。 而通过使用访问函数就能避免这样的局限性。
9.6 带可选参数的装饰器¶
问题:你想写一个装饰器,既可以不传参数给它,比如 @decorator , 也可以传递可选参数给它,比如 @decorator(x,y,z) 。
解决方案:
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)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
讨论:这个问题就是通常所说的编程一致性问题。 当我们使用装饰器的时候,大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。 其实从技术上来讲,我们可以定义一个所有参数都是可选的装饰器,就像下面这样:
@logged()
def add(x, y):
return x+y
但是,这种写法并不符合我们的习惯,有时候程序员忘记加上后面的括号会导致错误。这里我们向你展示了如何以一致的编程风格来同时满足没有括号和有括号两种情况。注意两种装饰器的等效调用形式:
# 第一种
@logged
def add(x, y):
return x + y
# 等效
def add(x, y):
return x + y
add = logged(add)
# 第二种
@logged(level=logging.CRITICAL, name='example')
def spam():
print('Spam!')
# 等效
def spam():
print('Spam!')
spam = logged(level=logging.CRITICAL, name='example')(spam)
对于一个无参数的简单装饰器,被装饰函数会被当做第一个参数直接传递给 logged 装饰器。 因此,logged()中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值。
而对于一个有参数的装饰器,初始调用 logged() 函数时,被包装函数并没有传递进来。 因此在装饰器内,它必须是可选的。这个反过来会迫使其他参数必须使用关键字来指定。 并且,但这些参数被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。 为了这样做,利用 functools.partial 。它会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了。
9.7 利用装饰器强制函数上的类型检查¶
问题:作为某种编程规约,你想在对函数参数进行强制类型检查。
解决方案:先说明我们的目标:能对函数参数类型进行断言
>>> @typeassert(int, int)
... def add(x, y):
... return x + y
...
>>>
>>> add(2, 3)
5
>>> add(2, 'hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument y must be <class 'int'>
下面是使用装饰器技术来实现 @typeassert :
from inspect import signature
from functools import wraps
def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
# If in optimized mode, disable type checking
if not __debug__:
return func
# Map function argument names to supplied types
sig = signature(func)
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
@wraps(func)
def wrapper(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
# Enforce type assertions across supplied arguments
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
可以看出这个装饰器非常灵活,既可以指定所有参数类型,也可以只指定部分。 并且可以通过位置或关键字来指定参数类型。
>>> @typeassert(int, z=int)
... def spam(x, y, z=42):
... print(x, y, z)
...
>>> spam(1, 2, 3)
1 2 3
>>> spam(1, 'hello', 3)
1 hello 3
>>> spam(1, 'hello', 'world')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument z must be <class 'int'>
讨论:首先,装饰器只会在函数定义时被调用一次。 有时候你去掉装饰器的功能,那么你只需要简单的返回被装饰函数即可。如果全局变量 debug 被设置成了False(当你使用-O或-OO参数的优化模式执行程序时), 那么就直接返回未修改过的函数本身 其次,这里还对被包装函数的参数签名进行了检查,我们使用了 inspect.signature() 函数。 简单来讲,它运行并提取一个可调用对象的参数签名信息。
>>> from inspect import signature
>>> def spam(x, y, z=42):
... pass
...
>>> sig = signature(spam)
>>> print(sig)
(x, y, z=42)
>>> sig.parameters
mappingproxy(OrderedDict([('x', <Parameter at 0x10077a050 'x'>),
('y', <Parameter at 0x10077a158 'y'>), ('z', <Parameter at 0x10077a1b0 'z'>)]))
>>> sig.parameters['z'].name
'z'
>>> sig.parameters['z'].default
42
>>> sig.parameters['z'].kind
<_ParameterKind: 'POSITIONAL_OR_KEYWORD'>
装饰器的开始部分,使用了 bind_partial() 方法来执行从指定类型到名称的部分绑定。
>>> bound_types = sig.bind_partial(int,z=int)
>>> bound_types
<inspect.BoundArguments object at 0x10069bb50>
>>> bound_types.arguments
OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
在这个部分绑定中,注意到缺失的参数被忽略了(比如并没有对y进行绑定)。 不过最重要的是创建了一个有序字典 bound_types.arguments 。 这个字典会将参数名以函数签名中相同顺序映射到指定的类型值上面去。 在装饰器例子中,这个映射包含了我们要强制指定的类型断言。在装饰器创建的实际包装函数中使用到了 sig.bind() 方法。 bind() 跟 bind_partial()类似,但是它不允许忽略任何参数。因此有了下面的结果:
>>> bound_values = sig.bind(1, 2, 3)
>>> bound_values.arguments
OrderedDict([('x', 1), ('y', 2), ('z', 3)])
使用这个映射我们可以很轻松的实现我们的强制类型检查。不过这个方案还有点小瑕疵,它对于有默认值的参数并不适用。 比如下面的代码可以正常工作,尽管items的类型是错误的
>>> @typeassert(int, list)
... def bar(x, items=None):
... if items is None:
... items = []
... items.append(x)
... return items
>>> bar(2)
[2]
>>> bar(2,3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument items must be <class 'list'>
>>> bar(4, [1, 2, 3])
[1, 2, 3, 4]
最后一点是关于适用装饰器参数和函数注解之间的争论。 例如,为什么不像下面这样写一个装饰器来查找函数中的注解呢?
@typeassert
def spam(x:int, y, z:int = 42):
print(x,y,z)
一个可能的原因是如果使用了函数参数注解,如果注解被用来做类型检查就不能做其他事情了。而且 @typeassert 不能再用于使用注解做其他事情的函数了。而使用上面的装饰器参数灵活性大多了,也更加通用。
9.8 将装饰器定义为类的一部分¶
问题:你想在类中定义装饰器,并将其作用在其他函数或方法上。
解决方案:在类里面定义装饰器很简单,但是首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法。
from functools import wraps
class A:
# Decorator as an instance method
def decorator1(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
# Decorator as a class method
@classmethod
def decorator2(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
使用
# As an instance method
a = A()
@a.decorator1
def spam():
pass
# As a class method
@A.decorator2
def grok():
pass
一个是实例调用,一个是类调用。
讨论:特别的 @property 装饰器实际上是一个类,它里面定义了三个方法 getter(), setter(), deleter() , 每一个方法都是一个装饰器。
它为什么要这么定义的主要原因是,各种不同的装饰器方法会在关联的 property 实例上操作它的状态。 因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法。
在类中定义装饰器有个难理解的地方就是对于额外参数 self 或 cls 的正确使用。 尽管最外层的装饰器函数比如 decorator1() 或 decorator2() 需要提供一个 self 或 cls 参数, 但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个 self 参数。 你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。
对于类里面定义的包装器还有一点比较难理解,就是在**涉及到继承的时候。例如,假设你想让在A中定义的装饰器作用在子类B中。需要这样写:
class B(A):
@A.decorator2
def bar(self):
pass
也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。 你不能使用 @B.decorator2 ,因为在方法定义时,这个类B还没有被创建。
9.9 将装饰器定义为类¶
问题:你想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。需要让装饰器可以同时工作在类定义的内部和外部。
解决方案:为了将装饰器定义成一个实例,需要确实现 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)
可以当做一个普通的装饰器来使用,在类里面或外面都可以:
@Profiled
def add(x, y):
return x + y
class Spam:
@Profiled
def bar(self, x):
print(self, x)
在交互环境中的使用示例:
>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls
2
>>> s = Spam()
>>> s.bar(1)
<__main__.Spam object at 0x10069e9d0> 1
>>> s.bar(2)
<__main__.Spam object at 0x10069e9d0> 2
>>> s.bar(3)
<__main__.Spam object at 0x10069e9d0> 3
>>> Spam.bar.ncalls
3
讨论:通常很容易会忽视上面的 get() 方法。如果你忽略它,保持其他代码不变再次运行, 你会发现当你去调用被装饰实例方法时出现很奇怪的问题。
>>> s = Spam()
>>> s.bar(3)
Traceback (most recent call last):
...
TypeError: bar() missing 1 required positional argument: 'x'
出错原因是当方法函数在一个类中被查找时,它们的 get() 方法依据描述器协议被调用, 在 8.9小节已经讲述过描述器协议了。在这里,get() 的目的是创建一个绑定方法对象 (最终会给这个方法传递self参数)。下面是一个例子来演示底层原理:
>>> s = Spam()
>>> def grok(self, x):
... pass
...
>>> grok.__get__(s, Spam)
<bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
get() 方法是为了确保绑定方法对象能被正确的创建。 type.MethodType() 手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。 如果这个方法是在类上面来访问, 那么 get() 中的instance参数会被设置成None并直接返回 Profiled 实例本身。 这样的话我们就可以提取它的 ncalls 属性了。
也可以考虑另外一个使用闭包和 nonlocal 变量实现的装饰器,这个在9.5小节有讲到。
import types
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
# Example
@profiled
def add(x, y):
return x + y
这个方式跟之前的效果几乎一样,除了对于 ncalls 的访问现在是通过一个被绑定为属性的函数来实现。
9.10 为类和静态方法提供装饰器¶
问题:你想给类或静态方法提供装饰器。
解决方案:给类或静态方法提供装饰器是很简单的,不过要确保装饰器在 @classmethod 或 @staticmethod 的内层,即先调用自定义的装饰器。
import time
from functools import wraps
# A simple decorator
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print(end-start)
return r
return wrapper
# Class illustrating application of the decorator to different kinds of methods
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
讨论:如果把装饰器的顺序写错了,调用它时就会出错。
问题在于 @classmethod 和 @staticmethod 实际上并不会创建可直接调用的对象, 而是创建特殊的描述器对象(参考8.9小节)。因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。 确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题。
当我们在抽象基类中定义类方法和静态方法(参考8.12小节)时,这里讲到的知识就很有用了。 例如,如果你想定义一个抽象类方法,可以使用类似下面的代码:
from abc import ABCMeta, abstractmethod
class A(metaclass=ABCMeta):
@classmethod
@abstractmethod
def method(cls):
pass
在这段代码中,@classmethod 跟 @abstractmethod 两者的顺序是有讲究的,如果你调换它们的顺序就会出错。
9.11 装饰器为被包装函数增加参数¶
问题:你想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则。
解决方案:可以使用关键字参数来给被包装函数增加额外参数。
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
讨论:用装饰器给函数增加参数的做法并不常见,不过有时可以避免一些重复代码。
例如,如果你有下面这样的代码:
def a(x, debug=False):
if debug:
print('Calling a')
def b(x, y, z, debug=False):
if debug:
print('Calling b')
def c(x, y, debug=False):
if debug:
print('Calling c')
那么你可以将其重构成这样:
from functools import wraps
import inspect
def optional_debug(func):
if 'debug' in inspect.getargspec(func).args:
raise TypeError('debug argument already defined')
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
@optional_debug
def a(x):
pass
@optional_debug
def b(x, y, z):
pass
@optional_debug
def c(x, y):
pass
这种实现方案之所以行得通,在于强制关键字参数很容易被添加到接受 *args
和 **kwargs
参数的函数中。 通过使用强制关键字参数,它被作为一个特殊情况被挑选出来, 并且接下来仅仅使用剩余的位置和关键字参数去调用这个函数时,这个特殊参数会被排除在外,它并不会被纳入到 `kwargs中去**。还有一个难点就是如何去处理被添加的参数与被包装函数参数直接的名字冲突。 例如,如果装饰器
@optional_debug作用在一个已经拥有一个
debug` 参数的函数上时会有问题。 这里我们增加了一步名字检查。
上面的方案还可以更完美一点,因为精明的程序员应该发现了被包装函数的函数签名其实是错误的。优化:
from functools import wraps
import inspect
def optional_debug(func):
if 'debug' in inspect.getargspec(func).args:
raise TypeError('debug argument already defined')
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
sig = inspect.signature(func)
parms = list(sig.parameters.values())
parms.append(inspect.Parameter('debug',
inspect.Parameter.KEYWORD_ONLY,
default=False))
wrapper.__signature__ = sig.replace(parameters=parms)
return wrapper
通过这样的修改,包装后的函数签名就能正确的显示 debug
参数的存在了。
>>> @optional_debug
... def add(x,y):
... return x+y
...
>>> print(inspect.signature(add))
(x, y, *, debug=False)
>>> add(2,3)
5
9.12 使用装饰器扩充类的功能¶
问题:你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式。
解决方案:这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法 __getattribute__
的类装饰器, 可以打印日志:
def log_getattribute(cls):
# Get the original implementation
orig_getattribute = cls.__getattribute__
# Make a new definition
def new_getattribute(self, name):
print('getting:', name)
return orig_getattribute(self, name)
# Attach to the class and return
cls.__getattribute__ = new_getattribute
return cls
# Example use
@log_getattribute
class A:
def __init__(self,x):
self.x = x
def spam(self):
pass
讨论:装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案。下面是一个采用混入方法的实现,参见 8.7 节
class LoggedGetattribute:
def __getattribute__(self, name):
print('getting:', name)
return super().__getattribute__(name)
# Example:
class A(LoggedGetattribute):
def __init__(self,x):
self.x = x
def spam(self):
pass
某种程度上来讲,类装饰器方案就显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。
如果你是想在一个类上面使用多个类装饰器,那么就需要注意下顺序问题。 例如,一个装饰器 A 会将其装饰的方法完整替换成另一种实现, 而另一个装饰器 B 只是简单的在其装饰的方法中添加点额外逻辑。 那么这时候装饰器A就需要放在装饰器B的前面。
9.13 使用元类控制实例的创建¶
问题:你想通过改变实例创建方式来实现单例、缓存或其他类似的特性。
解决方案:Python 程序员都知道,如果你定义了一个类,就能像函数一样的调用它来创建实
例。如果想自定义这个步骤,可以定义一个元类并自己实现 __call__()
方法。假设想定义一个不能被创建实例的类
class NoInstances(type):
def __call__(self, *args, **kwargs):
raise TypeError("Can't instantiate directly")
# Example
class Spam(metaclass=NoInstances):
@staticmethod
def grok(x):
print('Spam.grok')
用户只能调用这个类的静态方法,而不能使用通常的方法来创建它的实例
>>> Spam.grok(42)
Spam.grok
>>> s = Spam()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example1.py", line 7, in __call__
raise TypeError("Can't instantiate directly")
TypeError: Can't instantiate directly
实现单例模式(只能创建唯一实例的类)
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
# Example
class Spam(metaclass=Singleton):
def __init__(self):
print('Creating Spam')
那么Spam类就只能创建唯一的实例了。
>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True
创建 8.25 小节中那样的缓存实例,可以通过元类来实现:
import weakref
class Cached(type):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cache = weakref.WeakValueDictionary()
def __call__(self, *args):
if args in self.__cache:
return self.__cache[args]
else:
obj = super().__call__(*args)
self.__cache[args] = obj
return obj
# Example
class Spam(metaclass=Cached):
def __init__(self, name):
print('Creating Spam({!r})'.format(name))
self.name = name
>>> a = Spam('Guido')
Creating Spam('Guido')
>>> b = Spam('Diana')
Creating Spam('Diana')
>>> c = Spam('Guido') # Cached
>>> a is b
False
>>> a is c # Cached value returned
True
讨论:利用元类实现多种实例创建模式通常要比不使用元类的方式优雅得多。
假设你不使用元类,你可能需要将类隐藏在某些工厂函数后面。 比如实现一个单例
class _Spam:
def __init__(self):
print('Creating Spam')
_spam_instance = None
def Spam():
global _spam_instance
if _spam_instance is not None:
return _spam_instance
else:
_spam_instance = _Spam()
return _spam_instance
使用元类可能会涉及到比较高级点的技术,但是它的代码看起来会更加简洁舒服,而且也更加直观。更多关于创建缓存实例、弱引用等内容,参考 8.25 小节。
9.24 解析与分析Python 源码¶
Not Now
9.25 拆解Python 字节码¶
Not Now