python-3.x 使用正确的索引和切片从`tuple`继承/子类

ctzwtxfj  于 2023-05-30  发布在  Python
关注(0)|答案(3)|浏览(287)

我正在尝试subclass tuple并在此过程中使mypy快乐。我想使切片和索引工作。
以下是我尝试过的:

from functools import singledispatchmethod
from typing import Iterable

class MyIntTuple(tuple[int, ...]):
    def __new__(cls, iterable: Iterable[int]) -> "MyIntTuple":
        return super().__new__(cls, iterable)  # type: ignore

    @singledispatchmethod
    def __getitem__(self, value: slice, /) -> "MyIntTuple":
        return MyIntTuple(super().__getitem__(value))

    @__getitem__.register
    def __getitem__(self, value: int, /) -> int:
        return super().__getitem__(value)

这几乎可以工作,除了切片:

a = MyIntTuple(range(12))
print(a)        # (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
print(a[3])     # 3 
print(a[3:6])   # (3, 4, 5)
b = a[5:8]
print(type(a))  # <class '__main__.MyIntTuple'>
print(type(b))  # <class 'tuple'>

这里的问题是切片应该返回MyIntTuple的示例。好像搞错了。
mypy对此也不满意:

error: Name "__getitem__" already defined on line 488  [no-redef]

tuple中子元素为

Superclass:
    @overload
    def __getitem__(self, SupportsIndex, /) -> int
    @overload
    def __getitem__(self, slice, /) -> Tuple[int, ...]

我如何正确地超载那些?functools.singledispatchmethod是不是走错路了?还是我用错了?

ojsjcaue

ojsjcaue1#

你需要给予每个实现一个不同于“main”函数的名字:

@__getitem__.register
def _(self, value: int, /) -> int:
    return super().__getitem__(value)

此外,切片的实现应该与“main”函数(它被用作无法识别的参数类型的后备)分开,int实现应该真正接受任何支持__index__的东西。
由于MyIntTuple尚未定义,因此必须为切片实现显式地向register传递类型,因此它不必查看注解。此外,您可以删除您的__new__实现-继承的实现将正常工作:

from functools import singledispatchmethod
from typing import SupportsIndex

class MyIntTuple(tuple[int, ...]):
    @singledispatchmethod
    def __getitem__(self, arg, /):
        raise TypeError("Can't index MyIntTuple with argument of type {!r}".format(type(arg)))

    @__getitem__.register(slice)
    def _(self, value: slice, /) -> "MyIntTuple":
        return MyIntTuple(super().__getitem__(value))

    @__getitem__.register
    def _(self, value: SupportsIndex, /) -> int:
        return super().__getitem__(value)
v09wglhw

v09wglhw2#

overload在签名mypy中呈现给你的并不是一些抽象的东西,而是对来自typing的decorator的引用。
下面是匹配超类型定义的方法:

from typing import Iterable, SupportsIndex, overload
from typing_extensions import reveal_type

class MyIntTuple(tuple[int, ...]):
    # Your main problem was here: `int` is much stricter than `SupportsIndex`
    @overload
    def __getitem__(self, value: SupportsIndex, /) -> int: ...
    @overload
    def __getitem__(self, value: slice, /) -> "MyIntTuple": ...
    
    def __getitem__(self, value: SupportsIndex | slice, /) -> "MyIntTuple | int":
        if isinstance(value, slice):
            return MyIntTuple(super().__getitem__(value))

        return super().__getitem__(value)

t = MyIntTuple((1,2,3))
reveal_type(t)  # N: Revealed type is "__main__.MyIntTuple"
reveal_type(t[0])  # N: Revealed type is "builtins.int"
reveal_type(t[1:])  # N: Revealed type is "__main__.MyIntTuple"

# And here's why "SupportsIndex"
class MySomething:
    def __index__(self) -> int:
        return 1

reveal_type(t[MySomething()])  # N: Revealed type is "builtins.int"

上面的代码片段显示了使用int的一个问题:这个限制应该被削弱,因为更窄的参数类型违反了LSP。实际的tuple可以使用slice或任何定义了__index__ dunder方法的东西,而不仅仅是__getitem__中的int
有了overloadSupportsIndex,您几乎已经准备好了。您需要的唯一更改是交换重载顺序:mypy强制执行相同的重载顺序,SequenceSupportsIndex签名放在第一位(尝试将它们交换回来并观察错误)。在这种情况下,这并不重要,但通常不同的重载顺序可能会更改类型解析。重载按定义顺序尝试,最上面的重载获胜。如果签名重叠,通常您会看到一个额外的mypy错误-但无论如何最好保持重载的顺序。下面是逆序会产生的结果:

main.py:6: error: Signature of "__getitem__" incompatible with supertype "tuple"  [override]
main.py:6: note: Overload variants must be defined in the same order as they are in "tuple"

上面的代码typechecks--strict

mwg9r5ms

mwg9r5ms3#

singledispatchmethod有问题。

from __future__ import annotations
from functools import singledispatchmethod
from typing import Iterable, Sequence

class MyIntTuple(tuple[int, ...]):
    def __new__(cls, iterable: Iterable[int]) -> MyIntTuple:
        return super().__new__(cls, iterable)  # type: ignore

    @singledispatchmethod
    def __getitem__(self) -> None:
        raise NotImplementedError

    @__getitem__.register
    def _(self, value: slice, /) -> MyIntTuple:
        return MyIntTuple(super().__getitem__(value))

    @__getitem__.register
    def _(self, value: int, /) -> int:
        return super().__getitem__(value)
a = MyIntTuple(range(12))
print(a)        # (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
print(a[3])     # 3 
print(a[3:6])   # (3, 4, 5)
b = a[5:8]
print(type(a))  # <class '__main__.MyIntTuple'>
print(type(b))  # <class '__main__.MyIntTuple'>

相关问题