7

Python Cookbook —— 元编程

 3 years ago
source link: https://rollingstarky.github.io/2020/11/19/python-cookbook-metaprogramming/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python Cookbook —— 元编程

2020-11-19

| Python

|

|

10k

|

0:10

一、函数装饰器

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK