您可以在Python类型注释中指定差异吗? [英] Can you specify variance in a Python type annotation?

查看:103
本文介绍了您可以在Python类型注释中指定差异吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

您能在下面的代码中发现错误吗? Mypy无法。

 通过输入import Dict,任何

def add_items(d:Dict [str,Any])->无:
d ['foo'] = 5

d:Dict [str,str] = {}
add_items(d)

作为密钥, d.items()中的值:
print(f {repr(key)}:{repr(value.lower())})

Python当然会发现该错误,这有助于通知我们'int'对象没有属性'lower'。太糟糕了,直到运行时它才能告诉我们。



据我所知,mypy不会捕获此错误,因为它允许对<$ add_items 的c $ c> d 参数是协变的。如果我们仅从字典中读取内容,那将是有道理的。如果仅阅读,那么我们希望参数是协变的。如果我们准备读取任何类型,那么我们应该能够读取字符串类型。当然,如果我们只是阅读,则应将其键入为 typing.Mapping



重写时,我们实际上希望参数为 contravariant 。例如,对于某人来说,传递 Dict [Any,Any] 是很有意义的,因为这样可以完全存储字符串键和整数值。 / p>

如果我们正在阅读文字,那么除了参数不变之外别无选择。



有没有一种方法可以指定我们需要什么样的差异?更好的是,mypy是否足够复杂,以至于可以期望它通过静态分析确定方差,并且应该将其作为错误归档?还是Python中类型检查的当前状态根本无法捕获此类编程错误?

解决方案

您的分析不正确-实际上与方差无关,而mypy中的Dict类型实际上是不变的wrt



相反,问题在于您已将Dict的值声明为任何,动态类型。这实际上意味着您希望mypy基本上不对与Dict值相关的任何内容进行类型检查。而且由于您选择不进行类型检查,所以它自然不会出现任何与类型相关的错误。



(这是通过神奇地放置 Any 位于类型晶格的顶部和底部,基本上,给定某些类型的 T ,情况就是 Any 始终是T 的子类型, T始终是 Any 的子类型。



通过运行以下程序,您可以看到Dict对您来说是不变的:

 来自键入import Dict 

A类:通过
B类(A):通过
C类(B):通过

def accepts_a(x:Dict [str,A])->无:通过
def accepts_b(x:Dict [str,B])->无:通过
def accepts_c(x:Dict [str,C])->无:通过

my_dict:Dict [str,B] = { foo:B()}

#错误: accepts_a的参数1具有不兼容的类型 Dict [str,B];预期的 Dict [str,A]
#注意: Dict是不变的-请参见http://mypy.readthedocs.io/zh/latest/common_issues.html#variance
#注意:考虑改用映射,其值类型为
的协变量accepts_a(my_dict)

#类型检查!没错
accepts_b(my_dict)

#错误: accepts_c的参数1具有不兼容的类型 Dict [str,B];预期的 Dict [str,C]
accepts_c(my_dict)

仅调用 accept_b 成功,这与预期的方差一致。






为了回答有关如何设置方差的问题-mypy的设计使数据结构的方差在定义时设置,并且在调用时不能真正更改。



因此,由于Dict被定义为不变的,因此事后不能真正将其更改为协变或不变的。



有关在以下位置设置方差的更多详细信息定义时间,请参见有关泛型的mypy参考文档



如您所指出的,您可以声明要使用Mapping接受只读版本的Dict。通常情况下,您可能要使用任何PEP 484数据结构的只读版本,例如序列是List的只读版本。



AFAIK虽然没有默认的Dict只读版本。但是,您可以使用协议来自己破解,希望这样-即将进行结构化而不是标称输入的标准化方法:

 来自输入import Dict,TypeVar,Generic 
fromTypeing_extensions导入协议

K = TypeVar('K',变量= True)
V = TypeVar('V',变量= True)

#Mypy要求密钥也要互变。我怀疑这是因为
#它实际上无法验证所有满足WriteOnlyDict
#协议的类型都会以不变的方式使用键。
类WriteOnlyDict(Protocol,Generic [K,V]):
def __setitem __(self,key:K,value:V)->无:...

A级:通过
B(A)级:通过
C(B)级:通过

#所有三项函数仅接受使用协议中描述的签名实现
#__setitem__方法的对象。

#您也只能在函数体内使用此方法,
#强制执行只写性质。
def accepts_a(x:WriteOnlyDict [str,A])->无:传递
def accepts_b(x:WriteOnlyDict [str,B])->无:传递
def accepts_c(x:WriteOnlyDict [str,C])->无:通过

my_dict:WriteOnlyDict [str,B] = { foo:B()}

#错误: accepts_a的参数1具有不兼容的类型 WriteOnlyDict [str,B];预期的 WriteOnlyDict [str,A]
accepts_a(my_dict)

#两种类型的检查
accepts_b(my_dict)
accepts_c(my_dict)






回答您的隐含问题(如何获取mypy以进行检测类型错误在这里/正确地检查我的代码?),答案是简单-避免不惜一切代价使用 Any 。每次执行此操作时,您都是故意在类型系统中打开一个漏洞。



例如,一种更安全的类型来声明dict的值可以是任何值会一直使用 Dict [str,object] 。现在,mypy将标记对 add_items 函数的调用为非类型安全。



或者,考虑如果您知道自己的值将是异类的,请使用 TypedDict 。 / p>

您甚至可以通过启用禁用动态键入系列命令行标志/配置文件标志。



实践中,完全禁止使用Any通常是不现实的。即使您可以在代码中达到这一理想,许多第3方库都是未注释的或未完全注释的,这意味着它们不得不在各处使用Any。因此,不幸的是,完全删除它们的使用往往会导致需要大量额外的工作。


Can you spot the error in the code below? Mypy can't.

from typing import Dict, Any

def add_items(d: Dict[str, Any]) -> None:
    d['foo'] = 5

d: Dict[str, str] = {}
add_items(d)

for key, value in d.items():
    print(f"{repr(key)}: {repr(value.lower())}")

Python spots the error, of course, helpfully informing us that 'int' object has no attribute 'lower'. Too bad it can't tell us this until run time.

As far as I can tell, mypy doesn't catch this error because it allows arguments to the d parameter of add_items to be covariant. That would make sense if we were only reading from the dictionary. If we were only reading, then we would want the parameter to be covariant. If we're prepared to read any type, then we should be able to read string types. Of course, if we were only reading, then we should type it as typing.Mapping.

Since we're writing, we actually want the parameter to be contravariant. For instance, it would make perfect sense for someone to pass in a Dict[Any, Any], since that would be perfectly capable of storing a string key and integer value.

If we were reading and writing, there would be no choice but for the parameter to be invariant.

Is there a way to specify what kind of variance we need? Even better, is mypy sophisticated enough that it should be reasonable to expect it to determine the variance through static analysis, and this should be filed as a bug? Or is the current state of type checking in Python simply not able to catch this kind of programming error?

解决方案

Your analysis is incorrect -- this actually has nothing to do with variance, and the Dict type in mypy is actually invariant w.r.t. to its value.

Rather, the problem is that you've declared the value of your Dict to be of type Any, the dynamic type. This effectively means that you want mypy to just basically not type-check anything related to your Dict's values. And since you've opted out of type-checking, it naturally won't pick up any type-related errors.

(This is accomplished by magically placing Any at both the top and bottom of the type lattice. Basically, given some type T, it's the case that Any is always a subtype of T and T is always a subtype of Any. Mypy auto-magically picks whichever relationship results in no errors.)

You can see that Dict is invariant for yourself by running the following program:

from typing import Dict

class A: pass
class B(A): pass
class C(B): pass

def accepts_a(x: Dict[str, A]) -> None: pass
def accepts_b(x: Dict[str, B]) -> None: pass
def accepts_c(x: Dict[str, C]) -> None: pass

my_dict: Dict[str, B] = {"foo": B()}

# error: Argument 1 to "accepts_a" has incompatible type "Dict[str, B]"; expected "Dict[str, A]"
# note: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance
# note: Consider using "Mapping" instead, which is covariant in the value type
accepts_a(my_dict)

# Type checks! No error.
accepts_b(my_dict)

# error: Argument 1 to "accepts_c" has incompatible type "Dict[str, B]"; expected "Dict[str, C]"
accepts_c(my_dict)

Only the call to accept_b succeeds, which is consistent with the expected variance.


To answer your question about how to set the variance -- mypy is designed so that the variance of data structures is set at definition time and cannot really be altered at call time.

So since Dict was defined to be invariant, you can't really change after-the-fact to be either covariant or invariant.

For more details about setting variance at definition-time, see the mypy reference docs on generics.

As you pointed out, you can declare you want to accept a read-only version of the Dict by using Mapping. It's generally the case that there's a read-only version of any PEP 484 data structure you might want to use -- e.g. Sequence is the read-only version of List.

AFAIK there's no default write-only version of Dict though. But you can sort of hack one together yourself by using protocols, a hopefully-soon-to-be-standardized method of doing structural, rather than nominal, typing:

from typing import Dict, TypeVar, Generic
from typing_extensions import Protocol

K = TypeVar('K', contravariant=True)
V = TypeVar('V', contravariant=True)

# Mypy requires the key to also be contravariant. I suspect this is because
# it cannot actually verify all types that satisfy the WriteOnlyDict
# protocol will use the key in an invariant way.
class WriteOnlyDict(Protocol, Generic[K, V]):
    def __setitem__(self, key: K, value: V) -> None: ...

class A: pass
class B(A): pass
class C(B): pass

# All three functions accept only objects that implement the
# __setitem__ method with the signature described in the protocol.
#
# You can also use only this method inside of the function bodies,
# enforcing the write-only nature.
def accepts_a(x: WriteOnlyDict[str, A]) -> None: pass
def accepts_b(x: WriteOnlyDict[str, B]) -> None: pass
def accepts_c(x: WriteOnlyDict[str, C]) -> None: pass

my_dict: WriteOnlyDict[str, B] = {"foo": B()}

#  error: Argument 1 to "accepts_a" has incompatible type "WriteOnlyDict[str, B]"; expected "WriteOnlyDict[str, A]"
accepts_a(my_dict)

# Both type-checks
accepts_b(my_dict)
accepts_c(my_dict)


To answer your implicit question ("How do I get mypy to detect the type error here/properly type check my code?"), the answer is "simple" -- just avoid using Any at all costs. Every time you do, you're intentionally opening a hole in the type system.

For example, a more type-safe way of declaring that your dict's values can be anything would have been to use Dict[str, object]. And now, mypy would have flagged the call to add_items function as being un-typesafe.

Or alternatively, consider using TypedDict if you know your values are going to be heterogeneous.

You can even make mypy disallow certain usages of Any by enabling the Disable dynamic typing family of command-line flags/config file flags.

That said, in practice, completely disallowing the use of Any is often unrealistic. Even if you can meet this ideal in your code, many 3rd party libraries are either unannotated or not fully annotated, which means they resort to using Any all over the place. So expunging their use altogether unfortunately tends to end up requiring a lot of extra work.

这篇关于您可以在Python类型注释中指定差异吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆