【问题标题】:Instantiate a child in __new__ with different __new__ signature for a child在 __new__ 中实例化一个具有不同 __new__ 签名的孩子
【发布时间】:2019-11-26 11:08:53
【问题描述】:

前言

我想要 2 个类 IntervalSegment 具有以下属性:

  1. Interval 可以有 startend 点,它们中的任何一个都可以包含/排除(我已经使用必需的标志参数实现了这一点,例如 start_inclusive/end_inclusive)。
  2. Segment 是一个包含两个端点的 Interval,因此用户不需要指定这些标志。
  3. 如果用户尝试创建包含端点的Interval,他会得到Segment 之类的

    >>> Interval(0, 1, start_inclusive=True, end_inclusive=True)
    Segment(0, 1)
    

    (this doesn't look impossible)

问题

到目前为止,我的MCVE 实现是

Interval类:

class Interval:
    def __new__(cls, start: int, end: int,
                *,
                start_inclusive: bool,
                end_inclusive: bool) -> 'Interval':
        if cls is not __class__:
            return super().__new__(cls)
        if start == end:
            raise ValueError('Degenerate interval found.')
        if start_inclusive and end_inclusive:
            return Segment(start, end)
        return super().__new__(cls)

    def __init__(self,
                 start: int,
                 end: int,
                 *,
                 start_inclusive: bool,
                 end_inclusive: bool) -> None:
        self.start = start
        self.end = end
        self.start_inclusive = start_inclusive
        self.end_inclusive = end_inclusive

Segment类:

class Segment(Interval):
    def __new__(cls, start: int, end: int) -> 'Interval':
        return super().__new__(cls, start, end,
                               start_inclusive=True,
                               end_inclusive=True)

    def __init__(self, start: int, end: int) -> None:
        super().__init__(start, end,
                         start_inclusive=True,
                         end_inclusive=True)

创作有点效果

>>> Interval(0, 1, start_inclusive=False, end_inclusive=True)
<__main__.Interval object at ...>
>>> Interval(0, 1, start_inclusive=False, end_inclusive=False)
<__main__.Interval object at ...>
>>> Segment(0, 1)
<__main__.Segment object at ...>

但是

>>> Interval(0, 1, start_inclusive=True, end_inclusive=True)

未能关注TypeError

Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'end_inclusive'

所以我的问题是:

是否有任何惯用的方式在父级的 __new__ 中实例化子类,其中一些参数为 __new____init__ “绑定”子级?

【问题讨论】:

  • 您是否有理由不希望用户只选择他们想要的类?另外,您在区间中没有任何特殊功能吗?
  • @MadPhysicist:对这两个问题都是肯定的,检查点是否在段中会更快(我已经测量过了,是的,由于检查和间隔的数量,这个小改进确实很重要)因为我在检查端点时不需要找出使用哪个运算符&lt;&lt;=,因为它将是&lt;=,但从我的角度来看问题更普遍
  • Segment 和 Interval 似乎完全不同的想法,即使它们具有共同的功能。如果它们可以共享一个通用的抽象基类,而不是在层次结构中排列,那么两个想法都会更好地表达,并且您的实例化会更简单?
  • 这个设计在我看来很可疑。你到底想用代码的if cls is not __class__: 部分做什么?
  • 基本上,当__new__ 返回Segment 时,Segment 仍然是Interval 的子类,因此它会尝试在结果上调用type(obj).__init__(obj)。但是obj 在其__init__ 中缺少两个关键字参数。

标签: python python-3.x oop inheritance


【解决方案1】:

让我们先看看为什么会出现错误。当您调用从object 派生的类时,将调用metaclass (type) 的__call__ 方法。这通常是这样的

self = cls.__new__(...)
if isinstance(self, cls):
    type(self).__init__(self)

这只是大概的,但足以传达这里发生的事情:

  1. type.__call__ 致电Interval.__new__
  2. 由于start_inclusive and end_inclusiveInterval.__new__正确返回了Segment的实例
  3. 由于issubclass(Segment, Interval)type.__call__ 调用Segment.__init__ 使用您传递给Interval 调用的所有参数
  4. Segment.__init__ 不接受任何关键字参数,并引发您看到的错误。

对于这种情况有多种解决方法。 @jdehesa's answer 展示了如何覆盖 type 的行为,以便 type.__call__ 检查 type(obj) is cls 而不是使用 isinstance

另一种选择是分离IntervalSegment 的层次结构。你可以做类似的事情

class MyBase:
    # put common functionality here

class Interval(MyBase):
    # __new__ and __init__ same as before

class Segment(MyBase):
    # __new__ and __init__ same as before

在这种安排下,isinstance(Segment(...), Interval) 将变为 False,而type.__call__不会尝试在 Segment 上调用 Interval.__init__

在我看来,最简单的方法是使用工厂模式。有一个外部函数,它根据输入确定要返回的对象类型。这样,你根本不需要实现__new__,你的类构建过程会简单很多:

def factory(start, end, *, start_inclusive, end_inclusive):
    if start_inclusive and end_inclusive:
        return Segment(start, end)
    return Interval(start, end, start_inclusive=start_inclusive, end_inclusive=end_inclusive)

【讨论】:

  • 我认为这个问题本身很有趣,但是是的,我肯定会为此与工厂合作,称之为interval(小写)。如果你能以某种方式将实际类的构造函数设为“私有”,这样你就不会犯直接调用构造函数的错误,但我不确定在 Python 中是否有任何合理的方法可以做到这一点(因为我不要考虑从构造函数中检查堆栈跟踪以模拟 C++ friend 行为“合理”)。
  • @jdehesa。我发现好的文档和同意的成年人通常最终会解决其他语言使用隐私的问题。正如我之前告诉 OP 的,我不喜欢限制选项,即使我最终没有使用它们。有一天,我可能想明确地创建一个包含两个端点的Interval
  • 我最终使用了工厂(并将所有验证委托给它们)并认为这种棘手的 __new__ 行为是一种黑客行为,可以使用元类来强制执行,我真的不需要它们,在至少在当前状态
  • @AzatIbrakov。我很高兴你选择了明智的解决方案。为什么在不必要的时候让你的代码无法维护?
【解决方案2】:

__init____new__ 之后被调用时,您可以使用元类来解决这个问题:

class IntervalMeta(type):
    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        # Only call __init__ if class of object is exactly this class
        if type(obj) is cls:
            cls.__init__(obj, *args, **kwargs)
        # As opposed to default behaviour:
        # if isinstance(obj, cls):
        #     type(obj).__init__(obj, *args, **kwargs)
        return obj

# Code below does not change except for metaclass
class Interval(metaclass=IntervalMeta):
    def __new__(cls, start: int, end: int,
                *,
                start_inclusive: bool,
                end_inclusive: bool) -> 'Interval':
        if cls is not __class__:
            return super().__new__(cls)
        if start == end:
            raise ValueError('Degenerate interval found.')
        if start_inclusive and end_inclusive:
            return Segment(start, end)
        return super().__new__(cls)

    def __init__(self,
                 start: int,
                 end: int,
                 *,
                 start_inclusive: bool,
                 end_inclusive: bool) -> None:
        self.start = start
        self.end = end
        self.start_inclusive = start_inclusive
        self.end_inclusive = end_inclusive

class Segment(Interval):
    def __new__(cls, start: int, end: int) -> 'Interval':
        return super().__new__(cls, start, end,
                               start_inclusive=True,
                               end_inclusive=True)

    def __init__(self, start: int, end: int) -> None:
        super().__init__(start, end,
                         start_inclusive=True,
                         end_inclusive=True)

print(Interval(0, 1, start_inclusive=True, end_inclusive=True))
# <__main__.Segment object at ...>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-17
    • 1970-01-01
    相关资源
    最近更新 更多