【问题标题】:What's the idiomatic way to increment a Python new integer type?增加 Python 新整数类型的惯用方法是什么?
【发布时间】:2020-08-08 01:30:42
【问题描述】:

假设我这样定义一个新类型:

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 类型?

【问题讨论】:

  • 为什么要创建这样的类型,而不是仅仅定义 int 的子类并实现适当的 __add____iadd__ 方法?
  • @cs95 用于静态分析,请参阅the documentation 有时您想要一个新类型,它只是一个子类,只是为了与众不同,例如一个具有特定作用的整数,但这实际上并没有创建子类,但是,例如,mypy 会将其视为子类。
  • 没错。在此示例中,我想将索引与一般整数区分开来。
  • 无论如何,我认为你不能避免 Index(index + 1) 或等价物

标签: python python-3.x python-typing type-annotation


【解决方案1】:

您必须“真正地”创建一个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

【讨论】:

    【解决方案2】:

    我认为由于对 NewType 变量的操作,您将无法隐式(间接)保留类型,因为正如 documentation 所说:

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

    因此,对变量的任何操作都会产生基本类型的结果。如果您希望结果为NewType,则需要明确指定它,例如作为index = Index(index + 1)。这里Index 用作强制转换函数。

    这与NewType的目的一致:

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

    NewType 的预期目的是帮助您检测意外将旧基类型和新派生类型混合在一起的情况。

    【讨论】:

      猜你喜欢
      • 2010-09-27
      • 1970-01-01
      • 2019-03-03
      • 2011-03-06
      • 2019-02-25
      • 1970-01-01
      • 1970-01-01
      • 2022-08-18
      • 2018-05-29
      相关资源
      最近更新 更多