list[str] 是可迭代的吗?

Fal*_*ble 8 python python-internals python-typing

Python 3.10 不这么认为:

Python 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:38:29) [Clang 13.0.1 ] \
    on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import Iterable
>>> isinstance(list[str], Iterable)
False
>>> list(list[str])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'types.GenericAlias' object is not iterable
Run Code Online (Sandbox Code Playgroud)

Python 3.11 认为它是:

Python 3.11.0 | packaged by conda-forge | (main, Jan 15 2023, 05:44:48) [Clang 14.0.6 ] \
    on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import Iterable
>>> isinstance(list[str], Iterable)
True
>>> list(list[str])
[*list[str]]
Run Code Online (Sandbox Code Playgroud)

如果它是可迭代的,那么迭代它的结果应该是什么?该*list[str]项目似乎是unpacking其自身或类型变量元组的。
这里发生了什么?我知道 python 中的输入处于不断变化和快速发展的状态,但我真的不知道如何解释这一点。

更新:修复了Daniil Fajnberg 指出的 3.10 示例中的拼写错误

更新:我不想针对看似边缘的问题发表长篇文章,但我认为有必要了解一些背景知识。

据我所知,至少有一个案例导致了问题。我经常使用Fastcore库。在笔记本环境中,test_eq模块可以方便地记录/测试代码。辅助test_eq函数检查相等性 ( ==)。

直到 3.11 版本,下面的代码都是可以的:

test_eq(_generic_order((list[str],)), (list[str],))
Run Code Online (Sandbox Code Playgroud)

(_generic_order是一个按“通用性”对注释进行排序的函数,现在这并不重要)

在3.11版本中:

test_eq(_generic_order((list[str],)), (list[str],))

RecursionError                            Traceback (most recent call last)
Cell In[81], line 1
----> 1 test_eq(_generic_order((list[str],)), (list[str],))
...
File ~/dev/repo/project/rei/.micromamba/envs/rei/lib/python3.11/site-packages/fastcore/imports.py:33, in <genexpr>(.0)
     31 "Compares whether `a` and `b` are the same length and have the same contents"
     32 if not is_iter(b): return a==b
---> 33 return all(equals(a_,b_) for a_,b_ in itertools.zip_longest(a,b))
...
File ~/dev/repo/project/rei/.micromamba/envs/rei/lib/python3.11/typing.py:1550, in _SpecialGenericAlias.__subclasscheck__(self, cls)
   1548     return issubclass(cls.__origin__, self.__origin__)
   1549 if not isinstance(cls, _GenericAlias):
-> 1550     return issubclass(cls, self.__origin__)
   1551 return super().__subclasscheck__(cls)

File <frozen abc>:123, in __subclasscheck__(cls, subclass)

RecursionError: maximum recursion depth exceeded in comparison
Run Code Online (Sandbox Code Playgroud)

test_eq在内部检查参数是否为iterable并逐项比较。就像list[str]迭代本身一样,递归地狱。

如果test_eq想要真正通用,可能应该防止递归。但容器在第一层包含自身的情况很少见,这是不稳定的。这test_eq不是问题的重点,只是一个未记录的 3.11 更改产生的问题的示例。

请注意,旧的泛型(ListTuple等)使用该typing模块,但内置类型(listtupleGenericAlias在 C 中实现。因此,Python 维护者投入了大量精力来改变 的行为GenericAlias以使它们可迭代,不仅如此,但它们本身是可迭代的。文档或更改日志中没有提及这一事实(而且还没有时间深入研究 CPython 的 git 注释)。

这是一个边缘情况吗?大概。Python 文档经常警告我们,不鼓励将类型注释用于类型提示以外的目的。同时,类型提示自省在每个 Python 版本中都变得更加强大。

在引入类型的时候,我们还获得了数据类,这些数据类本身不仅是有用的数据容器,而且还是在运行时动态利用类型注释的优雅示例。Pydantic 和越来越多的工具正在使用注释来检查/更改代码,我发现自己在自己的代码中越来越多地使用它们。

我们在打字上投入了大量的精力,因为它是一种强大而有用的工具,除了类型检查之外,它可能也很有用。作为一名开发人员,我想了解我的工具,尤其是像打字一样重要的工具。

所以,问题仍然是:我好奇的不是如何,这很琐碎,而是为什么 GenericAlias 突然变得可迭代?

Fal*_*ble 5

感谢@anthonysotille@SUTerliakov的提示。

我们在这里看到的是设计对 3.11 的Variadic Generics ( PEP 646 ) 支持时所做出的决定的意外结果。特别是涉及TypeVarTuple 的类型注释中Unpack运算符的实现。*

拆包是

从概念上将对象标记为已解包的类型运算符。...

在 3.10 中:

from typing_extension import Unpack
from typing import Tuple

>>> Tuple[int, str], type(Tuple[int, str]), Unpack[Tuple[int, str]]
(typing.Tuple[int, str] <class 'typing._GenericAlias'> typing_extensions.Unpack[typing.Tuple[int, str]])
Run Code Online (Sandbox Code Playgroud)

我们已经知道通用别名不是可迭代的,

>>> list(list[str])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'types.GenericAlias' object is not iterable
Run Code Online (Sandbox Code Playgroud)

因为他们不需要。

Unpack适用于任何泛型,实际上是任何类型:

>>> Unpack[list[int]], Unpack[str]
(typing_extensions.Unpack[list[int]] typing_extensions.Unpack[str])
Run Code Online (Sandbox Code Playgroud)

与任何其他类型构造一样,的运行时表达式Unpack非常精简,仅涉及typing模块。尽管类型提示在类型上下文之外很有用,但 Python 会尽力不影响运行时性能。

然而,在 3.11 中,Python 维护者决定使用星号运算符 *作为 的语法糖,这需要对被调用者Unpack进行调用,并需要进行细微的语法更改并实现通用别名。对于、和老式泛型,更改就足够了,但是CPython 中的其余内置容器类型需要更改。这对类型上下文之外有影响,请参阅此处的详细信息。__iter__*__iter__ ListTupletyping._GenericAliaslisttypes.GenericAlias

>>> Tuple[int, str], type(Tuple[int, str]), [*Tuple[int, str]]
(typing.Tuple[int, str] <class 'typing._GenericAlias'> [*typing.Tuple[int, str]])

>>> list[str], type(list[str]), [*list[str]], [Unpack[list[str]]]
(list[str] <class 'types.GenericAlias'> [*list[str]] [*list[str]])
Run Code Online (Sandbox Code Playgroud)

解包时,GenericAlias.__iter__只需返回另一个标记为已解包的实例。

>>> type(list(List[str])[0]), hasattr(list(List[str])[0], '__unpacked__')
(<class 'typing._UnpackGenericAlias'>, False)  

>>> type(list(list[str])[0]), list(list[str])[0].__unpacked__
(<class 'types.GenericAlias'>, True)
Run Code Online (Sandbox Code Playgroud)

总之,实例GenericAlias在 Python 3.11 中是可迭代的,是由于 Unpack 运算符的实现,它需要进行较小的语法更改并实现__iter__泛型别名。有关更多详细信息和含义,请参阅答案中提供的资源。


ven*_*uil -5

typingmodule 不包含父类 - 它仅提供对类型提示的支持(主要由 IDE 或 linter 等使用)。

list[str] 一个可迭代的,你可以用iter函数检查它。

>>> iter(["a", "b", "c"])
<list_iterator object at 0x7fee38ee7580>
Run Code Online (Sandbox Code Playgroud)

您可以阅读此答案以获取更多详细信息 - /sf/answers/136673701/

  • OP 正在询问类型“list[str]”,而不是像“[“a”,“b”,“c”]”这样的类型的值。 (5认同)