元类中 __new__ 的行为(也在继承的上下文中)

Posted

技术标签:

【中文标题】元类中 __new__ 的行为(也在继承的上下文中)【英文标题】:Behavior of __new__ in a metaclass (also in context of inheritance) 【发布时间】:2021-07-10 07:45:36 【问题描述】:

好的,显然元类中的__new__ 在元类的实例(即类对象)被实例化时运行,因此元类中的__new__ 提供了一个挂钩来在类定义时拦截事件(/运行的代码)(例如验证/执行类属性(如方法等)的规则。

元类中__new__ 的许多在线示例从__new__ 返回类型构造函数的实例,这似乎有点问题,因为这会阻塞__init__ (docs: "如果__new__() 不返回cls 的实例,则不会调用新实例的__init__() 方法")。

在修改元类中 __new__ 的返回值时,我遇到了一些我不完全理解的奇怪案例,例如:

class Meta(type):
    
    def __new__(self, name, bases, attrs):
        print("Meta __new__ running!")
        # return type(name, bases, attrs)                     # 1. 
        # return super().__new__(self, name, bases, attrs)    # 2.
        # return super().__new__(name, bases, attrs)          # 3.  
        # return super().__new__(type, name, bases, attrs)    # 4. 
        # return self(name, bases, attrs)                     # 5.

    def __init__(self, *args, **kwargs):
        print("Meta __init__ running!")
        return super().__init__(*args, **kwargs)
    
class Cls(metaclass=Meta):
    pass
    这在示例中很常见并且通常有效,但会阻止__init__ 这有效,__init__ 也会触发;但是为什么要将 self 传递给 super() 调用呢? self/cls 不应该用 super() 自动传递吗? 这会引发一个我无法理解的奇怪错误:TypeError: type.__new__(X): X is not a type object (str); X是什么? self 不应该自动通过吗? 3.的错误启发我去玩super()调用的第一个arg,所以我尝试直接传type;这也阻止了__init__。这里发生了什么? 只是为了好玩;这会导致 RecursionError

尤其是案例 1. 和 2. 似乎对从绑定到元类的类继承具有相当深远的影响:

class LowerCaseEnforcer(type):
    """ Allows only lower case names as class attributes! """

    def __new__(self, name, bases, attrs): 
        for name in attrs:
            if name.lower() != name:
                raise TypeError(f"Inappropriate method name: name")
            
        # return type(name, bases, attrs)                    # 1.
        # return super().__new__(self, name, bases, attrs)   # 2.

    class Super(metaclass=LowerCaseEnforcer):
        pass
    
    class Sub(Super):
        
        def some_method(self):
            pass
    
        ## this will error in case 2 but not case 1
        def Another_method(self):
            pass
    预期行为:元类绑定到超类,而不是子类 将超类/和/子类绑定到元类; ?

如果有人能缓慢而友好地解释上述示例中到底发生了什么,我将不胜感激! :D

【问题讨论】:

【参考方案1】:

它也比你得到的更简单。

正如您所指出的,正确的做法是您上面的2

return super().__new__(self, name, bases, attrs)    # 2.

在这里:__new__ 是一种特殊的方法 - 尽管在某些文档中,甚至在官方文档的一部分中,它被描述为 classmethod,但并非如此:它作为一个对象,行为更像是一个静态方法——从某种意义上说,当调用MyClass.__new__() 时,Python 不会自动填充第一个参数——也就是说,你必须调用MyClass.__new__(MyClass) 才能使其工作。 (我在这里退了一步 - 此信息适用于所有类:元类和普通类)。

当您调用MyClass() 创建新实例时,Python 将调用MyClass.__new__ 并插入cls 参数作为第一个参数。

对于元类,创建元类新实例的调用由class 语句及其类主体的执行触发。同样,Python 将第一个参数填充到Metaclass.__new__,传递元类本身。

当您从元类'__new__ 中调用super().__new__ 时,您与手动调用__new__ 的情况相同:必须明确填写指定__new__ 应应用哪个类的参数。

现在,让您感到困惑的是,您将__new__ 的第一个参数写为self - 如果它是元类的实例(即普通类),这将是正确的。实际上,该参数是对元类本身的引用。

文档没有通知官方或推荐元类的第一个参数的名称__new__,但通常它与mcls, mcs, metaclass, metacls 类似 - 使其与cls 不同,后者是通常的名称非元类__new__ 方法的第一个参数。在元类中,“类” - cls 是通过最终调用 type.__new__(硬编码或使用 super())创建的,它的返回是新生类(可以进一步修改在调用超类之后的__new__ 方法中) - 当返回时,对__init__ 的调用正常进行。

所以,我将进一步评论尝试调用 type(name, bases, namespace) 而不是 type.__new__(mcls, name, bases, namespace) 的用法:第一种形式将创建一个普通类,就好像根本没有使用元类一样 - (在修改命名空间或基础的元类__new__,当然有它们的效果。但是生成的类将有type作为它的元类,它的子类根本不会调用元类。(为了记录,它作为“类前装饰器”工作 - 它可以在创建类参数之前对其进行操作,它甚至可以是一个普通函数,而不是具有 __new__ 方法的类 - 对 type 的调用将是什么毕竟创建新类)

检查元类是否“绑定”到您的类的一种简单方法是使用 type(MyClass)MyClass.__class__ 检查其类型。

【讨论】:

感谢您的回答!终于弄清楚问题出在哪里,在这里总结一下:***.com/a/67297367/6455731【参考方案2】:

我想我终于明白了(有点),我最初的困惑主要是因为我没有意识到这一点

    object.__new__type.__new__ 之间存在差异 从元类返回 type() 和返回 super().__new__ 之间存在区别

对这两点的讨论应该可以弄清楚我最初的例子以及看似神秘的继承行为。

1. object.__new__type.__new__的区别

首先谈谈__new__。 documenation 对此很清楚,但我仍然想补充和/或强调一些事情:

__new__ 可以理解为一种特殊的大小写静态方法,它将cls 作为第一个参数,并将其余参数(最常见的是*args, **kwargs)传递给__init__。 _ __new____init__ 被连续调用(实际上是由元类的__call__!),其中__init__ 只有在__new__ 返回cls 的实例时才会被调用。 __new__ 采用单个 参数(这是关于调用 __new__,而不是定义/重载它)即 cls 并返回该类的实例。

一开始我没有想到的重要一点是object.__new__type.__new__ 之间存在差异。 我在玩__new__ 的参数/参数时发现了这一点;看看这些“指导性错误”:

class ObjNewExample:
    
    def __new__(cls, *args, **kwargs):
        # return super().__new__(cls)                      # correct 
        return super().__new__(cls, *args, **kwargs)       # instructive error

    def __init__(self, some_attr):
        self._some_attr = some_attr

one = ObjNewExample(42)

class TypeNewExample(type):
    
    def __new__(mcls, name, bases, attrs):
        # return super().__new__(mcls, name, bases, attrs)  # correct
        return super().__new__(mcls)                        # instructive error

# class Cls(metaclass=TypeNewExample):
#     pass

带有return super().__new__(cls, *args, **kwargs) 的ObjNewexample 会抛出类似

TypeError: object.__new__() takes exactly one argument (the type to instantiate), 而带有return super().__new__(mcls) 的 TypeNewexample 则抛出 TypeError: type.__new__() takes exactly 3 arguments, 这表明object.__new__type.__new__ 是完全不同的方法!

另外:考虑__new__参数和实参的区别:

object.__new__ 接受 cls, *args, **kwargs 作为参数,但只需要 cls 作为参数(*args, **kwargs 被传递给 __init__type.__new__mcls, name, bases, attrs 作为参数参数

从元类返回type() 和返回super().__new__ 的区别

然而,我最初发布的示例的主要问题是返回 type() 和从元类的 __new__ 返回 super().__new__ 之间的区别(现在这很明显..)。 (另见this discussion)

mcls返回type(name, bases, attrs):创建type的实例 从mcls返回super().__new__(mcls, name, bases, attrs):创建实际元类的实例(派生自type), 这也解释了为什么 __init__ 在案例 1 中被禁止,而不是在初始示例的案例 2 中! (请记住:如果__new____new__ 的第一个参数即(m)cls 的情况下返回除实例之外的任何内容,则不会调用__init__

这应该是有启发性的:

class Meta(type):
    def __new__(mcls, name, bases, attrs):
        
        # this creates an intance of type (the default metaclass)
        # This should be eql to super().__new__(type, name, base, attrs)!
        obj1 = type(name, bases, attrs) 
        
        # this creates an instance of the actual custom metaclass (which is derived from type)
        # i.e. it creates an instance of type.__new__'s first arg
        obj2 = super().__new__(mcls, name, bases, attrs)

        print(isinstance(obj1, mcls))
        print(obj1.__class__)
        print(isinstance(obj2, mcls))
        print(obj2.__class__)
        
class Fun(metaclass=Meta):
    pass

从我最初的帖子中快速浏览案例 1-5:

1:返回一个新的type对象,即type的实例,而不是实际的自定义元类(派生自type),因此自定义元类的__init__被禁止;这似乎实际上等同于案例 4!

2:正如@jsbueno 指出的那样,这是最可能的预期(“正确”)行为:这会创建实际自定义元类的实例。

3:因为 type.__new__ 需要一个类型为 type 的对象(要实例化的对象)作为第一个参数,所以这个 barfs 本身就是一个参数

4:见案例 1

5:self(最好命名为“cls”或“mcls”)是Meta;在自己的构造函数中调用一个类显然是递归的。

上面还为我最初的帖子中第二个 sn-p 的看似奇怪的继承行为提供了解释! 那么为什么SubAnother_method的定义在LowerCaseEnforcer的case 2中会出错,而在case 1中却没有呢?

因为情况 1 Lowercaseenforcer 返回 type 的实例(不是 LowerCaseEnforcer!),所以 Super 的类型是 type(它的元类是 type,而不是 LowerCaseEnforcer)!因此,虽然LowerCaseEnforcer.__new__ 触发并强制Super 使用小写限制,但Super 只是type 类型的香草类,Sub 是从它派生的(没有特殊效果)。

而在案例 2 中,Super 的元类是 LowerCaseEnforcer 类型,Sub 也是如此,所以 LowerCaseEnforcer.__new__ 参与了 Sub 的定义。

但仍有一点不清楚的是静态方法在超级调用中的行为(另请参阅this discussion)。 例如。为什么super().__new__(cls) 有效?这不应该是super(cls, cls).__new__(cls)(或类似的东西)吗? 但我想这是另一个(有趣的)话题! :D

【讨论】:

以上是关于元类中 __new__ 的行为(也在继承的上下文中)的主要内容,如果未能解决你的问题,请参考以下文章

Python 元类行为(不调用 __new__),有解释吗?

元类中的属性()设置器问题

__init__和__new__

如何在我的元类中获取类的父类?

具有自定义元类行为的 Python 元类

在继承元类的类中调用 super 会引发错误