Возможно, вы сможете что-то взломать, сделав свой класс 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")
По сути, становится возможным заставить тип действовать более или менее как конечный автомат, пока мы придерживаемся , возвращая новых экземпляров (даже если во время выполнения, мы всегда возвращаем только «себя»).
С немного большей хитростью / несколькими компромиссами вы можете даже избавиться от актерского состава при каждом переходе из одного состояния в другое.
Но, я считаю, что этот вид уловки является излишним /, вероятно, неуместным для того, что вы пытаетесь сделать. Я лично рекомендовал бы подход «два класса + контекстный менеджер».