实例化__new__中具有不同__new__签名的孩子

Aza*_*kov 6 python oop inheritance python-3.x

前言

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

  1. Interval可以有startend点,其中的任何一个都可以包含/排除(我已经使用必需的标志参数(例如start_inclusive/ end_inclusive)实现了此功能)。
  2. SegmentInterval带有两个端点的,因此用户无需指定这些标志。
  3. 如果用户尝试Interval使用包含的端点进行创建,他将获得一个Segment

    >>> Interval(0, 1, start_inclusive=True, end_inclusive=True)
    Segment(0, 1)
    
    Run Code Online (Sandbox Code Playgroud)

    这似乎不是不可能的

问题

到目前为止,我的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
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

创作有点作品

>>> 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 ...>
Run Code Online (Sandbox Code Playgroud)

>>> Interval(0, 1, start_inclusive=True, end_inclusive=True)
Run Code Online (Sandbox Code Playgroud)

跟随失败 TypeError

>>> Interval(0, 1, start_inclusive=True, end_inclusive=True)
Segment(0, 1)
Run Code Online (Sandbox Code Playgroud)

所以我的问题是:

是否有父母的孩子实例类的任何惯用的方式__new__与一些参数__new____init__一个孩子“约束”?

Mad*_*ist 3

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

self = cls.__new__(...)
if isinstance(self, cls):
    type(self).__init__(self)
Run Code Online (Sandbox Code Playgroud)

这只是近似值,但足以传达这里发生的情况:

  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 的答案显示了如何覆盖 的行为,type以便type.__call__检查type(obj) is cls而不是使用isinstance.

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

class MyBase:
    # put common functionality here

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

class Segment(MyBase):
    # __new__ and __init__ same as before
Run Code Online (Sandbox Code Playgroud)

isinstance(Segment(...), Interval)有了这样的安排False,就type.__call__不会试图去打电话Interval.__init__Segment

在我看来,最简单的方法是使用工厂模式。有一个外部函数可以根据输入确定返回什么类型的对象。这样,你根本不需要实现__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)
Run Code Online (Sandbox Code Playgroud)