为什么 object.__new__ 接受参数?

Pae*_*els 5 python metaclass instantiation object-construction python-3.x

除了“再次”询问有关Python__new____init__Python 的明显问题之外,我可以保证,我知道它的作用。我将展示一些奇怪的、在我看来没有记录的行为,为此我寻求专业帮助:)。


背景

__slots__我正在使用用户定义的元类(称为 )实现多个功能,例如抽象方法、抽象类、必须重写的方法、单调行为、槽类(自动推断)和混合类(延迟槽) ExtendedType。以下代码可以在开发分支的pyTooling/pyTooling中找到。

因此,提出的问题是一个精简和简化的变体,展示了 的奇怪行为object.__new__

主意

根据 的内部算法ExtendedType,它可能会决定一个类A抽象的。如果是这样,该__new__方法将被替换为引发异常的虚拟方法 ( AbstractClassError)。稍后,当一个类B(A)继承自 时A,元类可能会做出决定,B不再是抽象的,因此我们希望允许再次创建对象并允许调用原始__new__方法。因此,原始方法被保留为类中的字段。

为了简化抽象性决策的内部算法,元类实现了一个布尔命名参数abstract

class AbstractClassError(Exception):
    pass

class M(type):
  # staticmethod
  def __new__(cls,  className, baseClasses, members, abstract):
    newClass = type.__new__(cls, className, baseClasses, members)
        
    if abstract:
      def newnew(cls, *_, **__):
        raise AbstractClassError(f"Class is abstract")
            
      # keep original __new__ and exchange it with a dummy method throwing an error
      newClass.__new_orig__ = newClass.__new__
      newClass.__new__ = newnew
    else:
      # 1. replacing __new__ with original (preserved) method doesn't work
      newClass.__new__ = newClass.__new_orig__

    return newClass


class A(metaclass=M, abstract=True):
  pass


class B(A, abstract=False):
  def __init__(self, arg):
    self.arg = arg

b = B(5)
Run Code Online (Sandbox Code Playgroud)

实例化时B我们会尝试两种情况:

  1. 使用单个参数:b = B(5)
    错误消息:
    TypeError: object.__new__() takes exactly one argument (the type to instantiate)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 不带参数:b = B()
    错误信息:
    TypeError: B.__init__() missing 1 required positional argument: 'arg'
    
    Run Code Online (Sandbox Code Playgroud)

后一种情况的错误消息是预期的,因为__init__需要B一个参数arg。奇怪的行为是在情况 1 中,它报告object.__new__()除了类型之外不接受其他参数。

因此,让我们研究一下交换方法是否正常工作:

TypeError: object.__new__() takes exactly one argument (the type to instantiate)
Run Code Online (Sandbox Code Playgroud)

结果:

object.__new__  <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
A.__new_orig__  <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
A.__new__       <function M.__new__.<locals>.newnew at 0x000001CF11AE5A80>
B.__new__       <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
Run Code Online (Sandbox Code Playgroud)

因此,保留的方法与类中的方法__new_orig__相同,并且在交换回类中的方法object.__new__后再次相同。__new__B

与普通班级比较

让我们看两个类XY(X)实例化它们:

TypeError: B.__init__() missing 1 required positional argument: 'arg'
Run Code Online (Sandbox Code Playgroud)

这当然可行,但是__new__方法不同吗?

print("object.__new__ ", object.__new__)
print("A.__new_orig__ ", A.__new_orig__)
print("A.__new__      ", A.__new__)
print("B.__new__      ", B.__new__)
Run Code Online (Sandbox Code Playgroud)

X可以Y使用与或相同的__new__方法。Bobject

因此,让我们实例化YandB并比较结果:

object.__new__  <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
A.__new_orig__  <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
A.__new__       <function M.__new__.<locals>.newnew at 0x000001CF11AE5A80>
B.__new__       <built-in method __new__ of type object at 0x00007FFE30EDD0C0>
Run Code Online (Sandbox Code Playgroud)

结果:

Y.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
y.arg           3
B.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
Traceback (most recent call last):
  File "C:\Temp\newIstKomisch.py", line 67, in <module>
    b = B(5)
        ^^^^
TypeError: object.__new__() takes exactly one argument (the type to instantiate)
Run Code Online (Sandbox Code Playgroud)

问题1:为什么new接受Y的参数,而不接受B的参数?

创建对象

当一个对象被创建时,__call__会执行元类的方法,大致翻译为:

class X:
    pass


class Y(X):
    def __init__(self, arg):
        self.arg = arg

y = Y(3)
Run Code Online (Sandbox Code Playgroud)

它首先调用__new__创建实例,然后调用__init__初始化对象。有人可能会争论并说:“也许在调用中有神奇的行为”来检查是否调用了内置方法或用户定义的方法”......

让我们快速检查一下object.__new__行为方式:

object.__new__  <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
A.__new_orig__  <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
A.__new__       <function M.__new__.<locals>.newnew at 0x000001CD1FB459E0>
B.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
X.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
Y.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
Run Code Online (Sandbox Code Playgroud)

结果:

TypeError: object() takes no arguments
Run Code Online (Sandbox Code Playgroud)

观察:错误消息与我们之前得到的不同。这表示“没有参数”,另一个表示“只有一个参数”。

或者,我们可以跳过元类来手动创建一个对象:

print("Y.__new__      ", Y.__new__)
y = Y(3)
print("y.arg          ", y.arg)

print("B.__new__      ", B.__new__)
b = B(5)
print("b.arg          ", y.arg)
Run Code Online (Sandbox Code Playgroud)

结果:

Y.__new__(Y, 3)  <__main__.Y object at 0x0000020ED770BD40>
y.__init__(3)    3
Run Code Online (Sandbox Code Playgroud)

在这里我们清楚地看到__new__可以接受额外的参数并忽略它们。

因此,让我们与手动实例创建进行比较B

Y.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
y.arg           3
B.__new__       <built-in method __new__ of type object at 0x00007FFE3B61D0C0>
Traceback (most recent call last):
  File "C:\Temp\newIstKomisch.py", line 67, in <module>
    b = B(5)
        ^^^^
TypeError: object.__new__() takes exactly one argument (the type to instantiate)
Run Code Online (Sandbox Code Playgroud)

结果:

Traceback (most recent call last):
  File "C:\Temp\newIstKomisch.py", line 51, in <module>
    b = B.__new__(B, 5)
        ^^^^^^^^^^^^^^^
TypeError: object.__new__() takes exactly one argument (the type to instantiate)
Run Code Online (Sandbox Code Playgroud)

问题2:同一个方法怎么会有不同的行为和异常处理?


补充笔记:

  • 所有行为都在方法中实现M.__new__或交换XXX.__new__方法而不是M.__call__,因此对象创建时间不受影响。修改元类调用会对性能产生巨大影响。

附件:

jsb*_*eno 3

对于一个问题肯定需要进行大量研究。

但答案更简单: objects__new____init__简单的特殊情况“额外参数的宽恕”,以一种使用自定义__init__方法创建新类感觉很自然的方式,而无需摆弄__new__.

所以,简而言之,objectnew 检查它实例化的类是否有自定义__init__和没有自定义__new__- 如果是这样,它“原谅”额外的 args 和 kwargs。对象的默认值则__init__相反:它检查它正在“初始化”的类是否有 custom__new__和没有 custom __init__。如果是这样,它也会原谅(并忘记)任何额外的参数。这里的“自定义”验证只是检查-中__new__的任何类的字典__mro__中是否存在方法,因此即使object.__new__在子类中设置相同的类也不起作用。

这种听起来奇怪的特殊情况是必要的,并且在 Python 中已经存在很长时间了,因为如果没有它,每当创建一个带有__init__参数的方法的类时,如果不实现__new__也会失败的方法 - 所以“简化”的情况通过使用更简单的__init__方法而不是修改来进行类定制”__new__是没有意义的。

以下是 REPL 上的一些示例,可以清楚地说明这一点:


In [11]: class A(object): pass

In [12]: b = A.__new__(A, 3)

TypeError (...)              

TypeError: A() takes no arguments
# There is no custom `__init__`, so it fails

In [13]: class A(object):
    ...:     def __init__(self, *args):
    ...:         pass
    ...: 

In [14]: b = A.__new__(A, 3)
# There was a custom `__init__` so, object.__new__ forgives us. 

# and finally your case, both a __new__ and __init__ even if `cls.__new__ is object.__new__` is true, errors:

In [17]: class A(object):
    ...:     def __new__(cls, *args):
    ...:         raise NotImplementedError()
    ...:     def __init__(self, *args):
    ...:         pass
    ...: 

In [18]: class B(A):
    ...:     __new__ = object.__new__
    ...: 

In [19]: c = B() #<- works with no args

In [20]: c = B(3)

TypeError              

TypeError: object.__new__() takes exactly one argument (the type to instantiate)

# And just one example with __init__ to show the converse case:

In [28]: class A(object):
    ...:     def __new__(cls, *args):
    ...:         # I strip down the extra args:
    ...:         return super().__new__(cls)
    ...:     # no custom __init__
    ...: 

In [29]: b = A(3)  # <- works

In [30]: class A(object):
    ...:     def __new__(cls, *args):
    ...:         # I strip down the extra args:
    ...:         return super().__new__(cls)
    ...:     # with a custom __init__ forwarding extra args
    ...:     def __init__(self, *args):
    ...:         print("init")
    ...:         super().__init__(*args)
    ...: 

In [31]: b = A(3)  # <- errors out
init

TypeError  (...)
Cell In[30], line 8, in A.__init__(self, *args)
      6 def __init__(self, *args):
      7     print("init")
----> 8     super().__init__(*args)

TypeError: object.__init__() takes exactly one argument (the instance to initialize)


Run Code Online (Sandbox Code Playgroud)

最后但并非最不重要:

对于您的情况,您不能简单地object.__new__在自定义“孙女”类中恢复 - 您将需要检查是否__orig_new__object.__new__,如果是,则使用__new__在调用之前删除额外参数的自定义object.__new__