用不同的子级的\u优new_uu新签名实例化一个子级

2024-05-18 06:11:45 发布

您现在位置:Python中文网/ 问答频道 /正文

前言

我希望有两个类IntervalSegment具有以下属性:

  1. Interval可以有start&;end点,它们中的任何一个都可以被包含/排除(我已经使用所需的标志参数,如start_inclusive/end_inclusive)来实现这一点。在
  2. Segment是一个包含两个端点的{},因此用户不需要指定这些标志。在
  3. 如果用户试图创建包含端点的Interval,他将得到一个Segment

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

    this doesn't look impossible

问题

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

Interval类:

^{pr2}$

Segment类:

^{3}$

创造有点管用

>>> 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__“绑定”的子类?


Tags: 用户falsetrueobjectmain标志segmentinclusive
2条回答

让我们先看看为什么会出现错误。当您调用从^{}派生的类时,metaclass^{})的^{}方法被调用。通常是这样的

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

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

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

对于这种情况有很多解决办法。@jdehesa's answer演示如何重写{}的行为,以便type.__call__检查{},而不是使用isinstance。在

另一种选择是分离Interval和{}的层次结构。你可以做些像

^{pr2}$

在这种安排下,isinstance(Segment(...), Interval)将是False,而{}将尝试在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)

当在__new__之后调用__init__时,可以使用一个要自定义的元类来解决这个问题:

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 ...>

相关问题 更多 >