为什么在Pycharm中连接混合类型的列表时会收到警告?

8
在Pycharm中,以下代码会产生一个警告:
from typing import List

list1: List[int] = [1, 2, 3]
list2: List[str] = ["1", "2", "3"]
list3: List[object] = list1 + list2
#                             ↳ Expected type List[int] (matched generic type List[_T]),
#                               got List[str] instead.

为什么?我不能将两个混合了类型提示的列表连接起来吗?

3
常用列表存储同类型的对象。混合不同类型的内容不算错误,但我认为这种做法风格不佳。 - Matthias
如果你将list1list2的注释也标记为List[object]类型,会发生什么?这将使你的程序至少在mypy下进行类型检查——也许它还能满足Pycharm。 - Michael0x2a
@Michael0x2a 这确实消除了警告,但并没有回答“为什么”...例如,如果list1list2本身没有类型提示,而是分配了具有提示返回类型的函数的输出(例如def gli() -> List[int]: return [1, 2, 3]然后list1 = gli()),我仍然会收到警告。如果这些函数是库代码怎么办?为什么连接它们的输出会有问题? - Cai
我认为这就是列表类型签名的定义方式--例如,可以查看 typeshed 中list.__add__ 的签名(标准库类型提示的规范集合)。类型签名可能会更加广泛(例如,重载以接受 List[S] 并返回 List[Union[T, S]]),但我怀疑在实践中这种情况并不太有用,并且会使那些想要严格同类列表或想要对其进行子类化的人生活更加困难。 - Michael0x2a
当然,类型检查器可能通过一些巧妙的类型签名调整或仔细地进行一些特殊处理来回避这些问题,但这只是一个优先级问题。Pycharm(以及mypy等)的问题跟踪器非常长,并且没有其他错误/功能请求需要解决。 - Michael0x2a
显示剩余3条评论
3个回答

7

根据评论中的要求,以下是类型检查器不允许此操作的原因。

第一个原因有点平凡:list.__add__ 的类型标记仅允许传入包含相同类型的列表:

_T = TypeVar('_T')

# ...snip...

class list(MutableSequence[_T], Generic[_T]):

    # ...snip...

    def __add__(self, x: List[_T]) -> List[_T]: ...

Pycharm支持PEP 484,并部分使用来自Typeshed的数据。

我们可以通过某种方式扩展这个类型签名(例如,重载它以接受List[_S]并在这种情况下返回List[Union[_T,_S]]),但我认为没有人去研究这种方法的可行性:这种做法在实践中并不太有用,会给那些想要严格同质列表或想要对其进行子类化的人带来麻烦,并且可能会破坏很多依赖当前类型签名的现有代码。

这种类型签名也可能反映了PEP 484初始设计期间所做的更广泛选择,即假定列表始终是同质的,始终包含相同类型的值。

PEP 484的设计者严格来说不需要做出这个选择:他们可以要求类型检查器特别处理与它的交互,就像我们目前对元组所做的那样。但我认为总体上不这样做更简单。(而且也可以说是更好的风格,但无论如何。)


第二个原因与PEP 484类型系统的基本限制有关:无法声明某些函数或方法不修改状态。基本上,只有在保证lst1.__add__(lst2)不会改变任何操作数的情况下,您想要的行为才是安全的。但实际上没有办法保证这一点——如果lst1是某个奇怪的列表子类,它会将项从lst2复制到自身中呢?那么暂时放松lst1的类型,从SomeListSubtype[int]到SomeListSubtype[object]是不安全的:在从lst2添加/注入字符串后,lst1将不再仅包含int。当然,实际编写这样的子类也是不好的做法,但类型检查器不能假定用户会遵循最佳实践,如果未强制执行,则类型检查器、编译器和类似工具从根本上来说都是保守的东西。
最后值得注意的是,这些问题都不是本质上无法克服的。类型检查器实现者可以做几件事情,例如:
  1. 调整列表的类型签名(并确保不会破坏任何现有代码)
  2. 引入某种声明方法是纯函数(不进行变异)的方式。基本上,将PEP 591背后的思想概括为函数也适用。(但这需要编写PEP,修改typeshed以使用新的类型构造,进行大量的仔细设计和实现工作...)
  3. 当我们确定两个变量不是列表的子类时,可能会特别处理此交互。(但实际上,我们能够确定这一点的次数非常有限。)

等等。

但是所有这些事情都需要时间和精力来完成:这是一个优先考虑的问题。PyCharm(和mypy等)的问题跟踪器非常长,而且还有大量其他的错误/功能请求需要解决。


3

其他评论已经明确解释了原因,因此我只想强调一下潜在的解决方法,适用于那些无法跳过其代码中的连接步骤且不希望看到这个烦人的警告。

在上面的情况下,产生没有警告的操作是:

list1 += list2

而有时我不得不执行以下操作:

[*list1, *list2]


0

就像Pycharm所说的那样,你可以连接不同的对象或列表,但这被认为是一种不好的做法,只是一个警告。


4
我明白警告很可能是有原因的。但为什么这被认为是不良实践? - Cai

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