Учитывая декоратор статического свойства:
class static_property:
def __init__(self, getter):
self.__getter = getter
def __get__(self, obj, objtype):
return self.__getter(objtype)
@staticmethod
def __call__(getter_fn):
return static_property(getter_fn)
Это применяется к классу следующим образом:
class Foo:
@static_prop
def bar(self) -> int:
return 10
Добавить называется статическим:
>>> print(Foo.bar)
10
Как мне добавить поддержку набора текста в static_property
, чтобы Foo.bar
выводился как тип int
, а не как Any
?
Или есть другой способ создать декоратор для поддержки вывода типов?
См. Также: как определить поле класса в python, которое является экземпляром класса
@chepner Хотя вы правы в отношении особого случая property
, фактический вариант использования здесь — это просто настраиваемый дескриптор. В зависимости от того, каковы именно ваши цели и в чем вы готовы пойти на компромисс, вы определенно можете создать его общим способом, чтобы разрешить вывод типа, возвращаемого базовым геттером. Ниже я показал несколько идей.
Мое предпочтительное решение: (проверено с Python 3.9
-3.11
)
from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar
R = TypeVar("R")
class static_property(Generic[R]):
def __init__(self, getter: Callable[[], R]) -> None:
self.__getter = getter
def __get__(self, obj: object, objtype: type) -> R:
return self.__getter()
@staticmethod
def __call__(getter_fn: Callable[[], R]) -> static_property[R]:
return static_property(getter_fn)
class Foo:
@static_property
def bar() -> int: # type: ignore[misc]
return 10
Подробности в разделе 3).
То, как написан ваш код в настоящее время, предполагает, что вы хотите создать дескриптор, которым вы можете украшать методы класса, о чем свидетельствует тот факт, что вы передаете objtype
своей функции-получателю внутри __get__
.
С другой стороны, имя вашего класса дескриптора static_property
и тот факт, что bar
эффективно статичен (ничего не делает с экземпляром или классом), предполагает, что вы на самом деле хотите декорировать им статические методы. Это потребует совсем другого подхода.
Но обо всем по порядку, решение для методов, принимающих класс в качестве единственного аргумента.
Этого можно добиться, сделав ваш класс дескриптора static_property
универсальным с точки зрения возвращаемого типа R
декорируемого метода (в следующем примере int
и str
).
from collections.abc import Callable
from typing import Any, Generic, TypeVar
R = TypeVar("R")
class static_property(Generic[R]):
def __init__(self, getter: Callable[[Any], R]) -> None:
self.__getter = getter
def __get__(self, obj: object, objtype: type) -> R:
return self.__getter(objtype)
@staticmethod
def __call__(getter_fn: Callable[[Any], R]) -> "static_property[R]":
return static_property(getter_fn)
class Foo:
@static_property
def bar(cls) -> int:
return 10
@static_property
def baz(cls) -> str:
return "a"
x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)
Выполнение этого через mypy --strict
дает следующий результат:
note: Revealed type is "builtins.int"
note: Revealed type is "builtins.str"
Success: no issues found in 1 source file
Технически мы уже сталкиваемся с проблемой, потому что методы bar
и baz
по-прежнему рассматриваются средством проверки типов как обычные методы экземпляра, а это означает, что он ожидает, что первый аргумент будет экземпляром Foo
, а не самим классом. Вот почему я выбрал Any
в качестве параметра для Callable
в методах дескриптора.
В данном случае это не имеет особого значения, поскольку (как я упоминал выше) методы в любом случае фактически статичны, поэтому первый аргумент не имеет смысла. Что еще более важно, поскольку методы bar
и baz
используются только в своей несвязанной форме внутри дескриптора __get__
, аннотации технически все еще верны.
Предполагая, что вы действительно хотите static_property
декорировать статические методы, то есть в данном случае методы, которые действительно не принимают никаких аргументов, для этого потребуется другой подход.
Поскольку встроенный класс staticmethod
является подтипом Callable
(из-за того, что он, очевидно, поддерживает протокол __call__
), мы можем декорировать методы bar
и baz
с помощью staticmethod
и передавать полученные объекты нашему собственному static_property.__call__
.
Мы можем сохранить R
, потому что typeshed также определяет staticmethod
как общий с точки зрения типа возвращаемого значения переданного ему метода. (По крайней мере, для Python 3.10+
.) Это означает, что мы можем сохранить информацию о типе возвращаемого значения наших методов bar
и baz
, даже если мы украсим их staticmethod
.
from collections.abc import Callable
from typing import Generic, TypeVar
R = TypeVar("R")
class static_property(Generic[R]):
def __init__(self, getter: Callable[[], R]) -> None:
self.__getter = getter
def __get__(self, obj: object, objtype: type) -> R:
return self.__getter()
@staticmethod
def __call__(getter_fn: Callable[[], R]) -> "static_property[R]":
return static_property(getter_fn)
class Foo:
@static_property
@staticmethod
def bar() -> int:
return 10
@static_property
@staticmethod
def baz() -> str:
return "a"
x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)
Вывод mypy --strict
остается таким же, как и в приведенном выше, то есть типы правильно выводятся как int
и str
соответственно.
Как вы можете видеть, функция-получатель теперь может быть просто аннотирована как Callable[[], R]
, то есть вызываемая функция не принимает аргументов, но возвращает аргумент типа нашего дескриптора.
staticmethod
Последний шаг — сделать ненужным использование декоратора staticmethod
. К сожалению, этого нельзя достичь, создав подкласс и снова явно определив наш подкласс как общий с точки зрения R
. В основном это связано с тем, что staticmethod.__get__
возвращает callable (снова см. Typeshed для справки), но мы явно хотим, чтобы он возвращал R
.
Тогда на нашем пути стоит то, что mypy
(и, предположительно, другие средства проверки типов) ожидают, что каждый метод, не украшенный staticmethod
, будет принимать по крайней мере один аргумент. Таким образом, мы получим жалобы, если определим bar
и baz
как не принимающие аргументов.
До сих пор я не нашел способа обойти это, кроме как явно игнорировать эту разную ошибку и действовать следующим образом.
from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar
R = TypeVar("R")
class static_property(Generic[R]):
def __init__(self, getter: Callable[[], R]) -> None:
self.__getter = getter
def __get__(self, obj: object, objtype: type) -> R:
return self.__getter()
@staticmethod
def __call__(getter_fn: Callable[[], R]) -> static_property[R]:
return static_property(getter_fn)
class Foo:
@static_property
def bar() -> int: # type: ignore[misc]
return 10
@static_property
def baz() -> str: # type: ignore[misc]
return "a"
x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)
Типы, очевидно, снова выводятся правильно, но mypy
будет жаловаться без директив type: ignore
, говоря error: Method must have at least one argument
. Однако дескриптор по-прежнему работает нормально, и здесь нет реальной проблемы безопасности типов. Просто чтобы было ясно, что type: ignore
из-за отсутствия параметра в подписях bar
и baz
, который mypy
обычно считает небезопасным.
Причина, по которой это на самом деле безопасно для типов, заключается в том, что мы снова имеем дело только с несвязанными методами bar
и baz
. Декоратор «поглощает» их, а это значит, что они никогда не будут привязаны к своему классу или его экземпляру. Таким образом, наши методы не нуждаются ни в каких аргументах.
(Кстати, я просто использовал __future__.annotations
здесь, чтобы избежать кавычек вокруг static_property[R]
. Вы, очевидно, можете сделать то же самое с решениями 1) и 2) выше.)
В конечном счете кажется, что вам придется решить, какое решение наиболее полезно для вас. У каждого есть свои минусы. Похоже, что мы не можем сделать это на 100 % прямо сейчас, но, может быть, кто-то другой найдет способ, или, может быть, система типизации изменится таким образом, чтобы обеспечить лучшее решение.
Блестяще! Можно ли расширить это для работы с @class_property
, чтобы def bar(cls) -> int:
можно было украсить @class_property
?
Не знаю почему, но раздел 2 не работает на питоне 3.8
.
Я не думаю, что вы можете.
mypy
должен быть специально закодирован, чтобы знать оproperty
, потому что то, что делаетproperty
, не является статическим свойством кода.