Python新整数类型的惯用递增方式是什么?

5

假设我按如下定义一个新类型:

import typing

Index = typing.NewType('Index', int)

假设我有一个名为Index的变量:

index = Index(0)

什么是增加“index”的惯用方式?
如果我这样做:
index += 1

这相当于

index = index + 1

从静态类型检查的角度来看,index 的类型会变成 int 而不是 Index:

$ mypy example.py

example.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "Index")

有什么比

更好的选择吗?

index = Index(index + 1)

如何保持Index类型?


2
你为什么要创建这样的类型,而不是定义 int 的子类并实现适当的 __add____iadd__ 方法呢? - cs95
2
@cs95 这是用于静态分析的,参见文档。有时候你需要一个新类型,它只是一个子类,以便于区分,例如一个具有特定角色的整数,但这并不会实际创建一个子类,但是,例如 mypy 将把它视为子类。 - juanpa.arrivillaga
没错。在这个例子中,我想要区分索引和普通整数。 - eepp
5
无论如何,我认为你无法避免使用Index(index+1)或等效代码。 - juanpa.arrivillaga
2个回答

2
你需要创建一个“真正的”int子类(这里没有双关语,但它足够糟糕,应该留在那里)。
这里有两个问题:
  • typing.NewType并不会创建一个新类。它只是将对象的“血统”分离,以便它们会“看起来”像一个新类对于静态类型检查工具 - 但是使用这样的类创建的对象仍然是运行时指定的类。
请在交互式提示中查看“typing.Newtype”的工作情况,而不是依赖于静态检查报告:
In [31]: import typing                                                                                                                                                              

In [32]: Index = typing.NewType("Index", int)                                                                                                                                       

In [33]: a = Index(5)                                                                                                                                                               

In [34]: type(a)                                                                                                                                                                    
Out[34]: int
  • 第二个问题是,即使您以正确的方式对int进行子类化,应用任何运算符产生的操作仍将将结果类型转换回int,而不会是创建的子类:
In [35]: class Index(int): pass                                                                                                                                                     

In [36]: a = Index(5)                                                                                                                                                               

In [37]: type(a)                                                                                                                                                                    
Out[37]: __main__.Index

In [38]: type(a + 1)                                                                                                                                                                
Out[38]: int

In [39]: type(a + a)                                                                                                                                                                
Out[39]: int

In [40]: a += 1                                                                                                                                                                     

In [41]: type(a)                                                                                                                                                                    
Out[41]: int

所以,唯一的解决方法是实际上将执行数值操作的所有魔术方法包装在“转换”结果回到子类的函数中。可以通过创建一个装饰器来执行此转换并将其应用于类主体中所有数字方法的for循环中,以避免在类主体中多次重复相同的模式。

In [68]: num_meths = ['__abs__', '__add__', '__and__',  '__ceil__', 
'__divmod__',  '__floor__', '__floordiv__', '__invert__', '__lshift__',
'__mod__', '__mul__', '__neg__', '__pos__', '__pow__', '__radd__', 
'__rand__', '__rdivmod__',  '__rfloordiv__', '__rlshift__', '__rmod__',
'__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', 
'__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__sub__', 
'__truediv__', '__trunc__', '__xor__',  'from_bytes'] 
# to get these, I did `print(dir(int))` copy-pasted the result, and 
# deleted all non-relevant methods to this case, leaving only the ones 
# that perform operations which should preserve the subclass


In [70]: class Index(int): 
             for meth in num_meths: 
                locals()[meth] = (lambda meth:
                    lambda self, *args:
                         __class__(getattr(int, meth)(self, *args))
                    )(meth) 
# creating another "lambda" layer to pass the value of meth in _each_
# iteration of the for loop is needed so that these values are "frozen" 
# for each created method

In [71]: a = Index(5)                                                                                                                                                               

In [72]: type(a)                                                                                                                                                                    
Out[72]: __main__.Index

In [73]: type(a + 1)                                                                                                                                                                
Out[73]: __main__.Index

In [74]: a += 1                                                                                                                                                                     

In [75]: type(a)                                                                                                                                                                    
Out[75]: __main__.Index


这实际上是可行的。

但是,如果意图是让静态类型检查“看到”这种包装的发生,那么你又偏离了轨道。 静态类型检查器不会理解在类体内循环应用装饰器创建方法。

换句话说,我认为唯一的出路就是将自动应用的转换复制并粘贴到所有相关的数字方法中,然后在此过程中创建注释:

from __future__ import annotations
from typing import Union

class Index(int):
    def __add__(self: Index, other: Union[Index, int]) -> Index:
        return __class__(super().__add__(other))
    def __radd__(self: Index, other: Union[Index, int]) -> Index:
        return __class__(super().__radd__(other))
    # Rinse and repeat for all numeric methods you intend to use

1
我认为你将无法在 NewType 变量上执行操作的结果中隐式地保留类型,因为正如 文档 所说:

您仍然可以对类型为 UserId 的变量执行所有 int 操作,但结果将始终是 int 类型

因此,任何变量上的操作都会产生基础类型的结果。如果您想要结果是 NewType,则需要明确指定,例如 index = Index(index + 1)。这里的 Index 作为转换函数。
这与 NewType 的目的一致:

静态类型检查器将把新类型视为原始类型的子类。这有助于捕获逻辑错误

NewType 的预期用途是帮助您检测意外混合旧基础类型和新派生类型的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接