如何暗示数字*类型*(即数字的子类)——而不是数字本身?
How to hint at number *types* (i.e. subclasses of Number) - not numbers themselves?
假设我想写一个函数接受Python中任意类型的数字,我可以这样注释它:
from numbers import Number
def foo(bar: Number):
print(bar)
将这个概念更进一步,我正在编写接受数字类型的函数,即 int
、float
或 numpy
dtypes 作为参数。目前,我正在写:
from typing import Type
def foo(bar: Type):
assert issubclass(bar, Number)
print(bar)
我想我可以用 NumberType
代替 Type
(类似于 NotImplementedType
和朋友 re-introduced in Python 3.10),因为所有数字类型都是 [= 的子类20=]:
from numbers import Number
import numpy as np
assert issubclass(int, Number)
assert issubclass(np.uint8, Number)
事实证明(或者至少据我所知),Python (3.9) 中没有通用 NumberType
这样的东西:
>>> type(Number)
abc.ABCMeta
是否有一种干净的方法(即没有运行时检查)来实现所需类型的注释?
没有通用的方法可以做到这一点。数字一开始就不是严格相关的,它们的类型就更少了。
虽然 numbers.Number
看起来像是“数字类型”,但它并不通用。例如,decimal.Decimal
is explicitly not a numbers.Number
as either subclass, subtype or virtual subclass. Specifically for typing, numbers.Number
is not endorsed by PEP 484 -- Type Hints.
为了有意义地键入提示“数字”,必须明确定义该上下文中的数字。这可能是一个预先存在的数字类型集,例如 int
<: float
<: complex
,一个 typing.Union
/TypeVar
的数字类型,一个 typing.Protocol
定义操作和代数结构,或类似的。
from typing import TypeVar
from decimal import Decimal
from fractions import Fraction
#: typevar of rational numbers if we squint real hard
Q = TypeVar("Q", float, Decimal, Fraction)
综上所述,“数字类型的类型”更没有意义。即使是 specific numbers.Number
实际上也没有任何特征:它不能转换为具体类型,也不能实例化为有意义的数字。
而是使用“一些类型的数字”:
from typing import Type
def zero(t: Type[Q]) -> Q:
return t() # all Type[Q]s can be instantiated without arguments
print(zero(Fraction))
如果 Type
的唯一目标是创建实例,则最好改为请求 Callable
。这涵盖了类型和工厂功能。
def one(t: Callable[[int], Q]) -> Q:
return t(1)
一般来说,我们如何提示classes,而不是classes的实例?
一般来说,如果我们想告诉类型检查器某个 class 的任何 实例 (或子类的任何 实例class of that class) 应该被接受为函数的参数,我们这样做:
def accepts_int_instances(x: int) -> None:
pass
class IntSubclass(int):
pass
accepts_int_instances(42) # passes MyPy (an instance of `int`)
accepts_int_instances(IntSubclass(666)) # passes MyPy (an instance of a subclass of `int`)
accepts_int_instances(3.14) # fails MyPy (an instance of `float` — `float` is not a subclass of `int`)
另一方面,如果我们有一个 class C
,我们想暗示 class C
本身(或 C
的子 class)应该作为参数传递给函数,我们使用 type[C]
而不是 C
。 (在 Python <= 3.8 中,您需要使用 typing.Type
而不是内置的 type
函数,但是从 Python 3.9 和 PEP 585 开始,我们可以直接参数化 type
。)
def accepts_int_and_subclasses(x: type[int]) -> None:
pass
class IntSubclass(int):
pass
accepts_int_and_subclasses(int) # passes MyPy
accepts_int_and_subclasses(float) # fails Mypy (not a subclass of `int`)
accepts_int_and_subclasses(IntSubclass) # passes MyPy
我们如何注释一个函数来表示 any numeric class 应该被某个参数接受?
int
、float
和所有 numpy
数值类型都是 numbers.Number
的子class,所以我们应该能够如果我们想说所有数字 class 都是允许的,请使用 type[Number]
!
至少,Python 说 float
和 int
是 Number
的子class :
>>> from numbers import Number
>>> issubclass(int, Number)
True
>>> issubclass(float, Number)
True
如果我们使用 运行time 类型检查库,例如 typeguard,使用 type[Number]
似乎工作正常:
>>> from typeguard import typechecked
>>> from fractions import Fraction
>>> from decimal import Decimal
>>> import numpy as np
>>>
>>> @typechecked
... def foo(bar: type[Number]) -> None:
... pass
...
>>> foo(str)
Traceback (most recent call last):
TypeError: argument "bar" must be a subclass of numbers.Number; got str instead
>>> foo(int)
>>> foo(float)
>>> foo(complex)
>>> foo(Decimal)
>>> foo(Fraction)
>>> foo(np.int64)
>>> foo(np.float32)
>>> foo(np.ulonglong)
>>> # etc.
但是等等!如果我们尝试将 type[Number]
与 static 类型检查器一起使用,它似乎不起作用。如果我们通过 MyPy 运行 以下片段,它会为每个 class 引发错误,除了 fractions.Fraction
:
from numbers import Number
from fractions import Fraction
from decimal import Decimal
NumberType = type[Number]
def foo(bar: NumberType) -> None:
pass
foo(float) # fails
foo(int) # fails
foo(Fraction) # succeeds!
foo(Decimal) # fails
Python 肯定不会骗我们说 float
和 int
是 Number
的子class。怎么回事?
为什么 type[Number]
不能作为数字 classes
的静态类型提示
虽然 issubclass(float, Number)
和 issubclass(int, Number)
的计算结果都是 True
,但实际上 float
和 int
都不是“严格的”子 [= numbers.Number
的 452=]。 numbers.Number
是一个抽象基 Class,int
and float
都注册为 Number
的“虚拟子classes”。这导致 Python 在 运行 时间 将 float
和 int
识别为 [= 的“subclasses” 34=],即使 Number
不在其中任何一个的方法解析顺序中。
See this Whosebug question for an explanation of what a class's "method resolution order", or "mro", is.
>>> # All classes have `object` in their mro
>>> class Foo: pass
>>> Foo.__mro__
(<class '__main__.Foo'>, <class 'object'>)
>>>
>>> # Subclasses of a class have that class in their mro
>>> class IntSubclass(int): pass
>>> IntSubclass.__mro__
(<class '__main__.IntSubclass'>, <class 'int'>, <class 'object'>)
>>> issubclass(IntSubclass, int)
True
>>>
>>> # But `Number` is not in the mro of `int`...
>>> int.__mro__
(<class 'int'>, <class 'object'>)
>>> # ...Yet `int` still pretends to be a subclass of `Number`!
>>> from numbers import Number
>>> issubclass(int, Number)
True
>>> #?!?!!??
What's an Abstract Base Class? Why is numbers.Number
an Abstract Base
Class? What's "virtual subclassing"?
- The docs for Abstract Base Classes ("ABCs") are here.
- PEP 3119, introducing Abstract Base Classes, is here.
- The docs for the
numbers
module are here.
- PEP 3141, which introduced
numbers.Number
, is here.
- I can recommend this talk by Raymond Hettinger, which has a detailed explanation of ABCs and the purposes of virtual subclassing.
问题是 MyPy does not understand the "virtual subclassing" mechanism that ABCs use (and, perhaps never will).
MyPy 确实理解标准库中的一些 ABC。例如,MyPy knows that list
is a subtype of collections.abc.MutableSequence
, even though MutableSequence
is an ABC, and list
is only a virtual subclass of MutableSequence
. 然而,MyPy only 将 list
理解为 MutableSequence
的子类型,因为我们一直在撒谎MyPy 关于 list
.
的方法解析顺序
MyPy 与所有其他主要类型检查器一起使用 typeshed repository for its static analysis of the classes and modules found in the standard library. If you look at the stub for list
in typeshed, you'll see that list
is given as being a direct subclass of collections.abc.MutableSequence
. That's not true at all — MutableSequence
is written in pure Python, whereas list
is an optimised data structure written in C. But for static analysis, it's useful for MyPy to think that this is true. Other collections classes in the standard library (for example, tuple
, set
, and dict
) are special-cased by typeshed in much the same way, but numeric types such as int
and float
中未找到的存根。
如果我们在集合 classes 方面对 MyPy 撒谎,为什么我们不在数字 classes 方面也对 MyPy 撒谎?
很多人(包括我!)认为我们应该这样做,并且关于是否应该进行此更改的讨论已经进行了很长时间(例如 typeshed proposal, MyPy issue)。但是,这样做有各种复杂性。
Credit goes to @chepner in the
可能的解决方案:使用 duck-typing
一个可能的(虽然有点 icky)解决方案可能是使用 typing.SupportsFloat
.
SupportsFloat
是一个 运行 时间可检查的协议,它有一个 abstractmethod
、__float__
。这意味着任何具有 __float__
方法的 classes 都被识别为子类型——在 运行 时间 和静态类型检查器 SupportsFloat
,即使 SupportsFloat
不在 class 的方法解析顺序中。
What's a protocol? What's duck-typing? How do protocols do what they do? Why are some protocols, but not all protocols, checkable at runtime?
- PEP 544, introducing
typing.Protocol
and structural-typing/duck-typing, explains in detail how typing.Protocol
works. It also explains how static type-checkers are able to recognise classes such as float
and int
as subtypes of SupportsFloat
, even though SupportsFloat
does not appear in the method resolution order for int
or float
.
- The Python docs for structural subtyping are here.
- The Python docs for
typing.Protocol
are here.
- The MyPy docs for
typing.Protocol
are here.
- The Python docs for
typing.SupportsFloat
are here.
- The source code for
typing.SupportsFloat
is here.
- By default, protocols cannot be checked at runtime with
isinstance
and issubclass
. SupportsFloat
is checkable at runtime because it is decorated with the @runtime_checkable
decorator. Read the documentation for that decorator here.
注意:虽然用户自定义协议只在Python >= 3.8中可用,但SupportsFloat
自模块添加到typing
以来一直在Python 3.5.
中的标准库
这个解决方案的优点
全面支持*所有主要数字类型:fractions.Fraction
、decimal.Decimal
、int
、float
、np.int32
、np.int16
, np.int8
, np.int64
, np.int0
, np.float16
, np.float32
, np.float64
, np.float128
, np.intc
, np.uintc
, np.int_
, np.uint
, np.longlong
, np.ulonglong
, np.half
, np.single
, np.double
, np.longdouble
, np.csingle
, np.cdouble
, 和 np.clongdouble
都有一个 __float__
方法。
如果我们将函数参数注释为 type[SupportsFloat]
,MyPy correctly accepts* the types that conform to the protocol, and correctly rejects the types that do not conform to the protocol.
这是一个相当的通用解决方案——您不需要显式枚举您希望接受的所有可能类型。
适用于静态类型检查器和 运行时间类型检查库,例如 typeguard
.
此解决方案的缺点
感觉就像(而且是)黑客。拥有 __float__
方法并不是任何人对摘要中定义“数字”的合理想法。
Mypy 无法将 complex
识别为 SupportsFloat
的子类型。 complex
实际上在 Python <= 3.9 中有一个 __float__
方法。但是,它在 typeshed stub for complex
. Since MyPy (along with all other major type-checkers) uses typeshed stubs for its static analysis, this means it is unaware that complex
has this method. complex.__float__
is likely omitted from the typeshed stub due to the fact that the method always raises TypeError
; for this reason, the __float__
method has in fact been removed from the complex
class in Python 3.10.
中没有 __float__
方法
任何用户定义的 class,即使它不是数字 class,也可能定义 __float__
。事实上,标准库中甚至有几个非数字的 class 定义了 __float__
。例如,虽然 Python 中的 str
类型(用 C 编写)没有 __float__
方法,但 collections.UserString
(用纯 Python) 做。 (The source code for str
is here, and the source code for collections.UserString
is here.)
用法示例
除了 complex
:
我测试过的所有数字类型都通过了 MyPy
from typing import SupportsFloat
NumberType = type[SupportsFloat]
def foo(bar: NumberType) -> None:
pass
如果我们也希望 complex
也被接受,对此解决方案的天真调整就是使用以下代码片段,特殊外壳 complex
。这满足了我能想到的每种数字类型的 MyPy。我还将 type[Number]
放入类型提示中,因为它 可能 有助于捕捉 确实 的假设 class ] 直接继承自 numbers.Number
而 没有 有 __float__
方法。我不知道为什么有人会写这样的 class,但是 一些 class 直接继承自 numbers.Number
(例如 fractions.Fraction
), and it certainly would be theoretically possible to create a direct subclass of Number
without a __float__
method. Number
itself is an empty class that has no methods — 它的存在仅仅是为了为标准库中的其他数字 class 提供一个“虚拟基础 class”。
from typing import SupportsFloat, Union
from numbers import Number
NumberType = Union[type[SupportsFloat], type[complex], type[Number]]
# You can also write this more succinctly as:
# NumberType = type[Union[SupportsFloat, complex, Number]]
# The two are equivalent.
# In Python >= 3.10, we can even write it like this:
# NumberType = type[SupportsFloat | complex | Number]
# See PEP 604: https://www.python.org/dev/peps/pep-0604/
def foo(bar: NumberType) -> None:
pass
翻译成英文,NumberType
这里相当于:
Any class, if and only if:
- It has a
__float__
method;
- AND/OR it is
complex
;
- AND/OR it is a subclass of
complex
;
- AND/OR it is
numbers.Number
;
- AND/OR it is a "strict" (non-virtual) subclass of
numbers.Number
.
我不认为这是 complex
问题的“解决方案”——它更像是一种变通方法。 complex
的问题说明了这种方法的一般危险。第三方库中可能还有其他不常见的数字类型,例如,不直接 subclass numbers.Number
或具有 __float__
方法。事先知道它们可能是什么样子,并且将它们全部特殊化是非常困难的。
附录
为什么SupportsFloat
而不是typing.SupportsInt
?
fractions.Fraction
有一个 __float__
方法 (inherited from numbers.Rational
) 但没有 __int__
方法。
为什么SupportsFloat
而不是SupportsAbs
?
甚至 complex
也有一个 __abs__
方法,所以 typing.SupportsAbs
乍一看似乎是一个很有前途的选择!但是,标准库中还有其他几个 classes 具有 __abs__
方法但没有 __float__
方法,并且争论它们都是数字是一种延伸classes。 (datetime.timedelta
对我来说 感觉 不是很像数字。)如果你使用 SupportsAbs
而不是 SupportsFloat
,你可能会撒网太宽了,允许各种非数字 classes.
为什么SupportsFloat
而不是SupportsRound
?
作为 SupportsFloat
的替代方案,您还可以考虑使用 typing.SupportsRound
,它接受所有具有 __round__
方法的 classes。这与 SupportsFloat
一样全面(它涵盖了 complex
以外的所有主要数字类型)。它还具有 collection.UserString
没有 __round__
方法的优点,而如上所述,它确实有 __float__
方法。最后,第三方非数字 classes 似乎不太可能包含 __round__
方法。
但是,如果您选择 SupportsRound
而不是 SupportsFloat
,在我看来,您将 运行 排除有效第三方数字的风险更大 class是的,无论出于何种原因,都没有定义 __round__
.
“有一个 __float__
方法”和“有一个 __round__
方法”对于 class 是一个“数字”的含义的定义都很差。然而,前者感觉比后者更接近“真实”定义。因此,与 __round__
方法。
如果您想在确保您的函数接受有效的第三方数字类型时“特别安全”,我看不出有什么特别的危害在扩展 NumberType
甚至 进一步 与 SupportsRound
:
from typing import SupportsFloat, SupportsRound, Union
from numbers import Number
NumberType = Union[type[SupportsFloat], type[SupportsRound], type[complex], type[Number]]
但是,考虑到任何具有 __round__
方法的类型很可能具有还有一个 __float__
方法。
*...除了complex
这不是完全原始问题的答案。 (Alex Waygood's answer is properly selected as responsive.) However, I have attempted to generalize my own work-arounds for the sharp edges between numbers and typing in Python. Those work-arounds now live in numerary
(having extracted it via c-section from dyce
,构思的地方)。
我没有在命名上花很多时间,希望它是短暂的。 Docs are online. It should be considered experimental, but it is rapidly approaching stability. Feedback, suggestions, and contributions 非常感谢。
假设我想写一个函数接受Python中任意类型的数字,我可以这样注释它:
from numbers import Number
def foo(bar: Number):
print(bar)
将这个概念更进一步,我正在编写接受数字类型的函数,即 int
、float
或 numpy
dtypes 作为参数。目前,我正在写:
from typing import Type
def foo(bar: Type):
assert issubclass(bar, Number)
print(bar)
我想我可以用 NumberType
代替 Type
(类似于 NotImplementedType
和朋友 re-introduced in Python 3.10),因为所有数字类型都是 [= 的子类20=]:
from numbers import Number
import numpy as np
assert issubclass(int, Number)
assert issubclass(np.uint8, Number)
事实证明(或者至少据我所知),Python (3.9) 中没有通用 NumberType
这样的东西:
>>> type(Number)
abc.ABCMeta
是否有一种干净的方法(即没有运行时检查)来实现所需类型的注释?
没有通用的方法可以做到这一点。数字一开始就不是严格相关的,它们的类型就更少了。
虽然 numbers.Number
看起来像是“数字类型”,但它并不通用。例如,decimal.Decimal
is explicitly not a numbers.Number
as either subclass, subtype or virtual subclass. Specifically for typing, numbers.Number
is not endorsed by PEP 484 -- Type Hints.
为了有意义地键入提示“数字”,必须明确定义该上下文中的数字。这可能是一个预先存在的数字类型集,例如 int
<: float
<: complex
,一个 typing.Union
/TypeVar
的数字类型,一个 typing.Protocol
定义操作和代数结构,或类似的。
from typing import TypeVar
from decimal import Decimal
from fractions import Fraction
#: typevar of rational numbers if we squint real hard
Q = TypeVar("Q", float, Decimal, Fraction)
综上所述,“数字类型的类型”更没有意义。即使是 specific numbers.Number
实际上也没有任何特征:它不能转换为具体类型,也不能实例化为有意义的数字。
而是使用“一些类型的数字”:
from typing import Type
def zero(t: Type[Q]) -> Q:
return t() # all Type[Q]s can be instantiated without arguments
print(zero(Fraction))
如果 Type
的唯一目标是创建实例,则最好改为请求 Callable
。这涵盖了类型和工厂功能。
def one(t: Callable[[int], Q]) -> Q:
return t(1)
一般来说,我们如何提示classes,而不是classes的实例?
一般来说,如果我们想告诉类型检查器某个 class 的任何 实例 (或子类的任何 实例class of that class) 应该被接受为函数的参数,我们这样做:
def accepts_int_instances(x: int) -> None:
pass
class IntSubclass(int):
pass
accepts_int_instances(42) # passes MyPy (an instance of `int`)
accepts_int_instances(IntSubclass(666)) # passes MyPy (an instance of a subclass of `int`)
accepts_int_instances(3.14) # fails MyPy (an instance of `float` — `float` is not a subclass of `int`)
另一方面,如果我们有一个 class C
,我们想暗示 class C
本身(或 C
的子 class)应该作为参数传递给函数,我们使用 type[C]
而不是 C
。 (在 Python <= 3.8 中,您需要使用 typing.Type
而不是内置的 type
函数,但是从 Python 3.9 和 PEP 585 开始,我们可以直接参数化 type
。)
def accepts_int_and_subclasses(x: type[int]) -> None:
pass
class IntSubclass(int):
pass
accepts_int_and_subclasses(int) # passes MyPy
accepts_int_and_subclasses(float) # fails Mypy (not a subclass of `int`)
accepts_int_and_subclasses(IntSubclass) # passes MyPy
我们如何注释一个函数来表示 any numeric class 应该被某个参数接受?
int
、float
和所有 numpy
数值类型都是 numbers.Number
的子class,所以我们应该能够如果我们想说所有数字 class 都是允许的,请使用 type[Number]
!
至少,Python 说 float
和 int
是 Number
的子class :
>>> from numbers import Number
>>> issubclass(int, Number)
True
>>> issubclass(float, Number)
True
如果我们使用 运行time 类型检查库,例如 typeguard,使用 type[Number]
似乎工作正常:
>>> from typeguard import typechecked
>>> from fractions import Fraction
>>> from decimal import Decimal
>>> import numpy as np
>>>
>>> @typechecked
... def foo(bar: type[Number]) -> None:
... pass
...
>>> foo(str)
Traceback (most recent call last):
TypeError: argument "bar" must be a subclass of numbers.Number; got str instead
>>> foo(int)
>>> foo(float)
>>> foo(complex)
>>> foo(Decimal)
>>> foo(Fraction)
>>> foo(np.int64)
>>> foo(np.float32)
>>> foo(np.ulonglong)
>>> # etc.
但是等等!如果我们尝试将 type[Number]
与 static 类型检查器一起使用,它似乎不起作用。如果我们通过 MyPy 运行 以下片段,它会为每个 class 引发错误,除了 fractions.Fraction
:
from numbers import Number
from fractions import Fraction
from decimal import Decimal
NumberType = type[Number]
def foo(bar: NumberType) -> None:
pass
foo(float) # fails
foo(int) # fails
foo(Fraction) # succeeds!
foo(Decimal) # fails
Python 肯定不会骗我们说 float
和 int
是 Number
的子class。怎么回事?
为什么 type[Number]
不能作为数字 classes
的静态类型提示
虽然 issubclass(float, Number)
和 issubclass(int, Number)
的计算结果都是 True
,但实际上 float
和 int
都不是“严格的”子 [= numbers.Number
的 452=]。 numbers.Number
是一个抽象基 Class,int
and float
都注册为 Number
的“虚拟子classes”。这导致 Python 在 运行 时间 将 float
和 int
识别为 [= 的“subclasses” 34=],即使 Number
不在其中任何一个的方法解析顺序中。
See this Whosebug question for an explanation of what a class's "method resolution order", or "mro", is.
>>> # All classes have `object` in their mro
>>> class Foo: pass
>>> Foo.__mro__
(<class '__main__.Foo'>, <class 'object'>)
>>>
>>> # Subclasses of a class have that class in their mro
>>> class IntSubclass(int): pass
>>> IntSubclass.__mro__
(<class '__main__.IntSubclass'>, <class 'int'>, <class 'object'>)
>>> issubclass(IntSubclass, int)
True
>>>
>>> # But `Number` is not in the mro of `int`...
>>> int.__mro__
(<class 'int'>, <class 'object'>)
>>> # ...Yet `int` still pretends to be a subclass of `Number`!
>>> from numbers import Number
>>> issubclass(int, Number)
True
>>> #?!?!!??
What's an Abstract Base Class? Why is
numbers.Number
an Abstract Base Class? What's "virtual subclassing"?
- The docs for Abstract Base Classes ("ABCs") are here.
- PEP 3119, introducing Abstract Base Classes, is here.
- The docs for the
numbers
module are here.- PEP 3141, which introduced
numbers.Number
, is here.- I can recommend this talk by Raymond Hettinger, which has a detailed explanation of ABCs and the purposes of virtual subclassing.
问题是 MyPy does not understand the "virtual subclassing" mechanism that ABCs use (and, perhaps never will).
MyPy 确实理解标准库中的一些 ABC。例如,MyPy knows that list
is a subtype of collections.abc.MutableSequence
, even though MutableSequence
is an ABC, and list
is only a virtual subclass of MutableSequence
. 然而,MyPy only 将 list
理解为 MutableSequence
的子类型,因为我们一直在撒谎MyPy 关于 list
.
MyPy 与所有其他主要类型检查器一起使用 typeshed repository for its static analysis of the classes and modules found in the standard library. If you look at the stub for list
in typeshed, you'll see that list
is given as being a direct subclass of collections.abc.MutableSequence
. That's not true at all — MutableSequence
is written in pure Python, whereas list
is an optimised data structure written in C. But for static analysis, it's useful for MyPy to think that this is true. Other collections classes in the standard library (for example, tuple
, set
, and dict
) are special-cased by typeshed in much the same way, but numeric types such as int
and float
中未找到的存根。
如果我们在集合 classes 方面对 MyPy 撒谎,为什么我们不在数字 classes 方面也对 MyPy 撒谎?
很多人(包括我!)认为我们应该这样做,并且关于是否应该进行此更改的讨论已经进行了很长时间(例如 typeshed proposal, MyPy issue)。但是,这样做有各种复杂性。
Credit goes to @chepner in the
可能的解决方案:使用 duck-typing
一个可能的(虽然有点 icky)解决方案可能是使用 typing.SupportsFloat
.
SupportsFloat
是一个 运行 时间可检查的协议,它有一个 abstractmethod
、__float__
。这意味着任何具有 __float__
方法的 classes 都被识别为子类型——在 运行 时间 和静态类型检查器 SupportsFloat
,即使 SupportsFloat
不在 class 的方法解析顺序中。
What's a protocol? What's duck-typing? How do protocols do what they do? Why are some protocols, but not all protocols, checkable at runtime?
- PEP 544, introducing
typing.Protocol
and structural-typing/duck-typing, explains in detail howtyping.Protocol
works. It also explains how static type-checkers are able to recognise classes such asfloat
andint
as subtypes ofSupportsFloat
, even thoughSupportsFloat
does not appear in the method resolution order forint
orfloat
.- The Python docs for structural subtyping are here.
- The Python docs for
typing.Protocol
are here.- The MyPy docs for
typing.Protocol
are here.- The Python docs for
typing.SupportsFloat
are here.- The source code for
typing.SupportsFloat
is here.- By default, protocols cannot be checked at runtime with
isinstance
andissubclass
.SupportsFloat
is checkable at runtime because it is decorated with the@runtime_checkable
decorator. Read the documentation for that decorator here.
注意:虽然用户自定义协议只在Python >= 3.8中可用,但SupportsFloat
自模块添加到typing
以来一直在Python 3.5.
这个解决方案的优点
全面支持*所有主要数字类型:
fractions.Fraction
、decimal.Decimal
、int
、float
、np.int32
、np.int16
,np.int8
,np.int64
,np.int0
,np.float16
,np.float32
,np.float64
,np.float128
,np.intc
,np.uintc
,np.int_
,np.uint
,np.longlong
,np.ulonglong
,np.half
,np.single
,np.double
,np.longdouble
,np.csingle
,np.cdouble
, 和np.clongdouble
都有一个__float__
方法。如果我们将函数参数注释为
type[SupportsFloat]
,MyPy correctly accepts* the types that conform to the protocol, and correctly rejects the types that do not conform to the protocol.这是一个相当的通用解决方案——您不需要显式枚举您希望接受的所有可能类型。
适用于静态类型检查器和 运行时间类型检查库,例如
typeguard
.
此解决方案的缺点
感觉就像(而且是)黑客。拥有
__float__
方法并不是任何人对摘要中定义“数字”的合理想法。Mypy 无法将
中没有complex
识别为SupportsFloat
的子类型。complex
实际上在 Python <= 3.9 中有一个__float__
方法。但是,它在 typeshed stub forcomplex
. Since MyPy (along with all other major type-checkers) uses typeshed stubs for its static analysis, this means it is unaware thatcomplex
has this method.complex.__float__
is likely omitted from the typeshed stub due to the fact that the method always raisesTypeError
; for this reason, the__float__
method has in fact been removed from thecomplex
class in Python 3.10.__float__
方法任何用户定义的 class,即使它不是数字 class,也可能定义
__float__
。事实上,标准库中甚至有几个非数字的 class 定义了__float__
。例如,虽然 Python 中的str
类型(用 C 编写)没有__float__
方法,但collections.UserString
(用纯 Python) 做。 (The source code forstr
is here, and the source code forcollections.UserString
is here.)
用法示例
除了 complex
:
from typing import SupportsFloat
NumberType = type[SupportsFloat]
def foo(bar: NumberType) -> None:
pass
如果我们也希望 complex
也被接受,对此解决方案的天真调整就是使用以下代码片段,特殊外壳 complex
。这满足了我能想到的每种数字类型的 MyPy。我还将 type[Number]
放入类型提示中,因为它 可能 有助于捕捉 确实 的假设 class ] 直接继承自 numbers.Number
而 没有 有 __float__
方法。我不知道为什么有人会写这样的 class,但是 一些 class 直接继承自 numbers.Number
(例如 fractions.Fraction
), and it certainly would be theoretically possible to create a direct subclass of Number
without a __float__
method. Number
itself is an empty class that has no methods — 它的存在仅仅是为了为标准库中的其他数字 class 提供一个“虚拟基础 class”。
from typing import SupportsFloat, Union
from numbers import Number
NumberType = Union[type[SupportsFloat], type[complex], type[Number]]
# You can also write this more succinctly as:
# NumberType = type[Union[SupportsFloat, complex, Number]]
# The two are equivalent.
# In Python >= 3.10, we can even write it like this:
# NumberType = type[SupportsFloat | complex | Number]
# See PEP 604: https://www.python.org/dev/peps/pep-0604/
def foo(bar: NumberType) -> None:
pass
翻译成英文,NumberType
这里相当于:
Any class, if and only if:
- It has a
__float__
method;- AND/OR it is
complex
;- AND/OR it is a subclass of
complex
;- AND/OR it is
numbers.Number
;- AND/OR it is a "strict" (non-virtual) subclass of
numbers.Number
.
我不认为这是 complex
问题的“解决方案”——它更像是一种变通方法。 complex
的问题说明了这种方法的一般危险。第三方库中可能还有其他不常见的数字类型,例如,不直接 subclass numbers.Number
或具有 __float__
方法。事先知道它们可能是什么样子,并且将它们全部特殊化是非常困难的。
附录
为什么SupportsFloat
而不是typing.SupportsInt
?
fractions.Fraction
有一个 __float__
方法 (inherited from numbers.Rational
) 但没有 __int__
方法。
为什么SupportsFloat
而不是SupportsAbs
?
甚至 complex
也有一个 __abs__
方法,所以 typing.SupportsAbs
乍一看似乎是一个很有前途的选择!但是,标准库中还有其他几个 classes 具有 __abs__
方法但没有 __float__
方法,并且争论它们都是数字是一种延伸classes。 (datetime.timedelta
对我来说 感觉 不是很像数字。)如果你使用 SupportsAbs
而不是 SupportsFloat
,你可能会撒网太宽了,允许各种非数字 classes.
为什么SupportsFloat
而不是SupportsRound
?
作为 SupportsFloat
的替代方案,您还可以考虑使用 typing.SupportsRound
,它接受所有具有 __round__
方法的 classes。这与 SupportsFloat
一样全面(它涵盖了 complex
以外的所有主要数字类型)。它还具有 collection.UserString
没有 __round__
方法的优点,而如上所述,它确实有 __float__
方法。最后,第三方非数字 classes 似乎不太可能包含 __round__
方法。
但是,如果您选择 SupportsRound
而不是 SupportsFloat
,在我看来,您将 运行 排除有效第三方数字的风险更大 class是的,无论出于何种原因,都没有定义 __round__
.
“有一个 __float__
方法”和“有一个 __round__
方法”对于 class 是一个“数字”的含义的定义都很差。然而,前者感觉比后者更接近“真实”定义。因此,与 __round__
方法。
如果您想在确保您的函数接受有效的第三方数字类型时“特别安全”,我看不出有什么特别的危害在扩展 NumberType
甚至 进一步 与 SupportsRound
:
from typing import SupportsFloat, SupportsRound, Union
from numbers import Number
NumberType = Union[type[SupportsFloat], type[SupportsRound], type[complex], type[Number]]
但是,考虑到任何具有 __round__
方法的类型很可能具有还有一个 __float__
方法。
*...除了complex
这不是完全原始问题的答案。 (Alex Waygood's answer is properly selected as responsive.) However, I have attempted to generalize my own work-arounds for the sharp edges between numbers and typing in Python. Those work-arounds now live in numerary
(having extracted it via c-section from dyce
,构思的地方)。
我没有在命名上花很多时间,希望它是短暂的。 Docs are online. It should be considered experimental, but it is rapidly approaching stability. Feedback, suggestions, and contributions 非常感谢。