Вызов функций как аннотации полей Python

Я работаю над небольшим модулем, чтобы использовать аннотации для включения дополнительных данных о полях класса, используя вызовы функций в качестве аннотаций (см. Код ниже). Я пытаюсь найти способ сделать это, сохранив при этом совместимость с проверкой статического типа. (Примечание: я делаю это с полным знанием PEP 563 и отложенной оценки аннотаций)

Я запустил следующий код через mypy 0.670, а также через pycharm 2019.2.4. mypy сообщает «об ошибке: недопустимый тип комментария или аннотации» в объявлении поля value. Однако pycharm предполагает, что поле значения должно быть int.

Похоже, что pycharm определила, что результатом вызова функции its_an_int() является тип int, и поэтому может обрабатывать поле как целое число для проверки статического типа и других функций IDE. Это идеальный вариант, и я надеюсь, что проверка типов Python может выполнить.

Я в первую очередь полагаюсь на pycharm и не использую mypy. Однако я осторожно отношусь к использованию этого дизайна, если он будет конфликтовать с тем, что считается «нормальным» для аннотаций типов, и особенно если другие средства проверки типов, такие как mypy, не справятся с этим.

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

Разумно ли ожидать, что поле value ниже может быть выведено как целое число с помощью статического анализа, как в настоящее время определено для Python 3.8 (по 4.0)? Mypy слишком строг или ограничен в своем анализе? Или пихарм слишком либерален?

from __future__ import annotations

import typing


def its_an_int() -> typing.Type[int]:
    # ...magic stuff happens here...
    pass


class Foo:

    # This should be as if "value: int" was declared, but with side effects
    # once the annotation is evaluted.
    value: its_an_int()

    def __init__(self, value):
        self.value = value


def y(a: str) -> str:
    return a.upper()


f = Foo(1)

# This call will fail since it is passing an int instead of a string.   A 
# static analyzer should flag the argument type as incorrect if value's type
# is known. 
print(y(f.value))

person metatheorem    schedule 09.11.2019    source источник


Ответы (2)


Кажется маловероятным, что используемый вами синтаксис будет соответствовать подсказкам типов, как определено в PEP 484.

Отчасти это связано с тем, что в PEP никогда не говорится, что использование произвольных выражений в качестве подсказок типов разрешено, а отчасти потому, что я не делаю этого, ваш пример действительно соответствует духу того, что пытается выполнить PEP 484.

В частности, одной из важных целей проектирования экосистемы типизации Python было строгое разделение между «миром времени выполнения» и миром «статических типов». В частности, всегда должна быть возможность полностью игнорировать подсказки типа во время выполнения, но это будет невозможно, если подсказки типа иногда будут иметь побочные эффекты при оценке.

Не исключено, что кто-то в конечном итоге разработает PEP, который учитывает то, что вы пытаетесь сделать, и будет успешно аргументировать его принятие, но я не думаю, что кто-то работает над таким PEP или есть большой спрос на него.

Вероятно, более каноническими способами прикрепления или записи метаданных, вероятно, было бы либо сделать операцию побочного эффекта явной, выполнив что-то вроде этого:

# Alternatively, make this a descriptor class if you want to do
# even fancier things: https://docs.python.org/3/howto/descriptor.html
def magic() -> Any:
    # magic here

class Foo:
    value: int = magic()

    def __init__(self, value):
        self.value = value

... или использовать новый тип Annotated description в явно принятом PEP 593, что позволяет сосуществовать подсказкам типа и произвольной информации, не являющейся подсказкой типа:

# Note: it should eventually be possible to import directly from 'typing' in
# future versions of Python, but for now you'll need to pip-install
# typing_extensions, the 'typing' backport.
from typing_extensions import Annotated

def magic():
    # magic here

class Foo:
    value: Annotated[int, magic()]

    def __init__(self, value):
        self.value = value

Основное предостережение этого последнего подхода заключается в том, что я не верю, что Pycharm еще поддерживает подсказку типа Annotated, учитывая, что она очень нова.


Отложив все это в сторону, стоит отметить, что не обязательно неправильно просто отклонить PEP 484 и продолжать использовать то, что Pycharm понимает. Меня немного озадачивает, что Pycharm, очевидно, может понять ваш пример (возможно, это артефакт реализации того, как Pycharm реализует анализ типов?), Но если он работает для вас и если настройка вашей кодовой базы для соответствия PEP 484 слишком болезненна, это Было бы разумно просто кататься с тем, что у вас есть.

И если вы хотите, чтобы ваш код по-прежнему был доступен для использования другими разработчиками, которые используют подсказки типа PEP 484, вы всегда можете решить распространять файлы-заглушки pyi вместе с вашим пакетом, как описано в PEP 561.

Для создания этих файлов-заглушек потребуется изрядно поработать, но заглушки действительно предлагают способ позволить коду, который отказался от использования PEP 484, взаимодействовать с кодом, который этого не сделал.

person Michael0x2a    schedule 10.11.2019
comment
Я думаю, что ваше наблюдение о разнице между средой выполнения и статическим миром - лучший ответ на мой вопрос. Мне нужна статическая проверка типов, и мое использование аннотаций, вероятно, будет слишком далеко от границ для PEP 484. Я не знаком с PEP 593. Он определенно может сработать для этого, особенно в сочетании с псевдонимами типов. Придется с этим поэкспериментировать. Я использовал дескрипторы до того, как попытался злоупотребить аннотациями, чтобы сделать эти объявления классов менее подробными. Я могу обойтись ими, если ничего не помогает. - person metatheorem; 12.11.2019

Следующее может делать то, что вы хотите; Я не уверен. По сути, существует функция test, которая будет вызывать ошибку каждый раз, когда пользователь пишет obj.memvar = y, если test(y) не вернул True. Например, foo может проверить, является ли y экземпляром класса int или нет.

import typing
import io
import inspect
import string

class TypedInstanceVar:
    def __init__(self, name:str, test:typing.Callable[[object], bool]):
        self._name = name
        self._test = test

    def __get__(descriptor, instance, klass):
        if not instance:
            with io.StringIO() as ss:
                print(
                    "Not a class variable",
                    file=ss
                )
                msg = ss.getvalue()
            raise ValueError(msg)
        return getattr(instance, "_" + descriptor._name)

    @classmethod
    def describe_test(TypedInstanceVar, test:typing.Callable[[object], bool]):
        try:
            desc = inspect.getsource(test)
        except BaseException:
            try:
                desc = test.__name__
            except AttributeError:
                desc = "No description available"
        return desc.strip()

    @classmethod
    def pretty_string_bad_input(TypedInstanceVar, bad_input):
        try:
            input_repr = repr(bad_input)
        except BaseException:
            input_repr = object.__repr__(bad_input)
        lamby = lambda ch:\
            ch if ch in string.printable.replace(string.whitespace, "") else " "
        with io.StringIO() as ss:
            print(
                type(bad_input),
                ''.join(map(lamby, input_repr))[0:20],
                file=ss,
                end=""
            )
            msg = ss.getvalue()
        return msg

    def __set__(descriptor, instance, new_val):
        if not descriptor._test(new_val):
            with io.StringIO() as ss:
                print(
                    "Input " + descriptor.pretty_string_bad_input(new_val),
                    "fails to meet requirements:",
                    descriptor.describe_test(descriptor._test),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        setattr(instance, "_" + descriptor._name, new_val)

Ниже мы видим TypedInstanceVar в использовании:

class Klass:
    x = TypedInstanceVar("x", lambda obj: isinstance(obj, int))
    def __init__(self, x):
        self.x = x
    def set_x(self, x):
        self.x = x

#######################################################################

try:
    instance = Klass(3.4322233)
except TypeError as exc:
    print(type(exc), exc)

instance = Klass(99)
print(instance.x)  # prints 99
instance.set_x(44) # no error
print(instance.x)  # prints 44

try:
    instance.set_x(6.574523)
except TypeError as exc:
    print(type(exc), exc)

В качестве второго примера:

def silly_requirement(x):
    status = type(x) in (float, int)
    status = status or len(str(x)) > 52
    status = status or hasattr(x, "__next__")
    return status

class Kalzam:
    memvar = TypedInstanceVar("memvar", silly_requirement)
    def __init__(self, memvar):
        self.memvar = memvar

instance = Kalzam("hello world")

Результат для второго примера:

TypeError: Input <class 'str'> 'hello world'
fails to meet requirements:
def silly_requirement(x):
    status = type(x) in (float, int)
    status = status or len(str(x)) > 52
    status = status or hasattr(x, "__next__")
    return status
person Toothpick Anemone    schedule 09.11.2019
comment
Мне очень жаль, но я не совсем понимаю, какое это имеет отношение к моему вопросу о статической типизации. - person metatheorem; 10.11.2019