Python Cookbook —— 元编程

一、函数装饰器

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
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)
elapsed = time.time() - start
print(func.__name__, elapsed)
return result
return wrapper


@timethis
def countdown(n):
while n > 0:
n -= 1


countdown(1000000)
# => countdown 0.29901695251464844

装饰器负责接收某个函数作为参数,然后返回一个新的函数作为输出。下面的代码:

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

实际上等同于

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

装饰器内部通常要定义一个接收任意参数(*args, **kwargs)的函数,即 wrapper()。在 wrapper 函数里,调用原始的作为参数传入的函数(func)并获取其结果,再根据需求添加上执行其他操作的代码(比如计时、日志等)。最后新创建的 wrapper 函数被返回并替换掉被装饰的函数(countdown),从而在不改变被装饰函数自身代码的情况下,为其添加额外的行为。

二、带参数的装饰器

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
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.
'''
logging.basicConfig(
level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

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.WARNING)
def spam():
pass


@logged(logging.INFO, name='Example', message='This is log message')
def foo():
pass


spam()
foo()
# => 2019-10-24 09:22:25,780 - __main__ - WARNING - spam
# => 2019-10-24 09:22:25,783 - Example - INFO - This is log message

最外层的函数 logged() 用于接收传入装饰器的参数,并使这些参数能够被装饰器中的内部函数(decorate())访问。内部函数 decorate 则用于实现装饰器的“核心逻辑”,即接收某个函数作为参数,通过定义一个新的内部函数(wrapper)添加某些行为,再将这个新的函数返回作为被装饰函数的替代品。

在类中定义的装饰器

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

spam()
# => Decorator 1

# As a class method
@A.decorator2
def grok():
pass

grok()
# => Decorator 2

利用装饰器向原函数中添加参数

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 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 add(x, y):
print(x + y)

add(2, 3)
# => 5

add(2, 3, debug=True)
# => Calling add
# => 5

装饰器修改类的定义

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

a = A(42)
print(a.x)
a.spam()

# => getting: x
# => 42
# => getting: spam

类装饰器可以用来重写类的部分定义以修改其行为,作为一种直观的类继承或元类的替代方式。
比如上述功能也可以通过类继承来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LoggedGetattribute:
def __getattribute__(self, name):
print('getting: ', name)
return super().__getattribute__(name)


class A(LoggedGetattribute):
def __init__(self, x):
self.x = x

def spam(self):
pass


a = A(42)
print(a.x)
a.spam()

在某些情况下,类装饰器的方案要更为直观一些,并不会向继承层级中引入新的依赖。同时由于不使用 super() 函数,速度也稍快一点。

使用元类控制实例的创建

Python 中的类可以像函数那样调用,同时创建实例对象:

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


a = Spam('Guido')
b = Spam('Diana')

如果开发人员想要自定义创建实例的行为,可以通过元类重新实现一遍 __call__() 方法。假设在调用类时不创建任何实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 class NoInstance(type):
def __call__(self, *args, **kwargs):
raise TypeError("Can't instantiate directly")


class Spam(metaclass=NoInstance):
@staticmethod
def grok(x):
print('Spam.grok')


Spam.grok(42) # Spam.grok
s = Spam()
# TypeError: Can't instantiate directly

元类实现单例模式
单例模式即类在创建对象时,单一的类确保只生成唯一的实例对象。

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

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

强制检查类定义中的代码规范

可以借助元类监控普通类的定义代码。通常的方式是定义一个继承自 type 的元类并重写其 __new__()__init__() 方法。

1
2
3
4
5
6
class MyMeta(type):
def __new__(cls, clsname, bases, clsdict):
# clsname is name of class being defined
# bases is tuple of base classes
# clsdict is class dictionary
return super().__new__(cls, clsname, bases, clsdict)

1
2
3
4
5
6
class MyMeta(type):
def __init__(self, clsname, bases, clsdict):
# clsname is name of class being defined
# bases is tuple of base classes
# clsdict is class dictionary
return 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__() 方法内部,可以方便地检查 class dictionary、base class 或者其他与类定义相关的内容。此外,当元类指定给某个普通类以后,该普通类的所有子类也都会继承元类的定义。

下面是一个用于检查代码规范的元类,确保方法的命名里只包含小写字母:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class NoMixedCaseMeta(type):
def __new__(cls, clsname, bases, clsdict):
for name in clsdict:
if name.lower() != name:
raise TypeError('Bad attribute name: ' + name)
return super().__new__(cls, clsname, bases, clsdict)


class Root(metaclass=NoMixedCaseMeta):
pass


class A(Root):
def foo_bar(self):
pass


class B(Root):
def fooBar(self):
pass
# TypeError: Bad attribute name: fooBar

元类的定义中重写 __new__() 还是 __init__() 方法取决于你想以何种方式产出类。__new__() 方法生效于类创建之前,通常用于对类的定义进行改动(通过修改 class dictionary 的内容);__init__() 方法生效于类创建之后,通常是与已经生成的类对象进行交互。比如 super() 函数只在类实例被创建后才能起作用。

以编程的方式定义类

可以通过编程的方式创建类,比如从字符串中产出类的源代码。
types.new_class() 函数可以用来初始化新的类对象,只需要向其提供类名、父类(以元组的形式)、关键字参数和一个用来更新 class dictionary 的回调函数。

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
# Methods
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,
}

# Make a class
import types

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


s = Stock('ACME', 50, 91.1)
print(s)
# => <__main__.Stock object at 0x7f0e3b62edc0>
print(s.cost())
# => 4555.0

通常形式的类定义代码:

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

转换成对应的 type.new_class() 形式的代码:

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

从代码中产出类对象在某些场景下是很有用的,比如 collections.nametupe() 函数:

1
2
3
4
>>> import collections
>>> Stock = collections.namedtuple('Stock', ['name', 'shares', 'price'])
>>> Stock
<class '__main__.Stock'>

下面是一个类似 namedtuple 功能的实现代码:

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
import operator
import types
import sys

def named_tuple(classname, fieldnames):
# Populate a dictionary of field property accessors
cls_dict = { name: property(operator.itemgetter(n))
for n, name in enumerate(fieldnames) }

# Make a __new__ function and add to the class dict
def __new__(cls, *args):
if len(args) != len(fieldnames):
raise TypeError('Expected {} arguments'.format(len(fieldnames)))
return tuple.__new__(cls, args)

cls_dict['__new__'] = __new__

# Make the class
cls = types.new_class(classname, (tuple,), {},
lambda ns: ns.update(cls_dict))

cls.__module__ = sys._getframe(1).f_globals['__name__']
return cls


Point = named_tuple('Point', ['x', 'y'])
print(Point)
# => <class '__main__.Point'>
p = Point(4, 5)
print(p.x)
# => 4
print(p.y)
# => 5
p.x = 2
# => AttributeError: can't set attribute

在定义时初始化类成员

在类定义时完成初始化或其他设置动作,是元类的经典用法(元类在类定义时触发)。

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


class Stock(StructTuple):
_fields = ['name', 'shares', 'price']


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


s = Stock('ACME', 50, 91.1)
print(s)
# => ('ACME', 50, 91.1)
print(s[0])
# => ACME
print(s.name)
# => ACME
s.shares = 23
# => AttributeError: can't set attribute

在上面的代码中,StructTupleMeta 元类从 _fields 类属性中读取属性名列表并将其转换成属性方法。operator.itemgetter() 函数负责创建访问方法(accessor function),property() 函数负责将它们转换成属性(property)。

StructTuple 类用作供其他类继承的基类。其中的 __new__() 方法负责创建新的实例对象。不同于 __init__()__new__() 方法会在实例创建之前触发,由于 tuple 是不可变对象,创建之后即无法被修改,因此这里使用 __new__()

参考资料

Python Cookbook, 3rd Edition