Python

Python Cookbook —— 元编程

2020-11-19  本文已影响0人  rollingstarky

一、函数装饰器

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

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

@timethis
def countdown(n):
    ...

实际上等同于

def countdown(n):
    ...
countdown = timethis(countdown)

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

二、带参数的装饰器

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)添加某些行为,再将这个新的函数返回作为被装饰函数的替代品。

在类中定义的装饰器

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

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

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

装饰器修改类的定义

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

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

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 中的类可以像函数那样调用,同时创建实例对象:

class Spam:
    def __init__(self, name):
        self.name = name


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

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

 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

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

# 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')
>>> from singleton import *
>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True

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

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

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

为了使用元类,通常会先定义一个供其他对象继承的基类:

class Root(metaclass=MyMeta):
    pass

class A(Root):
    pass

class B(Root):
    pass

元类的重要特性在于,它允许用户在类定义时检查类的内容。在重写的 __init__() 方法内部,可以方便地检查 class dictionary、base class 或者其他与类定义相关的内容。此外,当元类指定给某个普通类以后,该普通类的所有子类也都会继承元类的定义。

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

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 的回调函数。

# 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

通常形式的类定义代码:

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

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

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

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

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

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

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

在定义时初始化类成员

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

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

上一篇下一篇

猜你喜欢

热点阅读