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

python,多重,继承,metaclass,conflict,问题,解决,原理,探究 · 浏览次数 : 1370

小编点评

**metaclass Conflict** **问题:** 在多继承过程中,子类如何选择其父类的metaclass呢?如果子类同时继承了多个父类的metaclass,该问题会发生什么错误呢? **解决方案:** 为了解决metaclass冲突,子类可以选择以下三种方法: 1. **定义其自身的 metaclass。** 2. **使用多个父类的共同 metaclass。** 3. **使用多继承的嵌套设计。** **示例:** ```python class M0(type): pass class M1(type): pass class A(M0, M1): pass class B(M0, M1): pass class C(A, B): pass ``` 在这个例子中,`C`既是`A`又是`B`的子类,因此它需要定义自己的 metaclass才能满足多继承的要求。 **结论:** 在多继承过程中,子类可以选择使用不同的方法来选择其父类的 metaclass。如果子类同时继承了多个父类的metaclass,需要选择最合适的方法才能保持兼容性。

正文

背景

最近有一个需求需要自定义一个多继承abc.ABC与django.contrib.admin.ModelAdmin两个父类的抽象子类,方便不同模块复用大部分代码,同时强制必须实现所有抽象方法,没想按想当然的写法实现多继承时,居然报错metaclass conflict:

In [1]: import abc
In [2]: from django.contrib import admin
In [3]: class MyAdmin(abc.ABC, admin.ModelAdmin):
   ...:     pass
   ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-b159bc04ec1b> in <module>
----> 1 class MyAdmin(abc.ABC, admin.ModelAdmin):
      2     pass
      3
TypeError: 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 datetime
In [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.ctime
Out[33]: datetime.datetime(2022, 10, 29, 1, 22, 46, 750176)

在定义class B的时候,通过指定metaclass参数告诉解释器创建class B时不使用默认的type而是使用自定义的元类AddCTimeMetaclass。

metaclass confict(元类冲突)的清晰含义

初步定义了metaclass并了解简单使用之后,我们开始正式探究metaclass conflict,一个最简单触发metaclass conflict的例子如下:

In [42]: class M0(type):
    ...:     pass
    ...:
In [43]: class M1(type):
    ...:     pass
    ...:
In [44]: class A(metaclass=M0):
    ...:     pass
    ...:
In [45]: class B(metaclass=M1):
    ...:     pass
    ...:
In [46]: class C(A, B):
    ...:     pass
    ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-46-9900d594feda> in <module>
----> 1 class C(A, B):
      2     pass
      3

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

如上M0与M1为自定义metaclass,分别作为A、B的metaclass,当class C试图多继承A、B时就会出问题,从字面意思理解:子类的metaclass必须是其所有基类metaclass的(非严格)子类,看起来普通class的多继承和metaclass的多继承之间发生了什么问题。
这段话具体怎么理解?我们已经知道A、B都分别具有自己的metaclass M0、M1,那么当C多继承A、B的时候C的metaclass应该是M0还是M1呢?由于M0、M1两者之间并没有继承关系,用哪个都不行,Python不知道怎么办,只能告诉你出问题了。

解决方案

那理想情况下C的metaclass到底应该是什么呢?理想情况应该如下所示:

M0     M1
 : \   / :
 :  \ /  :
 A  M2  B
  \  :  /
   \ : /
     C

即采用多继承了M0、M1的M2作为C的metaclass,这也是解决这个问题的最终方案,具体代码如下:

In [58]: class M2(M0, M1):
    ...:     pass
    ...:
In [59]: class C(A, B, metaclass=M2):
    ...:     pass
    ...:

如上我们通过手动定义M2,并手动明确指定class C的metaclass为M2,如此解决metaclass conflict问题。
这时再回到开头碰到的多继承abc.ABC与admin.ModelAdmin时遇到的问题就很容易理解了:因为abc.ABC有自己的metaclass abc.ABCMeta,同时modelAdmin也有自己的metaclass django.forms.widgets.MediaDefiningClass,并且这两者之间没有继承关系,因而 class MyAdmin(abc.ABC, admin.ModelAdmin) 多继承时解释器无法推断出满足条件的metaclass,自然也就报错了,解决办法和上面的方案一样,定义一个两者metaclass的子类并将其指定为MyAdmin的metaclass即可,代码如下:

In [112]: print(type(abc.ABC), type(admin.ModelAdmin))
<class 'abc.ABCMeta'> <class 'django.forms.widgets.MediaDefiningClass'>
In [113]: class MyMeta(type(abc.ABC), type(admin.ModelAdmin)):
     ...:     pass
     ...:
In [114]: class MyAdmin(abc.ABC, admin.ModelAdmin, metaclass=MyMeta):
     ...:     pass
     ...:
In [115]: print(type(MyAdmin))
<class '__main__.MyMeta'>

转载请注明出处,原文地址:https://www.cnblogs.com/AcAc-t/p/python_metaclass_conflict_study.html

参考

https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072
https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python
https://www.cnblogs.com/JetpropelledSnake/p/9094103.html
http://www.phyast.pitt.edu/~micheles/python/metatype.html

与Python 多重继承时metaclass conflict问题解决与原理探究相似的内容:

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

背景 最近有一个需求需要自定义一个多继承abc.ABC与django.contrib.admin.ModelAdmin两个父类的抽象子类,方便不同模块复用大部分代码,同时强制必须实现所有抽象方法,没想按想当然的写法实现多继承时,居然报错metaclass conflict: In [1]: impo

全网最适合入门的面向对象编程教程:18 类和对象的 Python 实现-多重继承与 PyQtGraph 串口数据绘制曲线图

本文主要介绍了Python中创建自定义类时如何使用多重继承、菱形继承的概念和易错点,同时讲解了如何使用PyQtGraph库对串口接收的数据进行绘图。

Flask API 如何接入 i18n 实现国际化多语言

​ 1. 介绍 上一篇文章分享了 Vue3 如何如何接入 i18n 实现国际化多语言,这里继续和大家分享 Flask 后端如何接入 i18n 实现国际化多语言。 用户请求 API 的多语言化其实有两种解决方案: 后端返回:"USER_ERROR" => 前端渲染:"用户错误" 后端接收请求中 "Ac

Python:多进程并行编程与进程池

Python的并行编程可以采用multiprocessing或mpi4py模块来完成。multiprocessing是Python标准库中的模块,实现了共享内存机制,也就是说,可以让运行在不同处理器核心的进程能读取共享内存。在基于共享内存通信的多进程编程中,常常通过加锁或类似机制来实现互斥。)

深入理解Python多进程:从基础到实战

title: 深入理解Python多进程:从基础到实战 date: 2024/4/29 20:49:41 updated: 2024/4/29 20:49:41 categories: 后端开发 tags: 并发编程 多进程管理 错误处理 资源调度 性能优化 异步编程 Python并发库 引言 在P

Python 合并多张图片至一张图片

PDF 有多页,一次性转成JPG 图片,JAVA 报内存溢出, 现改为,每一页存成一张图片,然后再将多张图片合成一张图片。 安装库 pip3 install Image -i https://pypi.tuna.tsinghua.edu.cn/simple cv2.imread()不支持中文路径,

一篇文章掌握Python中多种表达式的使用:算术表达式、字符串表达式、列表推导式、字典推导式、_集合推导式、_生成器表达式、逻辑表达式、函数调用表达式

Python 中的表达式可以包含各种元素,如变量、常量、运算符、函数调用等。以下是 Python 表达式的一些分类及其详细例子: 1. 算术表达式 算术表达式涉及基本的数学运算,如加、减、乘、除等。 # 加法表达式 sum = 3 + 5 # 结果为 8 # 乘法表达式 product = 4 *

python基础环境

刚开始接触并学习一门开发语言,带着不求甚解的想法,其实也挺有好处的:我并不是所有的东西都知道,但是代码跑起来了。 但是时间久了,还是带着这种想法,可能就会遇到一些棘手的问题。比如电脑上不知不觉已经安装了多个python版本,python3.8/3.10/3.11,甚至一些软件中也集成有python解

为什么 C# 可能是最好的第一编程语言

纵观神州大地,漫游中华互联网,我看到很多人关注为什么你应该开始学习JavaScript做前端,而对blazor这样的面向未来的框架有种莫名的瞧不起,或者为什么你应该学习Python作为你的第一门编程语言,恕不知有多少公司业务是用Python开发的,Python更多是粘合剂,作为胶水语言来使用。我用C

Python 函数

函数返回多个返回值 ```python def multiple_return_value(): import datetime d = datetime.date.today() val_1 = '年份为:{}'.format(d.year) val_2 = '月份为:{}'.format(d.m