gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 10:47:31 25 4
gpt4 key购买 nike

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

  • 类(例如我的 DbQuery 类)已实例化,处于某种未初始化状态。例如我是一个数据库查询器,但我还没有连接到一个数据库。您可以说(抽象地)该实例的类型为“未连接的 Db 查询连接器”
  • 用户调用 .connect() 将类实例设置为已连接。现在可以认为这个类实例属于一个新的类别(协议(protocol)?)。您现在可以说该实例的类型为“Connected DB Query Connector”...
  • 用户调用 .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中表达这一点?

    最佳答案

    您可以通过制作 A 一起破解某些东西。类泛型类型,(ab)使用文字枚举,并注释 self 参数,但坦率地说,我认为这不是一个好主意。

    Mypy 通常假设调用一个方法不会改变一个方法的类型,并且如果不诉诸粗俗的技巧和一堆强制转换或 # type: ignore 可能无法避免这种情况。 s。

    相反,标准约定是使用两个类——“连接”对象和“查询”对象——以及上下文管理器。作为附带的好处,这还可以让您确保在使用完连接后始终关闭它们。

    例如:

    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'),就可以使类型或多或少地充当状态机。

    有了更多的聪明/更多的妥协,你甚至可以在你从一种状态转换到另一种状态时摆脱 Actor 阵容。

    但是,我认为这种技巧对于您似乎正在尝试做的事情来说太过分了/可能不合适。我个人会推荐两个类+上下文管理器的方法。

    关于python - 如何表达和强制一个类有两种操作模式,每一种都有一些有效和无效的方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59632405/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com