元编程
在函数上添加包装器
如果想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)。可以定义一个装饰器函数,例如:
1 | import time |
如下是使用装饰器的例子:
1 | @timethis |
一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数,如下面这种写法:
1 |
|
其实类似于下面这种写法:
1 | def countdown(n): |
顺便说一下,内置的装饰器比如@staticmethod
,@classmethod
,@property
原理也是一样的。例如,下面这两个代码片段是等价的:
1 | class A: |
在上面的wrapper()
函数中, 装饰器内部定义了一个使用*args
和**kwargs
来接受任意参数的函数。在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外的代码(比如计时)。 然后这个新的函数包装器被作为结果返回来代替原始函数。
需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。使用*args
和**kwargs
目的就是确保任何参数都能适用。而返回结果值基本都是调用原始函数func(*args, **kwargs)
的返回结果,其中func
就是原始函数。
刚开始学习装饰器的时候,会使用一些简单的例子来说明,比如上面演示的这个。不过实际场景使用时,还是有一些细节问题要注意的。比如上面使用@wraps(func)
注解是很重要的,它能保留原始函数的元数据。
创建装饰器时保留函数元信息
任何时候你定义装饰器的时候,都应该使用functools
库中的@wraps
装饰器来注解底层包装函数。例如:
1 | import time |
下面我们使用这个被包装后的函数并检查它的元信息:
1 | @timethis |
在编写装饰器的时候复制元信息是一个非常重要的部分。如果你忘记了使用@wraps
, 那么你会发现被装饰函数丢失了所有有用的信息。比如如果忽略@wraps
后的效果是下面这样的:
1 | countdown.__name__ |
@wraps
有一个重要特征是它能让你通过属性__wrapped__
直接访问被包装函数。例如:
1 | 100000) countdown.__wrapped__( |
解除一个装饰器
现在一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。假设装饰器是通过@wraps
来实现的,那么你可以通过访问__wrapped__
属性来访问原始函数:
1 | @somedecorator |
直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。但是这里的方案仅仅适用于在包装器中正确使用了@wraps
或者直接设置了__wrapped__
属性的情况。
如果有多个包装器,那么访问__wrapped__
属性的行为是不可预知的,应该避免这样做。在Python3.3
中,它会略过所有的包装层,比如,假如你有如下的代码:
1 | from functools import wraps |
在Python3.3
测试如下:
1 | 2, 3) add( |
在Python3.4
测试如下:
1 | 2, 3) add( |
需要注意的是,并不是所有的装饰器都使用了@wraps
,因此这里的方案并不全部使用,特别的,内置的装饰器@staticmethod
和@classmethod
就没有遵循这个约定。
定义一个带参数的装饰器
我们用一个例子详细阐述下接受参数的处理过程。假设你想写一个装饰器,给函数添加日志功能,同时允许用户指定日志的级别和其他的选项。下面是这个装饰器的定义和使用示例:
1 | from functools import wraps |
初看这种实现看上去很复杂,但是核心思想很简单。最外层的函数logged()
接受参数并将它们作用在内部的装饰器函数上面。内层的函数decorate()
接受一个函数作为参数,然后在函数上面放置一个包装器。这里的关键点是包装器是可以使用传递给logged()
的参数的。
可自定义属性的装饰器
如果想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为。可以引入一个访问函数,使用nonlocal
来修改内部变量。然后这个访问函数被作为一个属性赋值给包装函数。
1 | from functools import wraps, partial |
下面是交互环境下的使用例子:
1 | import logging |
这一小节的关键点在于访问函数(如set_message()
和set_level()
),它们被作为属性赋给包装器。每个访问函数允许使用nonlocal
来修改函数内部的变量。
带可选参数的装饰器
如果想写一个装饰器,既可以不传参数给它,比如@decorator
, 也可以传递可选参数给它,比如@decorator(x,y,z)
。示例如下:
1 | from functools import wraps, partial |
可以看到,@logged
装饰器可以同时不带参数或带参数。
利用装饰器强制函数上的类型检查
这里的目标是能对函数参数类型进行断言,类似下面这样:
1 | @typeassert(int, int) |
下面是使用装饰器技术来实现@typeassert
:
1 | from inspect import signature |
可以看出这个装饰器非常灵活,既可以指定所有参数类型,也可以只指定部分。并且可以通过位置或关键字来指定参数类型。下面是使用示例:
1 | @typeassert(int, z=int) |
将装饰器定义为类的一部分
如果想在类中定义装饰器,并将其作用在其他函数或方法上。首先要确认它的使用方式,比如到底是作为一个实例方法还是类方法,示例如下:
1 | from functools import wraps |
使用示例如下:
1 | # 作为实例方法 |
在类中定义装饰器初看上去好像很奇怪,但是在标准库中有很多这样的例子。 特别的,@property
装饰器实际上是一个类,它里面定义了三个方法getter()
,setter()
,deleter()
, 每一个方法都是一个装饰器。例如:
1 | class Person: |
为什么要这么定义的主要原因是各种不同的装饰器方法会在关联的property
实例上操作它的状态。 因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法。
在类中定义装饰器有个难理解的地方就是对于额外参数self
或cls
的正确使用。尽管最外层的装饰器函数比如decorator1()
或decorator2()
需要提供一个self
或cls
参数,但是在两个装饰器内部被创建的wrapper()
函数并不需要包含这个self
参数。你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。
对于类里面定义的包装器还有一点比较难理解,就是在涉及到继承的时候。例如,假设你想让在A
中定义的装饰器作用在子类B
中。你需要像下面这样写:
1 | class B(A): |
也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。不能使用@B.decorator2
,因为在方法定义时,这个类B
还没有被创建。
将装饰器定义为类
如果想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。需要让你的装饰器可以同时工作在类定义的内部和外部。
为了将装饰器定义成一个实例,需要确保它实现了__call__()
和__get__()
方法。例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层:
1 | import types |
可以将它当做一个普通的装饰器来使用,在类里面或外面都可以:
1 |
|
在交互环境中的使用示例如下:
1 | 2, 3) add( |
为类和静态方法提供装饰器
给类或静态方法提供装饰器的前提是要确保装饰器在@classmethod
或@staticmethod
之前。例如:
1 | import time |
装饰后的类和静态方法可正常工作,只不过增加了额外的计时功能:
1 | s = Spam() |
如果你把装饰器的顺序写错了就会出错。例如下面这样写:
1 | class Spam: |
调用这个静态方法就会报如下错误:
1 | 1000000) Spam.static_method( |
问题在于@classmethod
和@staticmethod
实际上并不会创建可直接调用的对象,而是创建特殊的描述器对象。因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题。
当我们在抽象基类中定义类方法和静态方法时,例如想定义一个抽象类方法,可以使用类似下面的代码:
1 | from abc import ABCMeta, abstractmethod |
在这段代码中,@classmethod
跟@abstractmethod
两者的顺序是有讲究的,如果调换它们的顺序就会出错。
装饰器为被包装函数增加参数
如果想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则。可以使用关键字参数来给包装函数增加额外参数,考虑如下装饰器。
1 | from functools import wraps |
使用示例如下:
1 | @optional_debug |
通过装饰器来给被包装函数增加参数的做法并不常见。尽管如此,有时候它可以避免一些重复代码。例如,如果有以下代码:
1 | def a(x, debug=False): |
可以将其重构为:
1 | from functools import wraps |
这种实现方案之所以行得通,在于强制关键字参数很容易被添加到接受*args
和**kwargs
参数的函数中。通过使用强制关键字参数,它被作为一个特殊情况被挑选出来,并且接下来仅仅使用剩余的位置和关键字参数去调用这个函数时,这个特殊参数会被排除在外。也就是说,它并不会被纳入到**kwargs
中去。
还有一个难点就是如何去处理被添加的参数与被包装函数参数直接的名字冲突。例如,如果装饰器@optional_debug
作用在一个已经拥有一个debug
参数的函数上时会有问题。这里增加了一步名字检查。
使用装饰器扩充类的功能
如果想通过反省或者重写类定义的某部分来修改它的行为,但是又不希望使用继承或元类的方式。可以使用类装饰器,例如下面是一个重写了特殊方法__getattribute__
的类装饰器,可以打印日志:
1 | def log_getattribute(cls): |
使用示例如下:
1 | 42) a = A( |
使用元类控制实例的创建
如果定义了一个类,就能像寒暑易用的调用它来创建实例,例如:
1 | class Spam: |
如果想自定义这个步骤,可以定义一个元类并自己实现__call__()
方法,如果不希望任何人创建这个类的实例:
1 | class NoInstances(type): |
这样的话,用户只能调用这个类的静态方法,而不能使用通常的方法来创建它的实例。例如:
1 | 42) Spam.grok( |
实现单例模式(只能创建唯一实例的类)的示例如下:
1 | class Singleton(type): |
这样Spam
类就只能创建唯一的实例了,示例如下:
1 | a = Spam() |
如果想创建缓存实例,可以通过元类来实现:
1 | import weakref |
测试如下:
1 | 'Guido') a = Spam( |
args和*kwargs的强制参数签名
对任何涉及到操作函数调用签名的问题,都应该使用inspect
模块中的签名特性。我们最主要关注两个类:Signature
和Parameter
。下面是一个创建函数前面的交互例子:
1 | from inspect import Signature, Parameter |
一旦有了一个签名对象,你就可以使用它的bind()
方法很容易的将它绑定到*args
和**kwargs
上去。下面是一个简单的演示:
1 | def func(*args, **kwargs): |
可以看出来,通过将签名和传递的参数绑定起来,可以强制函数调用遵循特定的规则,比如必填、默认、重复等等。
下面是一个强制函数签名更具体的例子。在代码中,我们在基类中先定义了一个非常通用的__init__()
方法,然后强制所有的子类必须提供一个特定的参数签名。
1 | from inspect import Signature, Parameter |
下面是使用这个Stock
类的示例:
1 | import inspect |
在类上强制使用编程规约
如果想监控类的定义,通常可以通过定义一个元类。一个基本元类通常是继承自type
并重定义它的__new__()
方法 或者是__init__()
方法。比如:
1 | class MyMeta(type): |
另一种是,定义__init__()
方法:
1 | class MyMeta(type): |
为了使用这个元类,通常要将它放到到一个顶级父类定义中,然后其他的类继承这个顶级父类。示例如下:
1 | class Root(metaclass=MyMeta): |
元类的一个关键特点是它允许你在定义的时候检查类的内容。在重新定义__init__()
方法中,你可以很轻松的检查类字典、父类等等。并且,一旦某个元类被指定给了某个类,那么就会被继承到所有子类中去。因此,一个框架的构建者就能在大型的继承体系中通过给一个顶级父类指定一个元类去捕获所有下面子类的定义。
在元类中选择重新定义__new__()
方法还是__init__()
方法取决于你想怎样使用结果类。__new__()
方法在类创建之前被调用,通常用于通过某种方式修改类的定义。 而__init__()
方法是在类被创建之后被调用,当你需要完整构建类对象的时候会很有用。 在最后一个例子中,这是必要的,因为它使用了super()
函数来搜索之前的定义。它只能在类的实例被创建之后,并且相应的方法解析顺序也已经被设置。
以编程方式定义类
可以使用函数types.new_class()
来初始化新的类对象。 你需要做的只是提供类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数。例如:
1 | def __init__(self, name, shares, price): |
这种方式会构建一个普通的类对象,示例如下:
1 | 'ACME', 50, 91.1) s = Stock( |
这种方法中,一个比较难理解的地方是在调用完types.new_class()
对Stock.__module__
的赋值。每次当一个类被定义后,它的__module__
属性包含定义它的模块名。这个名字用于生成__repr__()
方法的输出。它同样也被用于很多库,比如pickle
。因此,为了让你创建的类是“正确”的,你需要确保这个属性也设置正确了。
如果你想创建的类需要一个不同的元类,可以通过types.new_class()
第三个参数传递给它。例如:
1 | import abc |
第三个参数还可以包含其他的关键字参数。比如,一个类的定义如下:
1 | class Spam(Base, debug=True, typecheck=False): |
那么可以将其翻译成如下的new_class()
调用形式:
1 | Spam = types.new_class('Spam', (Base,), |
new_class()
第四个参数最神秘,它是一个用来接受类命名空间的映射对象的函数。通常这是一个普通的字典,但是它实际上是__prepare__()
方法返回的任意对象,这个函数需要使用update()
方法给命名空间增加内容。
在定义的时候初始化类的成员
如果想在类被定义的时候就初始化一部分类的成员,而不是要等到实例被创建后。下面是一个例子,利用这个思路来创建类似于collections
模块中的命名元组的类:
1 | import operator |
这段代码可以用来定义简单的基于元组的数据结构,如下所示:
1 | class Stock(StructTuple): |
使用示例如下:
1 | 'ACME', 50, 91.1) s = Stock( |
这一小节中,类StructTupleMeta
获取到类属性_fields
中的属性名字列表,然后将它们转换成相应的可访问特定元组槽的方法。函数operator.itemgetter()
创建一个访问器函数, 然后property()
函数将其转换成一个属性。
比较难懂的部分是知道不同的初始化步骤是什么时候发生的。StructTupleMeta
中的__init__()
方法只在每个类被定义时被调用一次。cls
参数就是那个被定义的类。实际上,上述代码使用了_fields
类变量来保存新的被定义的类,然后给它再添加一点新的东西。
StructTuple
类作为一个普通的基类,供其他使用者来继承。这个类中的__new__()
方法用来构造新的实例。这里使用__new__()
并不是很常见,主要是因为我们要修改元组的调用签名,使得我们可以像普通的实例调用那样创建实例。就像下面这样:
1 | s = Stock('ACME', 50, 91.1) # OK |
跟__init__()
不同的是,__new__()
方法在实例被创建之前被触发。由于元组是不可修改的,所以一旦它们被创建了就不可能对它做任何改变。而__init__()
会在实例创建的最后被触发,这也是为什么__new__()
方法已经被定义了。
避免重复的属性方法
考虑下一个简单的类,它的属性由属性方法包装:
1 | class Person: |
可以看到,为了实现属性值的类型检查我们写了很多的重复代码。只要你以后看到类似这样的代码,你都应该想办法去简化它。一个可行的方法是创建一个函数用来定义属性并返回它。例如:
1 | def typed_property(name, expected_type): |
本节我们演示内部函数或者闭包的一个重要特性,它们很像一个宏。例子中的函数typed_property()
看上去有点难理解,其实它所做的仅仅就是为你生成属性并返回这个属性对象。因此,当在一个类中使用它的时候,效果跟将它里面的代码放到类定义中去是一样的。尽管属性的getter
和setter
方法访问了本地变量如name
,expected_type
以及storate_name
,这些变量的值会保存在闭包当中。
我们还可以使用functools.partial()
像下面这样:
1 | from functools import partial |
定义上下文管理器
实现一个新的上下文管理器的最简单的方法就是使用contexlib
模块中的@contextmanager
装饰器。下面是一个实现了代码块计时功能的上下文管理器例子:
1 | import time |
在函数timethis()
中,yield
之前的代码会在上下文管理器中作为__enter__()
方法执行,所有在yield
之后的代码会作为__exit__()
方法执行。 如果出现了异常,异常会在yield
语句那里抛出。
通常情况下,如果要写一个上下文管理器,你需要定义一个类,里面包含一个__enter__()
和一个__exit__()
方法,如下所示:
1 | import time |
@contextmanager
应该仅仅用来写自包含的上下文管理函数。如果你有一些对象(比如一个文件、网络连接或锁),需要支持with
语句,那么你就需要单独实现 __enter__()
方法和__exit__()
方法。