详解python metaclass(元类)


Posted in Python onAugust 13, 2020

元编程,一个听起来特别酷的词,强大的Lisp在这方面是好手,对于Python,尽管没有完善的元编程范式,一些天才的开发者还是创作了很多元编程的魔法。Django的ORM就是元编程的一个很好的例子。

本篇的概念和例子皆在Python3.6环境下

一切都是对象

Python里一切都是对象(object),基本数据类型,如数字,字串,函数都是对象。对象可以由类(class)进行创建。既然一切都是对象,那么类是对象吗?

是的,类也是对象,那么又是谁创造了类呢?答案也很简单,也是类,一个能创作类的类,就像上帝一样,开启了万物之始。这样的类,称之为元类(classmeta)。

类的定义

对象是通过类创建的,这个很好理解。例如下面的代码:

class Bar(object):
  pass

bar = Bar()
print(bar, bar.__class__)  # <__main__.Bar object at 0x101eb4630> <class '__main__.Bar'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class 'type'>

可以看见对象 bar 是类 Bar 创建的实例。然而 Bar,看起来却是由一个叫 type 的类创建的实例。即 bar <-- Bar < -- type

上面的例子,对象是动态创建的,类则是通过关键字 class 声明定义的。class关键字背后的玄机是什么呢?

实际上,class Bar(object) 这样的代码,等价于 Bar = type('Bar', (objects, ), {})
即类 type 通过实例化创建了它的对象 Bar,而这个 Bar 恰恰是一个类。这样能创建类的类,就是 Python 的元类。

从创建 Bar 的代码上来看,元类 type 的 __init__ 方法有3个参数,

  • 第一个是创建的类的名字
  • 第二个是其继承父类的元类列表,
  • 最后就是一个属性字典,即该类所具有的属性。

type 元类

type是小写,因而很容易误以为它是一个函数。通过help(type)可以看到它的定义如下:

class type(object):
  """
  type(object_or_name, bases, dict)
  type(object) -> the object's type
  type(name, bases, dict) -> a new type
  """
  def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    # (copied from class doc)
    """
    pass

   @staticmethod # known case of __new__
  def __new__(*args, **kwargs): # real signature unknown
    """ Create and return a new object. See help(type) for accurate signature. """
    pass

如前所述,__init__方法接受三个参数,type 实例化的过程,会创建一个新的类。创建类的代码来自 __new__ 方法,它的参数其实和 __init__,一样。至于它们之间有什么关系,后面再做介绍。目前只要知道,当调用 type 进行实例化的时候,会先自动调用 __new__ 方法,然后再接着调用 __init__方法,在类外面来看,最终会实例化一个对象,这个对象是一个类。

从 type 的定义来看,它继承 object,Python3的所有类,都继承来着 object,类type 也是 object 的实例,令人奇怪的是,object 既是类也是对象,它也是由 type实例化而来。有一种鸡生蛋,蛋生鸡的悖论。暂且先不管,只要知道所有类的顶级继承来自 object 就好。

自定义元类

既然元类可以创建类,那么自定义元类就很简单了,直接继承类 type 即可。先看下面一个例子:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


print(MyType, MyType.__class__) # <class '__main__.MyType'> <class 'type'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>

可以看到,Bar在声明的时候,指定了其元类,此时的类 Bar 的__class__属性不再是 type,而是 MyType。即之前定义 Bar 的代码不再是 Bar = type('Bar', (objects, ), {}), 而是 Bar = MyType('Bar', (objects, ), {})。创建的元类的代码是MyType = type('MyType', (objects, ), {})

如果一个类没有显示的指定其元类,那么会沿着继承链寻找父类的元类,如果一直找不到,那么就使用默认的 type 元类。

元类冲突

每个类都可以指定元类,但是父类和子类的元类要是一条继承关系上的,否则会出现元类冲突。并且这个继承关系中,以继承最后面的元类为其元类。

元类的查找顺序大致为,先查看其继承的父类,找到父类的元类即停止。若直接父类没有元类,直到顶级父类 object ,此时父类(object)的元类是 type(basemetaclass),再看其自身有没有指定元类(submetaclass),如果指定了元类(submetaclass),再对比这个子元类(submetaclass)和父元类(basemetaclass),如果它们毫无继承关系,那么将会抛出元类冲突的错误。如果指定的子元类是父元类的父类,那么将会使用父元类,否则将使用期指定的子元类。

submetaclass <- basemetaclass使用 submetaclass 作为最终元类,
basemetaclass <- submetaclass, 使用 basemetaclass 作为最终元类,
两者无继承关系,抛出冲突。

有点像绕口令,且看代码例子

class MyType(type):
  pass

# 等价于 MyType = type('MyType', (object, ), {})

class Bar(object, metaclass=MyType):
  pass

# 等价于 Bar = MyType('Bar', (object, ), {})

class Foo(Bar):
  pass

# 等价于 Foo = MyType('Foo', (Foo, object, ), {})

print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

Bar的父元类(basemetaclass)type,指定子元类(submetaclass)是 MyType, MyType 继承自 type,所以Bar的元类是 MyType。

又如:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=type):
  pass


print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

尽管 Foo 也指定了元类(submetaclass) type,可是其父类的元类(basemetaclass)是 MyType, MyType
是 type的子类,因此 Foo的元类抛弃了指定的(submetaclass) type,而是沿用了其父类的MyType。

当 submetaclass 和 basemetaclass 没有继承关系的时候,将会元类冲突

class MyType(type):
  pass

class MyOtherType(type):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass

运行代码,当定义的时候就会出现TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict)元类冲突的错误。

修改代码如下:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

可以看到 Bar 和 Foo 分别有自己的元类,并且都符合继承关系中寻找。再调换一下元类看看:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyOtherType):
  pass


class Foo(Bar, metaclass=MyType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyOtherType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

都使用了Foo还是使用了元子类作为元类。究其原因,其实也很好理解。定义父类的时候,使用了元类MyOtherType 。定义子类的时候,通过继承,找到了创建父类的元类,那么父类就是 MyOtherType 的实例。

如果使用 MyType 做为元类,那么他就是 MyType 的实例,MyType的实例会比MyOtherType具有的属性少,那么在继承链上,它又是 Bar的子类,这样看就是子类比父类还狭窄了,显然不是一个好的关系。即变成了下面的关系

Bar     <-  MyOtherType
 
  |           ↑
  |           |
  ↓           |

Foo     <-  MyType

因此当 MyType 是 MyOtherType的父类的时候,即使 Foo 指定了 MyType作为元类,还是会被忽略,使用其父元类MyOtherType。

上面的线的箭头要一直,才能使用各自指定的元类,否则使用箭头指向的那个类作为元类。元类没有继承关系,元类冲突。

对象(类)实例化

目前为止,我们了解了类的定义,即类是如何被元类创建出来的,但是创建的细节尚未涉及。即元类是如何通过实例化创建类的过程。这也是对象创建的过程。

前文介绍了一个对象是通过类创建的,类对象是通过元类创建的。创建类中,会先调用元类的__new__方法,设置其名称,继承关系和属性,返回一个实例。然后再调用实例的__init__方法进行初始化实例对象。

class MyType(type):

  def __init__(self, *args, **kwargs):
    print('init ', id(self), args, kwargs)

  def __new__(cls, *args, **kwargs):
    print('new', id(cls), args, kwargs)
    instance = super(MyType, cls).__new__(cls, *args, **kwargs)
    print(id(instance))
    return instance


class Bar(object, metaclass=MyType):
  pass

运行代码可以看见输出:

new 4323381304 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}
4323382232
init  4323382232 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}

注意,上面代码仅关注 Bar 类的创建,即 Bar =MyType('Bar', (object, ), {})这个定义代码。MyType进行实例化创建 Bar的过程中,会先用 其 __new__ 方法,后者调用了父类 type的 __new__方法,并返回了元类的实例, 同时调用这个实例的__init__方法,后者对改实例对象进行初始化。这也就是为什么方法名为 __init__

通常我们会在 __init__方法初始化一些实例对象的属性如果 __new__ 方法什么也不返回,那么 __init__ 方法是不会被调用的。

instance = super(MyType, cls).__new__(cls, *args, **kwargs), 有的地方也喜欢写成 type.__new__或者 type,前者是python中如何调用父类方法的问题,后者是直接使用type创建类的过程。比较推荐的写法还是使用 super 调用其父类的方法的方式。

类是元类的对象,普通类创建对象的过程,也是一样。因此,只要重写 __new__方法,还可以实现一个类还可以创建另外一个类的实例的魔法。

移花接木

重写 __new__ 方法,让其创建另外一个类的实例。

class Bar:
  def __init__(self, name):
    self.name = name
    print('Bar init')

  def say(self):
    print('say: Bar {}'.format(self.name))


class Foo(object):

  def __init__(self):
    print('self {}'.format(self))

  def __new__(cls, *args, **kwargs):
    instance = super(Foo, cls).__new__(Bar, *args, **kwargs)
    print('instance {}'.format(instance))
    instance.__init__('a class')
    return instance

  def say(self):
    print('say: Foo')


m = Foo()
print('m {}'.format(m))
m.say()

输出

instance <__main__.Bar object at 0x104033240>
Bar init
m <__main__.Bar object at 0x104033240>
say: Bar a class

在类 Foo 中,通过重写 __new__返回了一个 Bar 类的实例对象,然后调用 Bar 实例的 __inti__ 方法初始化,由于返回了 bar 实例,因此 Foo 的实例没有被创建,因此也不会调用它的实例方法 __inti__ 。这样就把 移花(Bar)接木(Foo)上了。

也许有人会觉得这样的诡异魔法有什么用呢?实际上,Tornado框架使用了这样的技术实现了一个叫 Configurable 的工厂类,用于创建不同网络IO下的epoll还是select模型。有兴趣可以参考其实现方式。

元类的应用

讨论了那么多原理的东西,最后肯定是要应用到实际中才有意义。既然类可以被动态的创建,那么很多定义在类的方法,岂不是也可以被动态的创建了呢。这样就省去了很多重复工作,也能实现酷酷的元编程。

元类可以创建单例模式,也可以用来实现 ORM,下面介绍的是Django使用元类实现的查找方式。更经典的model定义网上有很多例子,就不再介绍了。下面介绍一个model通过manger管理器实现查询方法的例子

import inspect


class QuerySet:

  def get(self, *args, **kwargs):
    print('get method')
    return self

  def filter(self, *args, **kwargs):
    print('filter method')
    return self


class BaseManager:

  def __init__(self):
    pass

  @classmethod
  def from_queryset(cls, queryset_class, class_name=None):
    if class_name is None:
      class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
    class_dict = {
      '_queryset_class': queryset_class,
    }
    class_dict.update(cls._get_queryset_methods(queryset_class))
    return type(class_name, (cls,), class_dict)

  def get_queryset(self):
    return self._queryset_class()

  @classmethod
  def _get_queryset_methods(cls, queryset_class):
    def create_method(name, method):
      def manager_method(self, *args, **kwargs):
        return getattr(self.get_queryset(), name)(*args, **kwargs)

      manager_method.__name__ = method.__name__
      manager_method.__doc__ = method.__doc__
      return manager_method

    new_methods = {}
    for name, method in inspect.getmembers(queryset_class, predicate=inspect.isfunction):
      if hasattr(cls, name):
        continue
      queryset_only = getattr(method, 'queryset_only', None)
      if queryset_only or (queryset_only is None and name.startswith('_')):
        continue
      new_methods[name] = create_method(name, method)
    return new_methods


class Manager(BaseManager.from_queryset(QuerySet)):
  pass


class ModelMetaClass(type):

  def __new__(cls, *args, **kwargs):
    name, bases, attrs = args
    attrs['objects'] = Manager()
    return super(ModelMetaClass, cls).__new__(cls, name, bases, attrs)


class Model(object, metaclass=ModelMetaClass):
  pass


class User(Model):
  pass


User.objects.get()
User.objects.filter()
User.objects.filter().get()

这样model就用使用期管理器Manger 下的方法了。通过model的元类ModelMetaClass,定义model的时候,就初始化了一个 Manger对象挂载到Model下面,而定义Manger的时候,也通过元类将QuerySet下的查询方法挂载到Manger下了。

总结

Python里一切都是对象,对象都是由类进行创建实例化而来。既然一切是对象,那么类也是对象,而类这种对象又是由一种更高级类创建而来,即所谓的元类。

元类可以创建类,Python默认的元类是 type。通过继承type,可以自定义元类,在自定义元类的时候定义或者重载 __new__,可以创建该类的实例对象,同时也可以修改类创建对象的行为。类通过 __new__创建实例对象,然后调用实例对象的 __init__初始化实例对象。

在使用自定义元类的时候,子类的的元类和父类的元类有关系,前者指定的元类必须和父类的元类是一个继承关系上的,否则会出现元类冲突。子类选取元类的取决于指定的元类和父元类的继承关系,子元类若是父元类的子类,则指定的元类为子元类,否则将会被忽略,使用父元类为其元类。

元类是元编程的一种技术手段,常用于实现工厂模式的策略。通过定义元类动态创建类和展开,可以实现很多设计精妙的应用。ORM 正式其中一种常用的方法。

以上就是详解python metaclass(元类)的详细内容,更多关于python metaclass(元类)的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python生成随机数的方法
Jan 14 Python
跟老齐学Python之dict()的操作方法
Sep 24 Python
python下os模块强大的重命名方法renames详解
Mar 07 Python
Python中max函数用于二维列表的实例
Apr 03 Python
pygame游戏之旅 计算游戏中躲过的障碍数量
Nov 20 Python
python openpyxl使用方法详解
Jul 18 Python
python3 selenium自动化测试 强大的CSS定位方法
Aug 23 Python
keras模型可视化,层可视化及kernel可视化实例
Jan 24 Python
python判断链表是否有环的实例代码
Jan 31 Python
python递归调用中的坑:打印有值, 返回却None
Mar 16 Python
最新PyCharm从安装到PyCharm永久激活再到PyCharm官方中文汉化详细教程
Nov 17 Python
解决Python字典查找报Keyerror的问题
May 26 Python
MAC平台基于Python Appium环境搭建过程图解
Aug 13 #Python
Expected conditions模块使用方法汇总代码解析
Aug 13 #Python
深入了解Python装饰器的高级用法
Aug 13 #Python
python高级特性简介
Aug 13 #Python
Pytest如何使用skip跳过执行测试
Aug 13 #Python
matplotlib基础绘图命令之bar的使用方法
Aug 13 #Python
Python logging模块原理解析及应用
Aug 13 #Python
You might like
PHP语言中global和$GLOBALS[]的分析 之二
2012/02/02 PHP
phpadmin如何导入导出大数据文件及php.ini参数修改
2013/02/18 PHP
浅析PHP递归函数返回值使用方法
2013/02/18 PHP
CodeIgniter框架提示Disallowed Key Characters的解决办法
2014/04/21 PHP
PHP查找与搜索数组元素方法总结
2015/06/12 PHP
php实现仿写CodeIgniter的购物车类
2015/07/29 PHP
yii的入口文件index.php中为什么会有这两句
2016/08/04 PHP
CentOS 7.2 下编译安装PHP7.0.10+MySQL5.7.14+Nginx1.10.1的方法详解(mini版本)
2016/09/01 PHP
javascript 遍历验证所有文本框的值
2009/08/27 Javascript
javascript将数组插入到另一个数组中的代码
2013/01/10 Javascript
Jquery 全选反选实例代码
2015/11/19 Javascript
AngularJS bootstrap启动详解及实例代码
2016/09/14 Javascript
详解如何在vue中使用sass
2017/06/21 Javascript
详解AngularJS2 Http服务
2017/06/26 Javascript
weex slider实现滑动底部导航功能
2017/08/28 Javascript
让axios发送表单请求形式的键值对post数据的实例
2018/08/11 Javascript
npm scripts 使用指南详解
2018/10/08 Javascript
Vue实现简单的留言板
2020/10/23 Javascript
教你如何将 Sublime 3 打造成 Python/Django IDE开发利器
2014/07/04 Python
利用python脚本如何简化jar操作命令
2019/02/24 Python
用python求一个数组的和与平均值的实现方法
2019/06/29 Python
Python实现RGB与HSI颜色空间的互换方式
2019/11/27 Python
django 实现手动存储文件到model的FileField
2020/03/30 Python
基于python+selenium自动健康打卡的实现代码
2021/01/13 Python
常用的HTML5列表标签
2017/06/20 HTML / CSS
美国校园市场:OCM
2017/06/08 全球购物
巴黎一票通:The Paris Pass
2018/02/10 全球购物
丝芙兰波兰:Sephora.pl
2018/03/25 全球购物
班主任新年寄语
2014/04/04 职场文书
《荷花》教学反思
2014/04/16 职场文书
大学生学习面向未来的赶考思想汇报
2014/09/12 职场文书
企业2014年度工作总结
2014/12/10 职场文书
2016道德模范先进事迹材料
2016/02/26 职场文书
python使用XPath解析数据爬取起点小说网数据
2021/04/22 Python
MySQL 如何限制一张表的记录数
2021/09/14 MySQL
Python中Matplotlib的点、线形状、颜色以及绘制散点图
2022/04/07 Python