美文网首页Python
Python Cookbook —— 元编程

Python Cookbook —— 元编程

作者: rollingstarky | 来源:发表于2020-11-19 22:01 被阅读0次

    一、函数装饰器

    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

    相关文章

      网友评论

        本文标题:Python Cookbook —— 元编程

        本文链接:https://www.haomeiwen.com/subject/nwpqvctx.html