如何表示和强制一个类有两种操作模式,每种模式都有一些有效和无效的方法

2024-06-26 08:32:24 发布

您现在位置:Python中文网/ 问答频道 /正文

我对Python中的类型检查非常陌生。我想找到一种方法来使用它检查这种常见情况:

  1. 类(例如my DbQuery类)被实例化,处于某种未初始化状态。e、 我是一个数据库查询器,但我还没有连接到数据库。您可以说(抽象地)该实例属于“未连接的Db查询连接器”类型
  2. 用户调用.connect(),该函数将类实例设置为connected。现在可以将该类实例视为属于一个新类别(协议?)。现在可以说该实例的类型为“Connected DB Query Connector”
  3. 用户调用.query()等使用该类。对查询方法进行注释以表示在这种情况下,self必须是“Connected DB query Connector”

在一个不正确的用法中,我想自动检测:用户实例化db连接器,然后调用query(),而不首先调用connect

是否有带注释的表示?我可以表示connect()方法导致“self”加入一个新类型吗?还是这样做是正确的

是否有其他标准机制用于在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来表达这一点吗


Tags: 实例selfnonehost类型connectorisdef
1条回答
网友
1楼 · 发布于 2024-06-26 08:32:24

通过使用文字枚举将A类设置为泛型类型,(ab)并注释self参数,您可能可以将一些东西组合在一起,但坦率地说,我认为这不是一个好主意

Mypy通常假设调用一个方法不会改变方法的类型,如果不借助大量的hacks和强制转换或# 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)使用文字类型、泛型和自注释,并保持在只能返回具有新特性的实例的约束范围内

例如:

# 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")

基本上,只要我们坚持使用返回新实例(即使在运行时,我们总是只返回“self”),就可以使类型或多或少地充当状态机

再聪明一点/再折衷一点,你甚至可以在从一个州过渡到另一个州的时候摆脱演员阵容

但是我认为这类把戏对你似乎要做的事情来说是过分的/不合适的。我个人推荐两个类+contextmanager方法

相关问题 更多 >