我刚刚开始接受 Python 类型提示,但我很困惑如何为以下函数签名实现参数验证:
def read_file(file: Union[str, PathLike, TextIO]) -> str:
我对 pythonic 实现的最初尝试如下:
def read_file(file: Union[str, PathLike, TextIO]) -> str:
try:
with open(file) as fileIO:
return read_file(fileIO)
except TypeError:
return file.read()
虽然这看起来像是
read_file
的完全有效的 Pythonic 实现,但类型检查器对此进行了全面检查。这是可以理解的,因为 open
不接受除 TypeIO
之外的任何可能类型,并且没有任何东西可以缩小类型范围(尽管令人惊讶的是 except TypeError:
没有被考虑)。
所以我放弃了“请求原谅”,而是尝试明确地检查论点:
def read_file(file: Union[str, PathLike, TextIO]) -> str:
if isinstance(file, TextIO):
return file.read()
else:
with open(file) as fileIO:
return read_file(fileIO)
这扭转了整个逻辑并检查文件是否为
TextIO
,因此类型检查器很高兴。问题是,这实际上在运行时没有任何意义,因为 TextIO
实际上并不是您可以检查的基类,而是仅用于类型提示的类型。
现在我开始感到非常困惑,因为我意识到我实际上不知道如何在运行时检查某些东西是否是
TextIO
。我挖了各种兔子洞来检查变量是类似路径还是类似文件,但这一切感觉就像我在这里遗漏了一些基本的东西。我的意思是,如果类型检查器可以提前知道某些东西是 TextIO
那么在实现中缩小类型范围怎么会很难呢?
这一定是在各种库中完成的事情,但我发现大多数实现都使用模糊检查
read
和可迭代等。也许是为了向后兼容,但我的目标是 python 3.9+,所以希望通过现在可能有更好的解决方案。
注意:作为澄清,给出评论和现有答案。我不是问打开文本文件的正确实现是什么。我也不是问如何使用类型提示一般来说:我知道你应该使用类型缩小来让类型检查器满意。
我的问题是:您可以在代码中使用哪些函数或表达式来专门对
TextIO
类型提示进行类型缩小,以便编译器乐意丢弃变量的该类型? try.. except
不起作用,检查值是否为 TextIOBase
或 TextIO
的实例也不起作用。
io.TextIOBase
的实例:
import io
with open(__file__) as file:
print(isinstance(file, io.TextIOBase))
输出:
True
但是请注意,您在大多数实际应用程序中看到对属性
read
等的模糊检查的原因是,对于任何给定的情况,通常只需要 typing.IO
中定义的 20 个抽象方法中的少数几个。应用。这就是为什么接受文件对象的函数的文档通常将它们称为file-like对象,因为它们通常只需要对象中的一些方法,例如read
、tell
、readline
等,并且许多类似文件的类的实现并不是真正继承自 io.IOBase
,因此鸭子类型检查通常优于严格的 isinstance
检查。
使用类型缩小,非常仔细地注意如何在类型缩小块的子句中排序类型测试。具体来说,对于难以测试的类型,使用“catch-all”子句来捕获类型,即:
else
块中的if...elif...else
子句;_
块中的
通配符模式
match...case
。以下示例可以在 mypy-play.net 和 pyright-play.net 上查看:
import os
import typing as t
def read_file(file: str | os.PathLike[str] | t.TextIO) -> str:
if isinstance(file, (str, os.PathLike)): # Type(s) which can be tested at runtime
with open(file) as fileIO:
return read_file(fileIO)
else: # Type(s) which can't be tested at runtime
return file.read()
如果您的类型检查器支持使用
hasattr
缩小类型,这也可能是一个选项(mypy-play.net):
import os
import typing as t
def read_file(file: str | os.PathLike[str] | t.TextIO) -> str:
if hasattr(file, "read"): # Not `str` or `os.PathLike`, as they don't have the `read` attribute
return file.read()
else:
with open(file) as fileIO:
return read_file(fileIO)