我对于在Python中进行类型检查非常陌生。我想找到一种方法来检查这种常见情况:
在一种不正确的用法中,我想自动检测:用户实例化数据库连接器,然后在不先调用connect的情况下调用query()。
是否有带注释的表示形式?我可以表达connect()方法导致“自我”加入新类型吗?还是这样做的正确方法?
还有其他一些标准的机制可以在Python或mypy中表达和检测它吗?
我也许能够看到如何通过继承来表达这一点……我不确定
提前感谢!
编辑:
这是我希望我能做的:
from typing import Union, Optional, NewType, Protocol, cast
class Connector:
def __init__(self, host: str) -> None:
self.host = host
def run(self, sql: str) -> str:
return f"I ran {sql} on {self.host}"
# This is a version of class 'A' where conn is None and you can't call query()
class NoQuery(Protocol):
conn: None
# This is a version of class 'A' where conn is initialized. You can query, but you cant call connect()
class CanQuery(Protocol):
conn: Connector
# This class starts its life as a NoQuery. Should switch personality when connect() is called
class A(NoQuery):
def __init__(self) -> None:
self.conn = None
def query(self: CanQuery, sql: str) -> str:
return self.conn.run(sql)
def connect(self: NoQuery, host: str):
# Attempting to change from 'NoQuery' to 'CanQuery' like this
# mypy complains: Incompatible types in assignment (expression has type "CanQuery", variable has type "NoQuery")
self = cast(CanQuery, self)
self.conn = Connector(host)
a = A()
a.connect('host.domain')
print(a.query('SELECT field FROM table'))
b = A()
# mypy should help me spot this. I'm trying to query an unconnected host. self.conn is None
print(b.query('SELECT oops'))
对我来说,这是一个常见的情况(一个对象具有一些不同且非常有意义的操作模式)。没有办法用mypy表达这一点?
[您可以通过将A
类设为通用类型,(ab)使用Literal枚举,并注释self参数,来一起破解某些东西,但坦率地说,我认为这不是一个好主意。
[Mypy通常认为调用方法不会改变方法的类型,并且如果不借助粗暴的技巧和大量的强制转换或# type: ignore
,可能无法绕开它。
相反,标准约定是使用两个类(“连接”对象和“查询”对象)以及上下文管理器。作为附带的好处,它还可以让您确保使用完毕后始终关闭连接。
例如:
from typing import Union, Optional, Iterator
from contextlib import contextmanager
class RawConnector:
def __init__(self, host: str) -> None:
self.host = host
def run(self, sql: str) -> str:
return f"I ran {sql} on {self.host}"
def close(self) -> None:
print("Closing connection!")
class Database:
def __init__(self, host: str) -> None:
self.host = host
@contextmanager
def connect(self) -> Iterator[Connection]:
conn = RawConnector(self.host)
yield Connection(conn)
conn.close()
class Connection:
def __init__(self, conn: RawConnector) -> None:
self.conn = conn
def query(self, sql: str) -> str:
return self.conn.run(sql)
db = Database("my-host")
with db.connect() as conn:
conn.query("some sql")
如果您真的想将这两个新类合并为一个类,则可以通过(ab)使用文字类型,泛型和自我注释,并在一定范围内限制您只能使用新个性使用return实例。
例如:
# If you are using Python 3.8+, you can import 'Literal' directly from
# typing. But if you need to support older Pythons, you'll need to
# pip-install typing_extensions and import from there.
from typing import Union, Optional, Iterator, TypeVar, Generic, cast
from typing_extensions import Literal
from contextlib import contextmanager
from enum import Enum
class RawConnector:
def __init__(self, host: str) -> None:
self.host = host
def run(self, sql: str) -> str:
return f"I ran {sql} on {self.host}"
def close(self) -> None:
print("Closing connection!")
class State(Enum):
Unconnected = 0
Connected = 1
# Type aliases here for readability. We use an enum and Literal
# types mostly so we can give each of our states a nice name. We
# could have also created an empty 'State' class and created an
# 'Unconnected' and 'Connected' subclasses: all that matters is we
# have one distinct type per state/per "personality".
Unconnected = Literal[State.Unconnected]
Connected = Literal[State.Connected]
T = TypeVar('T', bound=State)
class Connection(Generic[T]):
def __init__(self: Connection[Unconnected]) -> None:
self.conn: Optional[RawConnector] = None
def connect(self: Connection[Unconnected], host: str) -> Connection[Connected]:
self.conn = RawConnector(host)
# Important! We *return* the new type!
return cast(Connection[Connected], self)
def query(self: Connection[Connected], sql: str) -> str:
assert self.conn is not None
return self.conn.run(sql)
c1 = Connection()
c2 = c1.connect("foo")
c2.query("some-sql")
# Does not type check, since types of c1 and c2 do not match declared self types
c1.query("bad")
c2.connect("bad")
基本上,只要我们坚持使用[[returning新实例(即使在运行时,我们总是只返回'self'),就可以使类型或多或少地充当状态机。
有了一些聪明/一些折衷,您甚至可以在从一种状态过渡到另一种状态时摆脱强制转换。但是tbh,我认为这种技巧过分/可能不适合您似乎想做的事情。我个人将推荐两个类+ contextmanager方法。