Python中的元类(metaclass)

metaclass在Python中是个“逆天”的存在,有人认为它是“阿拉丁神灯”,无所不能;有人认为它是“潘多拉魔盒”,会蛊惑程序员去滥用,释放“恶魔”,然后悲剧就产生了。就连硅谷一线大厂要想使用metaclass都得需要特批。深入理解它的Python开发人员占比不到0.1%。

它会带来好处也容易带来灾难,只有深入了解它,才能使用好它。

一切皆对象

类也是对象

在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段,在Python中这一点仍然成立。但是,Python中的类还远不止如此。类同样也是一种对象。只要你使用关键字class,Python解释器在执行的时候就会创建一个对象。下面的代码段:

class MyClass(object):
    pass

将在内存中创建一个对象,名字就是MyClass。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是你可以对它做如下的操作: 你可以将它赋值给一个变量, 你可以拷贝它, 你可以为它增加属性, 你可以将它作为函数参数进行传递。

在Python中有两种对象:

  • 类型(类)对象:可以被实例化和继承;

  • 非类型(实例)对象:不可以被实例和继承。

耳熟能详的一句话,Python中一切皆为对象:

  • 在Python里,int整形是对象,整数2也是对象,定义的函数、类都是对象,定义的变量也是对象。总之,在Python里能用到的都可以称之为对象。

type和object

明白了Python中一切皆对象之后,再来了解一下Python中对象之间的两种关系。面向对象体系中有两种关系:

  • 父子关系:通常描述为“继承关系的两个类,被继承的那个类是父类,继承的那个类是子类”。

  • 类型实例关系:这种关系存在于两个对象中,其中一个对象(实例)是另一个对象(类型)的具体实现。

Python中万物皆对象,一个Python对象可能拥有两个属性,__class____bases____class__表示这个对象是谁创建的,__bases__表示这个类的父类__class__type()函数效果一样。

代码示例:

class MyClass:
    pass
 
>>> MyClass.__class__
type
>>> MyClass.__bases__
(object,)
>>> int.__class__
type
>>> int.__bases__
(object,)
>>> object.__class__  # object是type的实例,object创建自type
type
>>> object.__bases__  # object没有超类,它本身是所以对象的超类
()
>>> type.__class__    # type创建自本身
type
>>> type.__bases__    # type继承自object,即object是type的超类
(object,)  

从上面的代码可以知道:

  • type对象的顶点,所有对象都创建自type

  • object类继承的顶点,所有类都继承自object

这是一个重点!

元类、类、实例

  • object是所有类的超类,而且type也是继承自object;所有对象创建自type,而且object也是type的实例。

  • “type是object的类型,同时,object又是type的超类”,那到底是先有object还是先有type呢?这就像“先有鸡还是先有蛋问题”。

  • object和type是Python中的两个源对象,事实上,它们是互相依赖对方来定义,所以它们不能分开而论。

  • 通过这两个源对象可以繁育出一堆对象:list、tuple、dict等。元类就是这些类的类,这些类是元类的实例。

metaclass

type(“造物的上帝”)

就像str是用来创建字符串对象的类,int是用来创建整数对象的类,而type就是创建类对象的类

类本身不过是一个名为type类的实例。在 Python的类型世界里,type这个类就是造物的上帝。用户自定义类,只不过是type类的__call__运算符的重载。当我们定义一个类的语句结束时,真正发生的情况,是 Python 调用 type 的__call__运算符。简单来说,当你定义一个类时,写成下面时:

class MyClass:
  data = 1

Python 真正执行的是下面这段代码:

class = type(classname, superclasses, attributedict)

这里等号右边的type(classname, superclasses, attributedict),就是type__call__运算符重载,它会进一步调用:

type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)

这一切都可以通过代码验证,比如下面这段代码示例:

class MyClass:
    data = 1
    
>>> instance = MyClass()
>>> MyClass, instance
(__main__.MyClass, <__main__.MyClass at 0x1b1b238a688>)
 
>>> MyClass = type('MyClass', (), {'data': 1})
>>> instance = MyClass()
>>> MyClass, instance
(__main__.MyClass, <__main__.MyClass at 0x1b1b2387708>)
 
>>> instance.data
1

由此可见,正常的 MyClass 定义,和手工去调用type运算符的结果是一样的。

class type(name, bases, dict)参数详解:

  1. 使用1个参数,返回对象的类型。

  • 就像object.__class__。

  1. 使用3个参数,返回一个新类型对象。本质上,这是类声明的一种动态形式。

  • 参数name是一个字符串,表示类名称,并记录为__name__属性;

  • 参数bases是一个元组,一个个记下基础类,并记录为__bases__属性;

  • 参数dict是一个字典,包含类本体的命名空间并被赋值到标准字典。并记录为__dict__属性。

举个例子:下面两个声明创建了相同类型的对象:

class X:
    a = 1
    
X = type('X', (object,), dict(a=1))
在Python 3.6发生改变, type的子类不能重写 type.__new__ ,不久将不再使用单参数的样式获取对象的类型。

slots(定位,跟踪)

当在类中定义一个魔术方法的时候,function除了__dict__中的条目之外,在整个类结构中,作为一个描述着这个类的指针一样结束。这个结构对于每一个魔术方法有一个字段。出于一些原因这些字段被称为type slots。

现在,这里有另一个特征,通过__slots__属性执行,一个拥有__slots__的class创造的实例不包含__dict__(这将使用更少的内存)。副作用是实例不能出现未在__slots__中指定的字段:如果你尝试设置一个不存在于__slots__中的字段,那么将会获得一个报错。

这里提及的单独的 slots都是 type slots不是 __slots__。(类里的魔术方法)
class Foobar:
    """
     A class that only allows these attributes: "a", "b" or "c"
    """
    __slots__ = "a", "b", "c"
 
>>> foo = Foobar()
>>> foo.a = 1
>>> # foo.x = 2
样例中去掉最后一行注释, foo.x = 2会报错。

metaclass属性

metaclasstype的子类,通过替换type__call__运算符重载机制,“超越变形”正常的类。其实,理解了以上几点,我们就会明白,正是 Python 的类创建机制,给了metaclass大展身手的机会。

一旦把一个类型MyClassmetaclass 设置成MyMetaMyClass就不再由原生的type创建,而是会调用MyMeta__call__运算符重载。

class = type(classname, superclasses, attributedict) 
# 变为了
class = MyMeta(classname, superclasses, attributedict)

来看个例子:

class MyMeta(type):
    def __new__(cls, *args, **kwargs):
        print('===>MyMeta.__new__')
        print(cls.__name__)
        return super().__new__(cls, *args, **kwargs)
 
    def __init__(self, classname, superclasses, attributedict):
        super().__init__(classname, superclasses, attributedict)
        print('===>MyMeta.__init__')
        print(self.__name__)
        print(attributedict)
        print(self.tag)
 
    def __call__(self, *args, **kwargs):
        print('===>MyMeta.__call__')
        obj = self.__new__(self, *args, **kwargs)
        self.__init__(self, *args, **kwargs)
        return obj
 
    
class Foo(object, metaclass=MyMeta):
    tag = '!Foo'
 
    def __new__(cls, *args, **kwargs):
        print('===>Foo.__new__')
        return super().__new__(cls)
 
    def __init__(self, name):
        print('===>Foo.__init__')
        self.name = name
# ----------------------输出----------------------
# ===>MyMeta.__new__
# MyMeta
# ===>MyMeta.__init__
# Foo
# {'__module__': '__main__', '__qualname__': 'Foo', 'tag': '!Foo', '__new__': <function Foo.__new__ at 0x000001B1B2379678>, '__init__': <function Foo.__init__ at 0x000001B1B2379708>, '__classcell__': <cell at 0x000001B1B23880A8: MyMeta object at 0x000001B1B1A509A8>}
# !Foo
 
>>> print('test start')
>>> foo = Foo('test')
>>> print('test end')
# test start
# ===>MyMeta.__call__
# ===>Foo.__new__
# ===>Foo.__init__
# test end

在创建Foo类的时候,python做了如下操作:

  1. Foo中有metaclass这个属性吗?如果是,Python会在内存中通过metaclass创建一个名字为Foo的类对象。

  1. 如果Python没有找到metaclass,它会继续在父类中寻找metaclass属性,并尝试做和前面同样的操作。

  1. 如果Python在任何父类中都找不到metaclass,它就会在模块层次中去寻找metaclass,并尝试做同样的操作。

  1. 如果还是找不到metaclass,Python就会用内置的type来创建这个类对象。

现在的问题就是,你可以在metaclass中放置些什么代码呢?答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东西都可以。用类实现可以(比如上面这个例子),用函数实现也可以。但是metaclass必须返回一个类

def MyMetaFunction(classname, superclasses, attributedict):
    attributedict['year'] = 2019
    return type(classname, superclasses, attributedict)
 
 
class Foo(object, metaclass=MyMetaFunction):
    tag = '!Foo'
 
    def __new__(cls, *args, **kwargs):
        print('===>Foo.__new__')
        return super().__new__(cls)
 
    def __init__(self, name):
        print('===>Foo.__init__')
        self.name = name
 
>>> foo = Foo('test')
===>Foo.__new__
===>Foo.__init__
>>> print('name:%s,tag:%s,year:%s' % (foo.name, foo.tag, foo.year))
name:test,tag:!Foo,year:2019

把上面的例子运行完之后就会明白很多了,正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以。换种方式理解:元类、装饰器、类装饰器都可以归为元编程

总结

正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以,这就使得程序代码的维护变得困难。metaclass是 Python 的黑魔法之一,在掌握它之前不要轻易尝试。感觉上Python的规范原则很松,但这也使得Python更灵活,对代码的编写理应由程序员自己负责,自己写的代码还是得自己负责啊。

元类、装饰器、类装饰器都可以归为元编程,它们之间有一些相似点,还是在实际的应用中选择比较,使用合适的工具进行编程。

猜你喜欢

转载自blog.csdn.net/weixin_41951954/article/details/128816495