YON Blog

Pyhon decorator descriptor and metaclass

backtrader 支持在线实时的数据源输入, 目前支持海外的 IbPy (盈透), Oanda 等方式的接入, 不支持我准备用的富途, 于是准备看源码参照 IbPy, Oanda 接入的方式, 自己对接下富途. 看了下源码, 发现用到了 Python 的几个特性: 装饰器, 描述符, 元类. 以前简单接触过, 理解不是太深, 所以这次就稍微整理下.

装饰器 decorator

Decorator 还是比较好理解的, 它本质上来说就是个函数 (严格来说只要是 callable 的就行), 如果你把它放在方法上, 那么它的入参就是这个方法, 返回的新方法就会替代原本的方法定义. 如果放在类上, 那么它的入参就是这个类, 返回的新类会替代原本的类定义.

Decorator 有两种形式, 一种就是直接的 @decorator 形式, 这个就是我上面说的. 另一种是 @decorator(arg1, arg2, ...) 这种用法其实是前一种的变体, decorator(arg1, arg2, ...) 就是个简单的方法调用, Python 会把它的返回值当成真正的装饰器来解析.

描述符 descriptor

Descriptor 的定义很简单, 只要是一个类定义了下面几个方法, 那么它就是一个描述符:

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

如果一个描述符只定义了上述方法中的 __get__ 方法, 那么它就是一个 non-data 的描述符, 如果除了 __get__ 以外还定义了其它的方法 ( __set__ 或者 __delete__ ), 那么就是一个 data 的描述符. data 与 non-data 的描述符有什么区别? 要回答这个问题, 我们要先明白, 描述符的作用是什么. 首先描述符只有被定义为类变量 (class variable [1]) 时才有意义, 当你使用描述符时, Python 会调用相应的描述符的方法而不是直接操作对应的描述符. 可以理解为描述符提供了一种对类变量的 get ( Clz.descr ), set ( Clz.descr = x ), delete ( del Clz.descr ) 进行重载的机制. 不过这边有点不符合直觉的是, 一般我们重载方法的时候是重载的主语, 所以按道理来说我们要重载 Clz.descr 这个表达式的话, 应该是在 Clz 中定义 __get__ 来重载, 但是描述符的重载是在宾语上发生的, 我们是在 descr 上定义的 __get__ 方法. 不过这个也好理解, 在 descr 定义就不用在每个用到描述符的地方都重复定义一遍了.

下面来回答 data 与 non-data 的描述符的区别. 当我们使用 obj.descr  表达式时, 如果对象中找不到 descr  Python 就会找到 Clz.descr  然后走描述符那套处理逻辑, 那么对象中有同名的 descr  时会怎么样呢? 这时候就要看 descr  是 data 还是 non-data 了. 如果是 data 的, 那么 Clz.descr  的优先级就高于 obj.descr . 如果是 non-data 的, 那么 obj.descr  的优先级就高于 Clz.descr  不会走描述符那套逻辑. 具体解析顺序的实现在 object.__getattribute__()  中, 下面是用 Python 描述的实现逻辑:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

常见的 @staticmethod 与 @classmethod 就是将装饰器与描述符结合起来的一种应用. 下面是对应的 Python 的实现版本:

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f
    
class MethodType:
    "Emulate Py_MethodType in Objects/classobject.c"

    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)
    
class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            # 这段逻辑是 Python 3.9 新增的, 用于实现描述符的链式调用 [1]
            # 原文档里上面的 if 判断条件是 hasattr(obj, '__get__'), 这个是错误的
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

元类 metaclass

元类算是这几个概念里比较难理解的了, 不过只要我们记住在 Python 里任何东西都是对象这个知识点, 那么理解元类就比较简单了. 任何东西都是对象, 那么类自然也不例外. 类的类被称为元类, 一般情况下, 我们通过 class 定义的类的元类是 type , type 的元类是 type 自身. 我们通过 type(Clz) 表达式来查看一个类的元类是什么.

在 Python2 中要指定一个类的元类通过 __metaclass__ 这个类变量来指定, 要注意的是, 只有当我们使用 class 关键字来定义一个类的时候, __metaclass__ 才生效 [2]. Python3 中使用下面的语句来指定一个类的元类:

class Clz(metaclass=type):
    pass

哪些对象能作为元类的? 答案是任何 callable 的对象. 当你指定了元类, Python 在创建类的时候会调用你指定的元类, 并传入以下参数:

  1. name: 当前类的名字
  2. bases: 当前类的基类
  3. attrs: 当前类的属性


一个类的默认的元类是 type , 所以当你使用 class 关键字来定义类时, 等价于下面两条语句:

class Clz(object):
    hello = 'world'

# 等价
Clz = type('Clz', (object,), {'hello': 'world'})

# 等价
Clz = type.__new__(type, 'Clz', (object,), {'hello': 'world'})

上面两种写法有什么区别呢? 只要了解了 __init__ 和 __new__ 的区别就明白了. __init__ 我们都知道, 当我们执行 Clz(arg1, arg2) 创建对象时, Python 会执行 Clz.__init__(self, arg1, args) 其中 self 表示当前正在创建的对象. 很自然的一个问题就是, self 哪里来的呢? self 就是通过 __new__ 创建出来的. 总结下就是, __new__ 创建对象, __init__ 初始化对象, 所以当我们执行 Clz(arg1, arg2) 时等价于执行下面的语句:

# Clz(arg1, arg2) 等价于
obj = Clz.__new__(Clz, arg1, arg2)
obj.__init__(arg1, arg2)

顺便再多说几句, 官方文档里提到 [3]:

If new() does not return an instance of cls, then the new instance’s init() method will not be invoked.


也就是说, 如果我们重写 __new__  方法, 但是 __new__  返回的对象的类不是第一个参数指定的类,
那么新创建的对象的 __init__  方法不会被调用:

class Foo:
    def __new__(cls, *args, **kwargs):
        print('Foo new')
        return Bar.__new__(Bar, *args, **kwargs)


class Bar:
    def __init__(self):
        print('Bar init')

# 不会调用到 Bar 的 __init__ 方法
Foo()


说了这么多的废话, 好像和元类一点关系都没有. 其实不是的, 我们看下 type 的 __new__ 方法的定义, 第一个参数表示要创建的对象的类, 而我们现在创建的对象就是一个类, 类的类可不就是我们所说的元类吗? 需要说明的是, 如果使用 type 的 __new__ 方法来创建类并指定元类, 那么就仅仅是指定了新创建类的元类, 新类并不是通过 call 元类来创建的.

最后我们说下元类的继承问题, 一句话, 一个类的元类如果没有指定的话会从父类继承. 虽然说任何 callable 的对象都可以当做元类, 但是如果我们想要写个可以被继承的元类就需要用 type.__new__ 来创建类:

class MetaClz(type):
    def __new__(mcs, *args, **kwargs):
        return type.__new__(mcs, *args, **kwargs)

class Foo(metaclass=MetaClz):
    pass

class Bar(Foo):
    pass

MetaClz 的 __new__ 方法中创建了一个类, 并且指定了这个类的元类, 所以最终创建出来的 Foo 的元类是 MetaClz , Bar 会继承 Foo 的元类 MetaClz , 所以 Bar 的元类也是 MetaClz . 我们再看另外一种错误的写法:

class MetaClz(type):
    def __new__(mcs, *args, **kwargs):
        return type(*args, **kwargs)

class Foo(metaclass=MetaClz):
    pass

class Bar(Foo):
    pass

和之前的写法唯一的区别就是我们直接用了 type() 来创建类, 这样写的话, 最终创建出来的 Foo 的元类是 type , Bar 会继承 Foo 的元类 type , 所以 Bar 的元类也是 type . 那么谁的元类是 MetaClz 呢? 答案是下面这个语句的元类是 MetaClz :

# 这条语句的元类是 MetaClz
class Foo(metaclass=MetaClz):
    pass


[1] Descriptor HowTo Guide
[2] Inheritance of metaclass
[3] Data model