我有一个大致如下所示的函数:
import datetime
from typing import Union
class Sentinel(object): pass
sentinel = Sentinel()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Sentinel] = sentinel,
) -> str:
if as_tz is not sentinel:
# Never reached if as_tz has wrong type (Sentinel)
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
此处使用
sentinel
值是因为 None
已经是 .astimezone()
的有效参数,因此目的是正确识别用户根本不想调用 .astimezone()
的情况。
但是,
mypy
抱怨这种模式:
错误:“datetime”的“astimezone”的参数 1 具有不兼容的类型 “联盟[tzinfo,无,哨兵]”;预期“可选[tzinfo]”
datetime
存根(理所当然)使用:
def astimezone(self, tz: Optional[_tzinfo] = ...) -> datetime: ...
但是,有没有办法让 mypy 知道由于
sentinel
检查,.astimezone()
值永远不会传递给 if
? 或者这是否只需要一个# type: ignore
而没有更干净的方法?
另一个例子:
from typing import Optional
import requests
def func(session: Optional[requests.Session] = None):
new_session_made = session is None
if new_session_made:
session = requests.Session()
try:
session.request("GET", "https://a.b.c.d.com/foo")
# ...
finally:
if new_session_made:
session.close()
第二个,像第一个一样,是“运行时安全的”(因为缺乏更好的术语):调用
AttributeError
和 None.request()
的 None.close()
将不会被达到或评估。 然而,mypy 仍然抱怨:
mypytest.py:9: error: Item "None" of "Optional[Session]" has no attribute "request"
mypytest.py:13: error: Item "None" of "Optional[Session]" has no attribute "close"
我应该在这里做一些不同的事情吗?
根据我的经验,最好的解决方案是使用
enum.Enum
。
一个好的哨兵模式有 3 个属性:
object()
is
和 is not
enum.Enum
经过 mypy 的特殊处理,因此它是我发现的唯一可以实现所有这三个要求并在 mypy 中正确验证的解决方案。
import datetime
import enum
from typing import Union
class Sentinel(enum.Enum):
SKIP_TZ = object()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Sentinel] = Sentinel.SKIP_TZ,
) -> str:
if as_tz is not Sentinel.SKIP_TZ:
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
这个解决方案还有一些其他有趣的属性。
sentinel.py
import enum
class Sentinel(enum.Enum):
sentinel = object()
main.py
import datetime
from sentinel import Sentinel
from typing import Union
SKIP_TZ = Sentinel.sentinel
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Sentinel] = SKIP_TZ,
) -> str:
if as_tz is not SKIP_TZ:
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
请注意,由于
Sentinel.sentinel
始终提供相同的 object
实例,因此两个可重用哨兵永远不应该在相同的上下文中使用。
Literal
用
Sentinel
替换 Literal[Sentinel.SKIP_TZ]]
可以使函数签名更加清晰,尽管这确实是多余的,因为只有一个枚举值。
import datetime
import enum
from typing import Union
from typing_extensions import Literal
class Sentinel(enum.Enum):
SKIP_TZ = object()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Literal[Sentinel.SKIP_TZ]] = Sentinel.SKIP_TZ,
) -> str:
if as_tz is not Sentinel.SKIP_TZ:
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
func(datetime.datetime.now(), as_tz=Sentinel.SKIP_TZ)
import datetime
from typing import Union
class SentinelType:
pass
SKIP_TZ = SentinelType()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, SentinelType] = SKIP_TZ,
) -> str:
if not isinstance(dt, SentinelType):
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
虽然这有效,但使用
isinstance(dt, SentinelType)
无法满足要求 3(“使用 is
”),因此也无法满足要求 2(“使用命名常量”)。 为了清楚起见,我希望能够使用 if dt is not SKIP_TZ
。
Literal
Literal
不适用于任意值(尽管它适用于枚举。见上文。)
import datetime
from typing import Union
from typing_extensions import Literal
SKIP_TZ = object()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Literal[SKIP_TZ]] = SKIP_TZ,
) -> str:
if dt is SKIP_TZ:
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
产生以下 mypy 错误:
error: Parameter 1 of Literal[...] is invalid
error: Variable "sentinel.SKIP_TZ" is not valid as a type
Literal
在这次尝试中,我使用了字符串文字而不是对象:
import datetime
from typing import Union
from typing_extensions import Literal
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Literal['SKIP_TZ']] = 'SKIP_TZ',
) -> str:
if as_tz is not 'SKIP_TZ':
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
func(datetime.datetime.now(), as_tz='SKIP_TZ')
即使这有效,它对于要求 1 来说也相当薄弱。
但是它没有在 mypy 中传递。 它产生错误:
error: Argument 1 to "astimezone" of "datetime" has incompatible type "Union[tzinfo, None, Literal['SKIP_TZ']]"; expected "Optional[tzinfo]"
cast
:
from typing import cast
...
if as_tz is not sentinel:
# Never reached if as_tz has wrong type (Sentinel)
as_tz = cast(datetime.tzinfo, as_tz)
dt = dt.astimezone(as_tz)
和
new_session_made = session is None
session = cast(requests.Session, session)
您也可以使用
assert
(尽管这是实际的运行时检查,而 cast
更明确地说是无操作):
assert isinstance(as_tz, datetime.tzinfo)
dt = dt.astimezone(as_tz)
和
new_session_made = session is None
assert session is not None
绕过此问题的一种方法是执行以下操作:
from typing import Optional
import requests
def func(session: Optional[requests.Session] = None) -> None:
new_session = session is None
if not session:
session = requests.Session()
try:
session.request("GET", "https://a.b.c.d.com/foo")
# other stuff
finally:
if not new_session:
session.close()
此外,我们还可以检查
mypy
是否可以处理使用不同参数类型的情况:
func('a') # mypy_typing.py:14: error: Argument 1 to "func" has incompatible type "str"; expected "Optional[Session]"
func(1) # mypy_typing.py:14: error: Argument 1 to "func" has incompatible type "int"; expected "Optional[Session]"
...
# PS: The test will break for any kind of types except for None and requests.Session
...
但是,如果我们使用
None
或 request.Session()
对象作为参数,则测试会顺利通过:
func(None) # No errors
func(requests.Session()) # No errors
有关更多信息,您可以阅读 mypy
官方文档中的
示例。
Mypy 对
isinstance
有特殊处理。除了检查身份之外,还可以这样做:
if not isinstance(as_tz, Sentinel):
dt = dt.astimezone(as_tz)
...
通过此更改,您的示例似乎可以进行类型检查。