我最近了解到我可以使用
Literal[True]
和 Literal[False]
参数重载返回类型。我正在实现自己的 subprocess.Popen
接口,但无法将 self.stdin
返回类型重载为 IO[bytes]
或 IO[str]
,具体取决于构造函数中 text
的值。我正在使用 Pyright 进行静态类型检查。
我尝试过以下方法:
from typing import IO, Literal, Optional, overload
class MyPopen:
@overload
def __init__(self, text: Literal[False] = False):
self.stdin: Optional[IO[bytes]]
@overload
def __init__(self, text: Literal[True] = True):
self.stdin: Optional[IO[str]]
def __init__(self, text: bool = False):
self.stdin = None
pp = MyPopen(text=True)
assert pp.stdin
pp.stdin.write("text") # should be ok
pp.stdin.write(b"text") # should error
pp = MyPopen(text=False)
assert pp.stdin
pp.stdin.write("text") # should error
pp.stdin.write(b"text") # should be ok
但是,它假定
pp.stdin
是 IO[str]
两次,并且声明 stdin 被隐藏:
$ pyright /dev/stdin <1.py
/dev/stdin
/dev/stdin:7:14 - error: Declaration "stdin" is obscured by a declaration of the same name (reportRedeclaration)
/dev/stdin:20:1 - error: No overloads for "write" match the provided arguments (reportCallIssue)
/dev/stdin:20:16 - error: Argument of type "Literal[b"text"]" cannot be assigned to parameter "__s" of type "str" in function "write"
"Literal[b"text"]" is incompatible with "str" (reportArgumentType)
/dev/stdin:24:1 - error: No overloads for "write" match the provided arguments (reportCallIssue)
/dev/stdin:24:16 - error: Argument of type "Literal[b"text"]" cannot be assigned to parameter "__s" of type "str" in function "write"
"Literal[b"text"]" is incompatible with "str" (reportArgumentType)
5 errors, 0 warnings, 0 informations
如何根据参数的值重载类实例成员类型?
注意它如何与 subprocess.Popen 一起使用:
import subprocess
pp = subprocess.Popen("", text=True)
assert pp.stdout
pp.stdout.write("text") # ok
pp.stdout.write(b"text") # error
pp = subprocess.Popen("", text=False)
assert pp.stdout
pp.stdout.write("text") # error
pp.stdout.write(b"text") # ok
我尝试阅读子进程源代码https://github.com/python/cpython/blob/main/Lib/subprocess.py#L1015,但我不知道类型注释存储在哪里。
我认为这不是
@overload
的有效使用。它用于为函数指定更精确的签名,而不是有条件地设置属性的类型。
但是,您可以创建两个空子类,一个用于
str
,一个用于 bytes
,然后让 MyPopen.__new__()
返回其中任何一个,并在 that上使用
@overload
。除了充当预表之外,他们不需要做任何事情;逻辑仍将在 MyPopen
中定义。
# Or T = TypeVar('T', str, bytes); class MyPopen(Generic[T]): ...
class MyPopen[T: (str, bytes)]:
stdin: IO[T] | None
# Now you can use @overload!
@overload
def __new__(cls, text: Literal[False]) -> _BytesPopen:
...
@overload
def __new__(cls, text: Literal[True]) -> _StrPopen:
...
@overload
def __new__(cls) -> _BytesPopen:
...
def __new__(cls, text: bool = False) -> _BytesPopen | _StrPopen:
if text is True:
subclass = _StrPopen
else:
subclass = _BytesPopen
# Use `object.__new__()` or `type: ignore` to pass strict mode
instance = super().__new__(subclass)
instance.stdin = None
return instance
def write(self, output: T) -> None:
...
class _StrPopen(MyPopen[str]):
pass
class _BytesPopen(MyPopen[bytes]):
pass
结果将是:
pp = MyPopen()
assert pp.stdin
pp.stdin.write("text") # error: "Literal['text']" is incompatible with "bytes"
pp.stdin.write(b"text") # fine
pp = MyPopen(text=False)
assert pp.stdin
pp.stdin.write("text") # error: "Literal['text']" is incompatible with "bytes"
pp.stdin.write(b"text") # fine
pp = MyPopen(text=True)
assert pp.stdin
pp.stdin.write("text") # fine
pp.stdin.write(b"text") # error: "Literal[b"text"]" is incompatible with "str"