如何正确地继承一个有__new__方法的父类?

16

假设我们有一个名为 'Parent' 的类,由于某种原因它定义了 __new__ 方法,还有一个继承自它的类 'Child'。 (在我的情况下,我试图继承一个无法修改的第三方类)

class Parent:
    def __new__(cls, arg):
        # ... something important is done here with arg

我的尝试是:

class Child(Parent):
    def __init__(self, myArg, argForSuperclass):
         Parent.__new__(argForSuperclass)
         self.field = myArg

但是,虽然

p = Parent("argForSuperclass")

按预期工作

c = Child("myArg", "argForSuperclass")

失败了,因为 'Child' 尝试调用从 'Parent' 继承的 __new__ 方法,而不是它自己的 __init__ 方法。

我需要在 'Child' 中做哪些更改才能获得期望的行为?


1
请参考以下相关内容:https://dev59.com/3XRB5IYBdhLWcg3wSVUI - charleyc
4个回答

14

首先,通常不建议覆盖 __new__ 来避免这些问题...但我知道这不是你的错。对于这种情况,在重写 __new__ 时最佳做法是使其接受可选参数...

class Parent(object):
    def __new__(cls, value, *args, **kwargs):
        print 'my value is', value
        return object.__new__(cls, *args, **kwargs)

...这样孩子们就能获得自己的:

class Child(Parent):
    def __init__(self, for_parent, my_stuff):
        self.my_stuff = my_stuff

那么,它就可以工作:

>>> c = Child(2, "Child name is Juju")
my value is 2
>>> c.my_stuff
'Child name is Juju'
然而,你的父类作者并不那么明智,他给了你这个问题:
class Parent(object):
    def __new__(cls, value):
        print 'my value is', value
        return object.__new__(cls)
在这种情况下,只需在子类中覆盖__new__,使其接受可选参数,并在那里调用父类的__new__:
class Child(Parent):
    def __new__(cls, value, *args, **kwargs):
        return Parent.__new__(cls, value)
    def __init__(self, for_parent, my_stuff):
        self.my_stuff = my_stuff

7
这句话的意思是“孩子正在为父母的(编程)错误付出代价”。 - smci

6

子类实例化时不会调用父类的__new__方法,而是会先调用自己继承自父类的__new__方法,然后再调用自己的__init__方法。

需要理解这些方法的作用以及Python何时调用它们。__new__方法用于创建一个实例对象,然后__init__方法用于初始化该实例的属性。Python将相同的参数传递给这两个方法:开始处理时传递给类的参数。

因此,在从具有__new__的类继承时,与从任何其他具有与父类不同签名的方法继承时所做的完全相同。您需要重写__new__方法来接收子类的参数,并使用其期望的参数列表调用Parent.__new__方法。然后您需要完全分别重写__init__方法(尽管具有相同的参数列表,并且需要使用其自己期望的参数列表调用Parent的__init__方法)。

然而,由于__new__方法用于自定义获取新实例的过程,可能会遇到两个潜在的困难。它不一定要实际创建一个新的实例(它可以找到已经存在的实例),事实上它甚至不必返回类的实例。这可能会导致以下问题:

  1. 如果__new__返回一个已存在的对象(比如Parent希望能够缓存其实例),那么你可能会发现__init__被调用在已经存在的对象上,这可能会非常糟糕,如果你的对象应该具有可变状态。但是,如果Parent希望能够做到这种事情,它可能会期望一堆约束条件来使用它,以任意方式打破这些约束条件(例如向一个期望不可变的类添加可变状态,以便可以缓存和回收其实例)几乎肯定不会起作用,即使你可以正确初始化你的对象。如果正在进行这种事情,并且没有任何文档告诉你应该如何子类化Parent,那么第三方很可能不打算让你子类化Parent,这将对你造成困难。不过,你最好尝试将所有初始化移动到__new__而不是__init__,尽管这很丑陋。

  2. Python只有在类的__new__方法实际上返回类的实例时才调用__init__方法。如果Parent正在使用它的__new__方法作为某种“工厂函数”来返回其他类的对象,那么直接子类化很可能会失败,除非你需要做的只是改变“工厂”的工作方式。


1

据我理解,当您调用Child("myarg", "otherarg")时,实际上意味着类似于这样:

c = Child.__new__(Child, "myarg", "otherarg")
if isinstance(c, Child):
    c.__init__("myarg", "otherarg")

你可以:
  1. 编写一个替代构造函数,例如 Child.create_with_extra_arg("myarg", "otherarg"),它在执行其他操作之前实例化 Child("otherarg")

  2. 覆盖 Child.__new__,类似于以下代码:

.

def __new__(cls, myarg, argforsuperclass):
    c = Parent.__new__(cls, argforsuperclass)
    c.field = myarg
    return c

我还没有测试过那个。重写__new__可能会很快变得混乱,所以最好尽可能避免使用它。


0
虽然brandizzi的答案对简单的例子很有效,但在我的实际用例中,我不得不深入挖掘一下:如何正确继承一个只使用__new__方法的类,并且Child类有自己的属性,并且在实际创建新对象之前(即在super().__new__之前)需要对给定数据执行一些操作/检查。
在我的情况下,这个问题出现在使用Shapely 2.0包时,尝试从shapely.geometry.Polygon类继承。在Shapely Github上有一个问题线程,并且有一个带有自定义Child属性的工作解决方案:https://github.com/shapely/shapely/issues/1233#issuecomment-977837620
from shapely.geometry import Point

class PropertyPoint(Point):
    _id_to_attrs = {}
    __slots__ = Point.__slots__  # slots must be the same for assigning __class__ - https://dev59.com/m67la4cB1Zd3GeqPXRv9#52140968

    def __init__(self, coord: Tuple[float, float], name: str):
        self._id_to_attrs[id(self)] = dict(name=name)

    def __new__(cls, coord: Tuple[float, float], name: str):
        point = super().__new__(cls, coord)
        point.__class__ = cls
        return point

    ...  # Irrelevant code 
 
   @property
    def name(self):
        return self._id_to_attrs[id(self)]['name']

    @name.setter
    def name(self, name):
        self._id_to_attrs[id(self)]['name'] = name

    ...  # Irrelevant code

对于任何遇到相同问题的人,我希望这个解决方案能为你节省一些时间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接