PythonTips(Part 2)

元编程

在函数上添加包装器

如果想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)。可以定义一个装饰器函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
import time
from functools import wraps

def timethis(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

如下是使用装饰器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> @timethis
... def countdown(n):
... '''
... Counts down
... '''
... while n > 0:
... n -= 1
...
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown(10000000)
countdown 0.87188299392912
>>>

一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数,如下面这种写法:

1
2
3
@timethis
def countdown(n):
pass

其实类似于下面这种写法:

1
2
3
def countdown(n):
pass
countdown = timethis(countdown)

顺便说一下,内置的装饰器比如@staticmethod,@classmethod,@property原理也是一样的。例如,下面这两个代码片段是等价的:

1
2
3
4
5
6
7
8
9
class A:
@classmethod
def method(cls):
pass

class B:
def method(cls):
pass
method = classmethod(method)

在上面的wrapper()函数中, 装饰器内部定义了一个使用*args**kwargs来接受任意参数的函数。在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外的代码(比如计时)。 然后这个新的函数包装器被作为结果返回来代替原始函数。

需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。使用*args**kwargs目的就是确保任何参数都能适用。而返回结果值基本都是调用原始函数func(*args, **kwargs)的返回结果,其中func就是原始函数。

刚开始学习装饰器的时候,会使用一些简单的例子来说明,比如上面演示的这个。不过实际场景使用时,还是有一些细节问题要注意的。比如上面使用@wraps(func)注解是很重要的,它能保留原始函数的元数据。

创建装饰器时保留函数元信息

任何时候你定义装饰器的时候,都应该使用functools库中的@wraps装饰器来注解底层包装函数。例如:

1
2
3
4
5
6
7
8
9
10
11
import time
from functools import wraps
def timethis(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

下面我们使用这个被包装后的函数并检查它的元信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> @timethis
... def countdown(n):
... '''
... Counts down
... '''
... while n > 0:
... n -= 1
...
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown.__name__
'countdown'
>>> countdown.__doc__
'\n\tCounts down\n\t'
>>> countdown.__annotations__
{'n': <class 'int'>}
>>>

在编写装饰器的时候复制元信息是一个非常重要的部分。如果你忘记了使用@wraps, 那么你会发现被装饰函数丢失了所有有用的信息。比如如果忽略@wraps后的效果是下面这样的:

1
2
3
4
5
6
>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}
>>>

@wraps有一个重要特征是它能让你通过属性__wrapped__直接访问被包装函数。例如:

1
2
>>> countdown.__wrapped__(100000)
>>>

解除一个装饰器

现在一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。假设装饰器是通过@wraps来实现的,那么你可以通过访问__wrapped__属性来访问原始函数:

1
2
3
4
5
6
7
8
>>> @somedecorator
>>> def add(x, y):
... return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>

直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。但是这里的方案仅仅适用于在包装器中正确使用了@wraps或者直接设置了__wrapped__属性的情况。

如果有多个包装器,那么访问__wrapped__属性的行为是不可预知的,应该避免这样做。在Python3.3中,它会略过所有的包装层,比如,假如你有如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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测试如下:

1
2
3
4
5
6
7
>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
5
>>>

Python3.4测试如下:

1
2
3
4
5
6
7
8
>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
Decorator 2
5
>>>

需要注意的是,并不是所有的装饰器都使用了@wraps,因此这里的方案并不全部使用,特别的,内置的装饰器@staticmethod@classmethod就没有遵循这个约定。

定义一个带参数的装饰器

我们用一个例子详细阐述下接受参数的处理过程。假设你想写一个装饰器,给函数添加日志功能,同时允许用户指定日志的级别和其他的选项。下面是这个装饰器的定义和使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import wraps
import logging

def logged(level, name=None, message=None):
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

# 使用示例
@logged(logging.DEBUG)
def add(x, y):
return x + y

@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')

初看这种实现看上去很复杂,但是核心思想很简单。最外层的函数logged()接受参数并将它们作用在内部的装饰器函数上面。内层的函数decorate()接受一个函数作为参数,然后在函数上面放置一个包装器。这里的关键点是包装器是可以使用传递给logged()的参数的。

可自定义属性的装饰器

如果想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为。可以引入一个访问函数,使用nonlocal来修改内部变量。然后这个访问函数被作为一个属性赋值给包装函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from functools import wraps, partial
import logging

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):
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_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

@logged(logging.DEBUG)
def add(x, y):
return x + y

@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')

下面是交互环境下的使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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来修改函数内部的变量。

带可选参数的装饰器

如果想写一个装饰器,既可以不传参数给它,比如@decorator, 也可以传递可选参数给它,比如@decorator(x,y,z)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

# Example use
@logged
def add(x, y):
return x + y

@logged(level=logging.CRITICAL, name='example')
def spam():
print('Spam!')

可以看到,@logged装饰器可以同时不带参数或带参数。

利用装饰器强制函数上的类型检查

这里的目标是能对函数参数类型进行断言,类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from inspect import signature
from functools import wraps

def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
if not __debug__:
return func

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)
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

可以看出这个装饰器非常灵活,既可以指定所有参数类型,也可以只指定部分。并且可以通过位置或关键字来指定参数类型。下面是使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> @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'>
>>>

将装饰器定义为类的一部分

如果想在类中定义装饰器,并将其作用在其他函数或方法上。首先要确认它的使用方式,比如到底是作为一个实例方法还是类方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

使用示例如下:

1
2
3
4
5
6
7
8
9
# 作为实例方法
a = A()
@a.decorator1
def spam():
pass
# 作为类方法
@A.decorator2
def grok():
pass

在类中定义装饰器初看上去好像很奇怪,但是在标准库中有很多这样的例子。 特别的,@property装饰器实际上是一个类,它里面定义了三个方法getter(),setter(),deleter(), 每一个方法都是一个装饰器。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person:
# Create a property instance
first_name = property()

@first_name.getter
def first_name(self):
return self._first_name

@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value

为什么要这么定义的主要原因是各种不同的装饰器方法会在关联的property实例上操作它的状态。 因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法。

在类中定义装饰器有个难理解的地方就是对于额外参数selfcls的正确使用。尽管最外层的装饰器函数比如decorator1()decorator2()需要提供一个selfcls参数,但是在两个装饰器内部被创建的wrapper()函数并不需要包含这个self参数。你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。

对于类里面定义的包装器还有一点比较难理解,就是在涉及到继承的时候。例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:

1
2
3
4
class B(A):
@A.decorator2
def bar(self):
pass

也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。不能使用@B.decorator2,因为在方法定义时,这个类B还没有被创建。

将装饰器定义为类

如果想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。需要让你的装饰器可以同时工作在类定义的内部和外部。
为了将装饰器定义成一个实例,需要确保它实现了__call__()__get__()方法。例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

可以将它当做一个普通的装饰器来使用,在类里面或外面都可以:

1
2
3
4
5
6
7
8
@Profiled
def add(x, y):
return x + y

class Spam:
@Profiled
def bar(self, x):
print(self, x)

在交互环境中的使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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

为类和静态方法提供装饰器

给类或静态方法提供装饰器的前提是要确保装饰器在@classmethod@staticmethod之前。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 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

装饰后的类和静态方法可正常工作,只不过增加了额外的计时功能:

1
2
3
4
5
6
7
8
9
10
11
>>> s = Spam()
>>> s.instance_method(1000000)
<__main__.Spam object at 0x1006a6050> 1000000
0.11817407608032227
>>> Spam.class_method(1000000)
<class '__main__.Spam'> 1000000
0.11334395408630371
>>> Spam.static_method(1000000)
1000000
0.11740279197692871
>>>

如果你把装饰器的顺序写错了就会出错。例如下面这样写:

1
2
3
4
5
6
7
class Spam:
@timethis
@staticmethod
def static_method(n):
print(n)
while n > 0:
n -= 1

调用这个静态方法就会报如下错误:

1
2
3
4
5
6
7
>>> Spam.static_method(1000000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "timethis.py", line 6, in wrapper
start = time.time()
TypeError: 'staticmethod' object is not callable
>>>

问题在于@classmethod@staticmethod实际上并不会创建可直接调用的对象,而是创建特殊的描述器对象。因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题。
当我们在抽象基类中定义类方法和静态方法时,例如想定义一个抽象类方法,可以使用类似下面的代码:

1
2
3
4
5
6
from abc import ABCMeta, abstractmethod
class A(metaclass=ABCMeta):
@classmethod
@abstractmethod
def method(cls):
pass

在这段代码中,@classmethod@abstractmethod两者的顺序是有讲究的,如果调换它们的顺序就会出错。

装饰器为被包装函数增加参数

如果想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则。可以使用关键字参数来给包装函数增加额外参数,考虑如下装饰器。

1
2
3
4
5
6
7
8
9
10
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

使用示例如下:

1
2
3
4
5
6
7
8
9
10
>>> @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
>>>

通过装饰器来给被包装函数增加参数的做法并不常见。尽管如此,有时候它可以避免一些重复代码。例如,如果有以下代码:

1
2
3
4
5
6
7
8
9
10
11
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')

可以将其重构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import wraps
import inspect

def optional_debug(func):
if 'debug' in inspect.getfullargspec(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参数的函数上时会有问题。这里增加了一步名字检查。

使用装饰器扩充类的功能

如果想通过反省或者重写类定义的某部分来修改它的行为,但是又不希望使用继承或元类的方式。可以使用类装饰器,例如下面是一个重写了特殊方法__getattribute__的类装饰器,可以打印日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def log_getattribute(cls):
orig_getattribute = cls.__getattribute__

def new_getattribute(self, name):
print('getting:', name)
return orig_getattribute(self, name)

cls.__getattribute__ = new_getattribute
return cls

@log_getattribute
class A:
def __init__(self,x):
self.x = x
def spam(self):
pass

使用示例如下:

1
2
3
4
5
6
7
>>> a = A(42)
>>> a.x
getting: x
42
>>> a.spam()
getting: spam
>>>

使用元类控制实例的创建

如果定义了一个类,就能像寒暑易用的调用它来创建实例,例如:

1
2
3
4
5
6
class Spam:
def __init__(self, name):
self.name = name

a = Spam('warry')
b = Spam('elssm')

如果想自定义这个步骤,可以定义一个元类并自己实现__call__()方法,如果不希望任何人创建这个类的实例:

1
2
3
4
5
6
7
8
9
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')

这样的话,用户只能调用这个类的静态方法,而不能使用通常的方法来创建它的实例。例如:

1
2
3
4
5
6
7
8
9
>>> 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
>>>

实现单例模式(只能创建唯一实例的类)的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

class Spam(metaclass=Singleton):
def __init__(self):
print('Creating Spam')

这样Spam类就只能创建唯一的实例了,示例如下:

1
2
3
4
5
6
7
8
9
>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True
>>>

如果想创建缓存实例,可以通过元类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

class Spam(metaclass=Cached):
def __init__(self, name):
print('Creating Spam({!r})'.format(name))
self.name = name

测试如下:

1
2
3
4
5
6
7
8
9
10
>>> a = Spam('Guido')
Creating Spam('Guido')
>>> b = Spam('Diana')
Creating Spam('Diana')
>>> c = Spam('Guido') # 缓存
>>> a is b
False
>>> a is c # 返回的是缓存的值
True
>>>

args和*kwargs的强制参数签名

对任何涉及到操作函数调用签名的问题,都应该使用inspect模块中的签名特性。我们最主要关注两个类:SignatureParameter。下面是一个创建函数前面的交互例子:

1
2
3
4
5
6
7
8
9
>>> 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)
>>>

一旦有了一个签名对象,你就可以使用它的bind()方法很容易的将它绑定到*args**kwargs上去。下面是一个简单的演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
>>> def func(*args, **kwargs):
... bound_values = sig.bind(*args, **kwargs)
... for name, value in bound_values.arguments.items():
... print(name,value)
...
>>> # Try various examples
>>> func(1, 2, z=3)
x 1
y 2
z 3
>>> func(1)
x 1
>>> func(1, z=3)
x 1
z 3
>>> func(y=2, x=1)
x 1
y 2
>>> func(1, 2, 3, 4)
Traceback (most recent call last):
...
File "/usr/local/lib/python3.3/inspect.py", line 1972, in _bind
raise TypeError('too many positional arguments')
TypeError: too many positional arguments
>>> func(y=2)
Traceback (most recent call last):
...
File "/usr/local/lib/python3.3/inspect.py", line 1961, in _bind
raise TypeError(msg) from None
TypeError: 'x' parameter lacking default value
>>> func(1, y=2, x=3)
Traceback (most recent call last):
...
File "/usr/local/lib/python3.3/inspect.py", line 1985, in _bind
'{arg!r}'.format(arg=param.name))
TypeError: multiple values for argument 'x'
>>>

可以看出来,通过将签名和传递的参数绑定起来,可以强制函数调用遵循特定的规则,比如必填、默认、重复等等。
下面是一个强制函数签名更具体的例子。在代码中,我们在基类中先定义了一个非常通用的__init__()方法,然后强制所有的子类必须提供一个特定的参数签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from inspect import Signature, Parameter

def make_sig(*names):
parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
for name in names]
return Signature(parms)

class Structure:
__signature__ = make_sig()
def __init__(self, *args, **kwargs):
bound_values = self.__signature__.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
setattr(self, name, value)

# Example use
class Stock(Structure):
__signature__ = make_sig('name', 'shares', 'price')

class Point(Structure):
__signature__ = make_sig('x', 'y')

下面是使用这个Stock类的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import inspect
>>> print(inspect.signature(Stock))
(name, shares, price)
>>> s1 = Stock('ACME', 100, 490.1)
>>> s2 = Stock('ACME', 100)
Traceback (most recent call last):
...
TypeError: 'price' parameter lacking default value
>>> s3 = Stock('ACME', 100, 490.1, shares=50)
Traceback (most recent call last):
...
TypeError: multiple values for argument 'shares'
>>>

在类上强制使用编程规约

如果想监控类的定义,通常可以通过定义一个元类。一个基本元类通常是继承自type并重定义它的__new__()方法 或者是__init__()方法。比如:

1
2
3
class MyMeta(type):
def __new__(self, clsname, bases, clsdict):
return super().__new__(cls, clsname, bases, clsdict)

另一种是,定义__init__()方法:

1
2
3
class MyMeta(type):
def __init__(self, clsname, bases, clsdict):
super().__init__(clsname, bases, clsdict)

为了使用这个元类,通常要将它放到到一个顶级父类定义中,然后其他的类继承这个顶级父类。示例如下:

1
2
3
4
5
6
7
8
class Root(metaclass=MyMeta):
pass

class A(Root):
pass

class B(Root):
pass

元类的一个关键特点是它允许你在定义的时候检查类的内容。在重新定义__init__()方法中,你可以很轻松的检查类字典、父类等等。并且,一旦某个元类被指定给了某个类,那么就会被继承到所有子类中去。因此,一个框架的构建者就能在大型的继承体系中通过给一个顶级父类指定一个元类去捕获所有下面子类的定义。

在元类中选择重新定义__new__()方法还是__init__()方法取决于你想怎样使用结果类。__new__()方法在类创建之前被调用,通常用于通过某种方式修改类的定义。 而__init__()方法是在类被创建之后被调用,当你需要完整构建类对象的时候会很有用。 在最后一个例子中,这是必要的,因为它使用了super()函数来搜索之前的定义。它只能在类的实例被创建之后,并且相应的方法解析顺序也已经被设置。

以编程方式定义类

可以使用函数types.new_class()来初始化新的类对象。 你需要做的只是提供类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price

cls_dict = {
'__init__' : __init__,
'cost' : cost,
}

import types

Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))
Stock.__module__ = __name__

这种方式会构建一个普通的类对象,示例如下:

1
2
3
4
5
6
>>> s = Stock('ACME', 50, 91.1)
>>> s
<stock.Stock object at 0x1006a9b10>
>>> s.cost()
4555.0
>>>

这种方法中,一个比较难理解的地方是在调用完types.new_class()Stock.__module__的赋值。每次当一个类被定义后,它的__module__属性包含定义它的模块名。这个名字用于生成__repr__()方法的输出。它同样也被用于很多库,比如pickle。因此,为了让你创建的类是“正确”的,你需要确保这个属性也设置正确了。

如果你想创建的类需要一个不同的元类,可以通过types.new_class()第三个参数传递给它。例如:

1
2
3
4
5
6
7
8
9
10
>>> import abc
>>> Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta},
... lambda ns: ns.update(cls_dict))
...
>>> Stock.__module__ = __name__
>>> Stock
<class '__main__.Stock'>
>>> type(Stock)
<class 'abc.ABCMeta'>
>>>

第三个参数还可以包含其他的关键字参数。比如,一个类的定义如下:

1
2
class Spam(Base, debug=True, typecheck=False):
pass

那么可以将其翻译成如下的new_class()调用形式:

1
2
3
Spam = types.new_class('Spam', (Base,),
{'debug': True, 'typecheck': False},
lambda ns: ns.update(cls_dict))

new_class()第四个参数最神秘,它是一个用来接受类命名空间的映射对象的函数。通常这是一个普通的字典,但是它实际上是__prepare__()方法返回的任意对象,这个函数需要使用update()方法给命名空间增加内容。

在定义的时候初始化类的成员

如果想在类被定义的时候就初始化一部分类的成员,而不是要等到实例被创建后。下面是一个例子,利用这个思路来创建类似于collections模块中的命名元组的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import operator

class StructTupleMeta(type):
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
for n, name in enumerate(cls._fields):
setattr(cls, name, property(operator.itemgetter(n)))

class StructTuple(tuple, metaclass=StructTupleMeta):
_fields = []
def __new__(cls, *args):
if len(args) != len(cls._fields):
raise ValueError('{} arguments required'.format(len(cls._fields)))
return super().__new__(cls,args)

这段代码可以用来定义简单的基于元组的数据结构,如下所示:

1
2
3
4
5
class Stock(StructTuple):
_fields = ['name', 'shares', 'price']

class Point(StructTuple):
_fields = ['x', 'y']

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> s = Stock('ACME', 50, 91.1)
>>> s
('ACME', 50, 91.1)
>>> s[0]
'ACME'
>>> s.name
'ACME'
>>> s.shares * s.price
4555.0
>>> s.shares = 23
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>

这一小节中,类StructTupleMeta获取到类属性_fields中的属性名字列表,然后将它们转换成相应的可访问特定元组槽的方法。函数operator.itemgetter()创建一个访问器函数, 然后property()函数将其转换成一个属性。

比较难懂的部分是知道不同的初始化步骤是什么时候发生的。StructTupleMeta中的__init__()方法只在每个类被定义时被调用一次。cls参数就是那个被定义的类。实际上,上述代码使用了_fields类变量来保存新的被定义的类,然后给它再添加一点新的东西。

StructTuple类作为一个普通的基类,供其他使用者来继承。这个类中的__new__()方法用来构造新的实例。这里使用__new__()并不是很常见,主要是因为我们要修改元组的调用签名,使得我们可以像普通的实例调用那样创建实例。就像下面这样:

1
2
s = Stock('ACME', 50, 91.1) # OK
s = Stock(('ACME', 50, 91.1)) # Error

__init__()不同的是,__new__()方法在实例被创建之前被触发。由于元组是不可修改的,所以一旦它们被创建了就不可能对它做任何改变。而__init__()会在实例创建的最后被触发,这也是为什么__new__()方法已经被定义了。

避免重复的属性方法

考虑下一个简单的类,它的属性由属性方法包装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person:
def __init__(self, name ,age):
self.name = name
self.age = age

@property
def name(self):
return self._name

@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('name must be a string')
self._name = value

@property
def age(self):
return self._age

@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError('age must be an int')
self._age = value

可以看到,为了实现属性值的类型检查我们写了很多的重复代码。只要你以后看到类似这样的代码,你都应该想办法去简化它。一个可行的方法是创建一个函数用来定义属性并返回它。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def typed_property(name, expected_type):
storage_name = '_' + name

@property
def prop(self):
return getattr(self, storage_name)

@prop.setter
def prop(self, value):
if not isinstance(value, expected_type):
raise TypeError('{} must be a {}'.format(name, expected_type))
setattr(self, storage_name, value)

return prop

# Example use
class Person:
name = typed_property('name', str)
age = typed_property('age', int)

def __init__(self, name, age):
self.name = name
self.age = age

本节我们演示内部函数或者闭包的一个重要特性,它们很像一个宏。例子中的函数typed_property()看上去有点难理解,其实它所做的仅仅就是为你生成属性并返回这个属性对象。因此,当在一个类中使用它的时候,效果跟将它里面的代码放到类定义中去是一样的。尽管属性的gettersetter方法访问了本地变量如name,expected_type以及storate_name,这些变量的值会保存在闭包当中。

我们还可以使用functools.partial()像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import partial

String = partial(typed_property, expected_type=str)
Integer = partial(typed_property, expected_type=int)

# Example:
class Person:
name = String('name')
age = Integer('age')

def __init__(self, name, age):
self.name = name
self.age = age

定义上下文管理器

实现一个新的上下文管理器的最简单的方法就是使用contexlib模块中的@contextmanager装饰器。下面是一个实现了代码块计时功能的上下文管理器例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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__()方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import time

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))

@contextmanager应该仅仅用来写自包含的上下文管理函数。如果你有一些对象(比如一个文件、网络连接或锁),需要支持with语句,那么你就需要单独实现 __enter__()方法和__exit__()方法。