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

问题描述 投票:0回答:1

我对于在Python中进行类型检查非常陌生。我想找到一种方法来检查这种常见情况:

  1. 类(例如,我的DbQuery类)已实例化,处于某些未初始化状态。例如我是数据库查询员,但尚未连接数据库。您可以说(抽象地),该实例的类型为“未连接的Db查询连接器”
  2. 用户调用.connect(),将类实例设置为已连接。现在可以认为该类实例属于新类别(协议?)。您可以说实例现在为“ Connected DB Query Connector”类型...
  3. 用户调用.query()等使用该类。带注释的查询方法表示在这种情况下,self必须是“ Connected DB Query Connector”]

在一种不正确的用法中,我想自动检测:用户实例化数据库连接器,然后在不先调用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表达这一点?

python mypy
1个回答
0
投票

[您可以通过将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方法。

© www.soinside.com 2019 - 2024. All rights reserved.