【Python】深入理解Python中的元类(MetaClass)

类作为对象

在理解元类之前,您需要掌握Python的类。Python从Smalltalk语言中借用了一个非常特殊的类概念。

在大多数语言中,类只是描述如何产生对象的代码段。在Python中也是如此:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

但是类比Python中的更多。类也是对象。

是的,对象。

一旦使用关键字class,Python就会执行它并创建一个对象。指令

>>> class ObjectCreator(object):
...       pass
...

在内存中创建一个名称为“ ObjectCreator”的对象。

这个对象(类)本身具有创建对象(实例)的能力,这就是为什么它是一个类。

但是,它仍然是一个对象,因此:

  • 您可以将其分配给变量
  • 你可以复制它
  • 您可以为其添加属性
  • 您可以将其作为函数参数传递

例如:

>>> print(ObjectCreator) # 你可以打印一个类,因为它是一个对象
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # 你可以将一个类作为参数传递
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # 你可以为类增加属性
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # 你可以将一个类赋值给一个变量
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

动态创建类

由于类是对象,因此您可以像创建任何对象一样即时创建它们。

首先,您可以使用class以下方法在函数中创建一个类:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # 返回类,而不是实例
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # 该函数返回一个类,而不是实例
<class '__main__.Foo'>
>>> print(MyClass()) # 您可以从这个类创建一个对象
<__main__.Foo object at 0x89c6d4c>

但这并不是那么动态,因为您仍然必须自己编写整个类。

由于类是对象,因此它们必须由某种东西生成。

使用class关键字时,Python会自动创建此对象。但是,与Python中的大多数事情一样,它为您提供了一种手动进行操作的方法。

还记得功能type吗?好的旧函数可以让您知道对象的类型:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

嗯,type具有完全不同的能力,它也可以动态创建类。type可以将类的描述作为参数,并返回一个类。

(我知道,根据传递给它的参数,同一个函数可以有两种完全不同的用法是很愚蠢的。由于Python向后兼容,这是一个问题)

type 这样工作:

type(name, bases, attrs)

参数含义:

  • name:类名称
  • bases:父类的元组(对于继承,可以为空)
  • attrs:包含属性名称和值的字典

例如:

>>> class MyShinyClass(object):
...       pass

可以通过以下方式手动创建:

>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一个类对象
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # 创建一个类的实例
<__main__.MyShinyClass object at 0x8997cec>

您会注意到,我们使用“ MyShinyClass”作为类的名称和变量来保存类引用。它们可以不同,但是没有理由使事情复杂化。

type接受字典来定义类的属性。所以:

>>> class Foo(object):
...       bar = True

可以翻译为:

>>> Foo = type('Foo', (), {'bar':True})

并用作普通类:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

当然,您可以从中继承,因此:

>>>   class FooChild(Foo):
...         pass

将是:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar是从Foo继承的
True

最终,您需要向类中添加方法。只需定义具有适当签名的函数并将其分配为属性即可。

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

在动态创建类之后,您可以添加更多方法,就像将方法添加到正常创建的类对象中一样。

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

您会看到我们要去的方向:在Python中,类是对象,您可以动态动态地创建一个类。这就是Python在使用关键字class时所做的事情,并且通过使用元类来做到这一点。


什么是元类(最终)

元类是创建类的“东西”。

您定义类是为了创建对象,对吗?

但是我们了解到Python类是对象。

好吧,元类就是创建这些对象的原因。它们是类的类,您可以通过以下方式描绘它们:

MyClass = MetaClass()
my_object = MyClass()

您已经看到,type您可以执行以下操作:

MyClass = type('MyClass', (), {})

这是因为该函数type实际上是一个元类。type是Python用于在幕后创建所有类的元类。

现在您想知道为什么用小写而不是小写Type?

好吧,我想这与str创建字符串对象和int类创建整数对象的类的一致性有关。type只是创建类对象的类。

您可以通过检查__class__属性来看到。

一切,我的意思是一切,都是Python中的对象。其中包括整数,字符串,函数和类。它们都是对象。所有这些都是从一个类创建的:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

现在,什么是所有__class__的__class__?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

因此,元类只是创建类对象的东西。

如果愿意,可以将其称为“类工厂”。

type 是Python使用的内置元类,但是您当然可以创建自己的元类。


__metaclass__属性

在Python 2中,您可以__metaclass__在编写类时添加属性(有关Python 3语法,请参见下一部分):

class Foo(object):
    __metaclass__ = something...
    [...]

如果这样做,Python将使用元类创建类Foo。

小心点,这很棘手。

您class Foo(object)先编写,但Foo尚未在内存中创建类对象。

Python将__metaclass__在类定义中查找。如果找到它,它将使用它来创建对象类Foo。如果没有,它将 type用于创建类。

读几次。

当您这样做时:

class Foo(Bar):
    pass

Python执行以下操作:

Foo中有__metaclass__属性吗?

如果有的话,在内存中创建一个类对象(我说的是类对象,陪在我身边在这里),名称Foo使用__metaclass__赋予的元类。

如果Python找不到__metaclass__,它将在MODULE级别查找__metaclass__(这里不明白是什么意思) ,然后尝试执行相同的操作(但仅适用于不继承任何内容的类,基本上是老式的类)。

然后,如果根本找不到任何对象__metaclass__,它将使用Bar(第一个父对象)的元类(可能是默认的type)创建类对象。

请注意,该__metaclass__属性将不会被继承,父(Bar.__class__)的元类将被继承。如果Bar使用的__metaclass__是创建的属性Bar与type()(不是type.__new__()),子类不会继承该行为。

现在最大的问题是,您可以赋予__metaclass__变量什么内容?

答案是可以创建一个类的东西。

什么可以创建一个类?type,或任何继承或使用它的内容。


Python 3中的元类

设置元类的语法在Python 3中已更改:

class Foo(object, metaclass=something):
    ...

即不再使用__metaclass__该属性,而在基类(即父类)列表中使用关键字参数。

但是,元类的行为(功能)基本保持不变。

在Python 3中元类得到增强,可以将属性作为关键字参数传递给元类,如下所示:

class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):
    ...

阅读以下部分以了解python如何处理此问题。


自定义元类

元类的主要目的是在创建类时自动更改它。

通常,您要对API进行此操作,在API中要创建与当前上下文匹配的类。

想象一个愚蠢的示例,在该示例中,您决定模块中的所有类的属性都应以大写形式编写。有多种方法可以执行此操作,但是一种方法是__metaclass__在模块级别进行设置。

这样,将使用此元类创建该模块的所有类,而我们只需要告诉元类将所有属性都转换为大写即可。

幸运的是,__metaclass__实际上可以是任何可调用的东西(原文:__metaclass__ can actually be any callable),它不必是正式的类(我知道,名称中带有“ class”的东西不必是类,请弄清楚……但这很有用)。

因此,我们将从使用函数的简单示例开始。

# 元类将自动获得您通常传递给“type”的相同参数
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """
    # 选择任何不以“__”开头的属性并转换为大写
    uppercase_attrs = {
        attr if attr.startswith("__") else attr.upper(): v
        for attr, v in future_class_attrs.items()
    }

    # 使用type()方法来创建类
    return type(future_class_name, future_class_parents, uppercase_attrs)

__metaclass__ = upper_attr # 这将影响模块中的所有类

class Foo(): # 尽管全局的__metaclass__对“对象”不起作用
    # 但是我们可以在这里定义__metaclass__属性,从而只影响这个类
    # 同时在这个类的子类中也生效
    bar = 'bip'

让我们测试一下:

>>> hasattr(Foo, 'bar')
False
>>> hasattr(Foo, 'BAR')
True
>>> Foo.BAR
'bip'

现在,让我们做完全一样的操作,但是对元类使用真实的类:

# 上文说过,“type”实际上是类似于“str”和“int”的类,所以你可以继承它
class UpperAttrMetaclass(type):
    # __new__ 是先于 __init__ 被调用的方法。
    # 它的功能是创建并返回对象,而__init__只是初始化作为参数传递的对象。__new__很少被用到,除非您想控制对象的创建方式。
    # 这里创建的对象是类,我们要对其进行自定义,因此我们重载__new__。
    # 如果您希望一些高级用法也涉及覆盖__call__,那么您也可以在__init__中做一些事情,但是我们不会看到。
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in future_class_attrs.items()
        }
        return type(future_class_name, future_class_parents, uppercase_attrs)

让我们重载上面的内容,但是现在有了更短,更实际的变量名,我们知道它们的含义了:

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return type(clsname, bases, uppercase_attrs)

您可能已经注意到了额外的争论cls。它没有什么特别的:__new__总是将其定义的类作为第一个参数。就像您self将接收实例作为第一个参数的普通方法或类方法的定义类一样。

但这不是适当的OOP。我们是直接调用type(),而不是覆盖或调用父类的__new__。让我们改为:

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return type.__new__(cls, clsname, bases, uppercase_attrs)

通过使用super,我们可以使其更加整洁,这将简化继承(因为是的,您可以具有元类,从元类继承,从类型继承):

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return super(UpperAttrMetaclass, cls).__new__(
            cls, clsname, bases, uppercase_attrs)

哦,在python 3中,如果您使用关键字参数进行此调用,如下所示:

class Foo(object, metaclass=MyMetaclass, kwarg1=value1):
    ...

它将在元类中转换为使用它:

class MyMetaclass(type):
def new(cls, clsname, bases, dct, kwargs1=default):

就是这样。元类实际上没有更多的内容。

使用元类的代码之所以复杂,并不是因为元类,而是因为您通常使用元类来做一些扭曲的事情,这些事情依赖于内省、操作继承、改变返回成员变量(如__dict__)等。(原文:The reason behind the complexity of the code using metaclasses is not because of metaclasses,it’s because you usually use metaclasses to do twisted stuff relying on introspection, manipulating inheritance, vars such as __dict__, etc.)

译者注:正常情况下生成的类属性都是存放在__dict__中的,你可以使用“类名.__dict__”的方式查看它(如果要查看实例对象的属性,即初始化时设置的self.XXX=XXX这类属性,可以使用“对象.__dict__”进行查看。但请记住,元类是针对类属性的,对实例对象的属性无效,因为__new__函数的调用在__init__之前,无法影响到实例对象的属性)。但有时候我们会利用元类,将某些类属性放到__dict__以外的变量中,举个例子:

class IntegerAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        mappings = dict()
        for k,v in attrs.items():  # 遍历attrs,将value是int类型的属性存放到mappings中
            if isinstance(v,int):
                mappings[k] = v
        for k in mappings.keys():  # 删除相关属性,这样就无法使用“对象.属性名”的形式直接获取到值
            attrs.pop(k)
        attrs['__mappings__'] = mappings  # 将int类型的属性以字典的形式全部存放在__mappings__属性中
        return super(IntegerAttrMetaclass, cls).__new__(cls, clsname, bases, attrs)
        
        
class Student(object, metaclass=IntegerAttrMetaclass):
    name = "student"  # 注意,这是类属性,调用方式是Student.name,区别于Student().name
    age = 6

    def __init__(self, stu_name, stu_age):
        self.name = stu_name   # 调用方式:Student().name
        self.age = stu_age
        
        
>>> stu = Student("Bob", 12)
>>> print("Student.__dict__:", Student.__dict__)
Student.__dict__: {'__module__': '__main__', 'name': 'student', '__init__': <function Student.__init__ at 0x0000029E70582AE8>, '__mappings__': {'age': 6}, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
>>> print("Student.__mappings__:", Student.__mappings__)
Student.__mappings__: {'age': 6}
>>> print("stu.__dict__:", stu.__dict__)
stu.__dict__: {'name': 'Bob', 'age': 12}

实际上,元类在执行黑魔法时特别有用,因此可以处理复杂的事情。但就其本身而言,它们是简单的:

  • 拦截类创建
  • 修改类
  • 返回修改后的类

为什么要使用元类?

现在是个大问题。为什么要使用一些晦涩的易错功能?

好吧,通常您不会:

元类是更深层的魔术,99%的用户永远不必担心它。如果您想知道是否需要它们,则说明你不需要它们(实际上需要他们的人可以肯定地知道他们需要它们,并且不需要解释原因)。

Python大师Tim Peters

元类的主要用例是创建API。一个典型的例子是Django ORM。它允许您定义如下内容:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

但是,如果您这样做:

person = Person(name='bob', age='35')
print(person.age)

它不会返回IntegerField对象。它将返回一个int,甚至可以直接从数据库中获取它。

这是可能的,因为models.Modeldefine __metaclass__并使用了一些魔术,这些魔术将使Person您使用简单的语句定义的对象变成与数据库字段的复杂挂钩。

Django通过公开一个简单的API并使用元类,从该API重新创建代码来完成幕后的实际工作,使看起来复杂的事情变得简单。


写在最后

首先,您知道类是可以创建实例的对象。

好吧,实际上,类本身就是元类的实例。

>>> class Foo(object): pass
>>> id(Foo)
142630324

一切都是Python中的对象,它们都是类的实例或元类的实例。

除了type。

type实际上是它自己的元类。这不是您可以在纯Python中复制的东西,而是通过在实现级别上作弊来完成的。

其次,元类很复杂。您可能不希望将它们用于非常简单的类更改。您可以使用两种不同的技术来更改类:

99%的情况您需要类变更,最好使用这些。

但是98%的情况,您根本不需要更改类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值