Как express и обеспечить, чтобы у класса было 2 режима работы, каждый из которых имеет несколько допустимых и недействительных методов - PullRequest
2 голосов
/ 07 января 2020

Я очень новичок в проверке типов в Python. Я хотел бы найти способ использовать его для проверки этой распространенной ситуации: экземпляр класса

  1. (например, мой класс DbQuery) находится в неинициализированном состоянии. Например, я запросил базу данных, но еще не подключился к базе данных. Можно сказать (абстрактно), что экземпляр имеет тип «Unconnected Db Query Connector»
  2. , пользователь вызывает .connect (), который устанавливает экземпляр класса подключенным. Теперь можно думать об этом экземпляре класса как о новой категории (протокол?). Можно сказать, что экземпляр имеет тип «Соединитель запроса к подключенной БД» ...
  3. пользователь вызывает .query () и т. Д. c. использует класс. Метод запроса помечен express, что self в этом случае должно быть 'Соединителем запроса к БД'

При неправильном использовании, которое я хотел бы обнаружить автоматически: пользователь создает экземпляр БД Разъем, а затем вызывает query () без вызова connect.

Существует ли для этого представление с аннотациями? Могу ли я express сообщить, что метод connect () вызвал присоединение self к новому типу? или это правильный способ сделать это?

Есть ли какой-то другой стандартный механизм для выражения этого и обнаружения его в Python или mypy?

Я мог бы увидеть, как это могло бы быть выраженным с наследованием, может быть ... Я не уверен

Заранее спасибо!

РЕДАКТИРОВАТЬ:

Вот что я могу sh Я мог бы сделать:

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'))

Для меня это обычный сценарий (объект, имеющий несколько различных и очень значимых режимов работы). Нет ли способа express это в mypy?

1 Ответ

2 голосов
/ 08 января 2020

Возможно, вы сможете что-то взломать, сделав свой класс A обобщенным типом c, (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")

По сути, становится возможным заставить тип действовать более или менее как конечный автомат, пока мы придерживаемся , возвращая новых экземпляров (даже если во время выполнения, мы всегда возвращаем только «себя»).

С немного большей хитростью / несколькими компромиссами вы можете даже избавиться от актерского состава при каждом переходе из одного состояния в другое.

Но, я считаю, что этот вид уловки является излишним /, вероятно, неуместным для того, что вы пытаетесь сделать. Я лично рекомендовал бы подход «два класса + контекстный менеджер».

...