Вызовы функций как аннотации полей Python - PullRequest
0 голосов
/ 09 ноября 2019

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

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

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

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

Как PEP 563говорит, что " использование для аннотаций, несовместимых с вышеупомянутыми ПКП, должно рассматриваться как устаревшее. ". Я хотел бы принять это, чтобы означать, что аннотации в основном предназначены для указания типов, но я не вижу ничего в любом из 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))

Ответы [ 2 ]

1 голос
/ 10 ноября 2019

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

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

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

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

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

# 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, описанный в, по-видимому, простопринято 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

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


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

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

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

0 голосов
/ 10 ноября 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
...