Pyhon decorator descriptor and metaclass
26 Dec 2020backtrader 支持在线实时的数据源输入, 目前支持海外的 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 在创建类的时候会调用你指定的元类, 并传入以下参数:
- name: 当前类的名字
- bases: 当前类的基类
- 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