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

时间:2019-07-17 14:58:10

标签: python python-3.x oop inheritance

前言

我想拥有两个具有以下属性的类IntervalSegment

  1. Interval可以具有startend点,可以包含/排除其中的任何一个点(我已经使用start_inclusive / {{ 1}})。
  2. end_inclusive是包含两个端点的Segment,因此用户无需指定这些标志。
  3. 如果用户尝试创建包含端点的Interval,他将得到一个Interval,如

    Segment

    this doesn't look impossible

问题

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

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

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__被孩子“绑定”?

2 个答案:

答案 0 :(得分:3)

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

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

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

  1. type.__call__呼叫Interval.__new__
  2. start_inclusive and end_inclusive起,Interval.__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__尝试在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)

答案 1 :(得分: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 ...>