Python 多重继承时metaclass conflict问题解决与原理探究

背景最近有一个需求需要自定义一个多继承abc.ABC与django.contrib.admin.ModelAdmin两个父类的抽象子类,方便不同模块复用大部分代码,同时强制必须实现所有抽象方法,没想按想当然的写法实现多继承时,居然报错metaclass conflict:
In [1]: import abcIn [2]: from django.contrib import adminIn [3]: class MyAdmin(abc.ABC, admin.ModelAdmin):...:pass...:---------------------------------------------------------------------------TypeErrorTraceback (most recent call last)<ipython-input-3-b159bc04ec1b> in <module>----> 1 class MyAdmin(abc.ABC, admin.ModelAdmin):2pass3TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases一时之间疑惑满满,先是通过搜索快速找到了一个解决方案,但是却并没有弄明白问题的根本原因与解决方案的原理,最近终于有些时间可以深入探究一番,这里记录一下 。PS: 本文所有讨论均基于Python3,不考虑Python2的部分差异之处 。
什么是metaclass(元类)首先要弄清楚什么是metaclass,才可能明白metaclass conflict的真正含义 。
类比普通class与metaclass这里采用class(类)和instance(实例)的关系来类比解释,如果要创建一个自定义class A,然后创建其实例,一般我们会这么写:
In [1]: class A:...:def test(self):...:print('call test')In [2]: a = A()In [3]: print(a, type(a))<__main__.A object at 0x7f9f95414970> <class '__main__.A'>如上我们自定义了class A,并且生成了class A的实例对象a,print语句的输出可以看出实例a的类型正是class A,此时如果我们进一步探究A的类型会发现:
In [10]: print(type(A))<class 'type'>A类型是 class type 。我们会说a是class A的实例,那以此类推可以说class A是class type的实例,或者换一种说法:class A的实例是a,class type的实例是A 。现在我们尝试定义metaclass:在python中class不仅能创建实例对象,其本身也是一个对象,普通class创建实例普通对象,metaclass(元类)则创建实例class对象 。PS: 严格来说metaclass本身不一定要是一个class,它可以是任意可以返回class的callable对象,这里我们不做深入探讨 。
自定义与使用metaclass在python中应该怎么定义一个metaclass呢,其实type就是一个metaclass,type是所有class的默认metaclass,而且所有自定义的metaclass 最终也都会使用到type来执行最后创建class的工作 。事实上上面使用class A... 的语法定义类A时,Python解释器最终也是调用type来创建的class A,其等价于以下代码:
In [23]: def fn(self):...:print('call test')...:In [24]: A = type('A', (object, ), dict(test=fn))type创建class的签名如下:
type(name, bases, attrs)name: 要创建的class名称bases: 要继承的父类tuple(可以为空,但python3自定义class一般都默认继承object)attrs: 包含class定义属性名称和值的dict绝大多数情况下我们并不需要用到metaclass,极少数需要动态创建/修改class的复杂场景比如Django的ORM才需要用到这一技术 。这里举一个metaclass简单使用示例,比如我们可以简单创建一个给class统一加上其创建时间的metaclass,以满足需要时可以查看对应class首次创建时间的这个伪需求(仅为举本例而提的需求_),如下AddCTimeMetaclass定义:
In [30]: from datetime import datetimeIn [31]: class AddCTimeMetaclass(type):...:def __new__(cls, name, bases, attrs):...:attrs['ctime'] = datetime.now()...:return super().__new__(cls, name, bases, attrs)...:In [32]: class B(metaclass=AddCTimeMetaclass):...:pass...:In [33]: B.ctimeOut[33]: datetime.datetime(2022, 10, 29, 1, 22, 46, 750176)在定义class B的时候,通过指定metaclass参数告诉解释器创建class B时不使用默认的type而是使用自定义的元类AddCTimeMetaclass 。

经验总结扩展阅读