作为Python字段注释的函数调用

问题描述 投票:0回答:2

我正在研究一个小模块,以利用注释来包含额外的内容通过使用函数调用作为注释来获取有关类字段的数据(请参见代码下面)。我正在努力做到这一点,同时保持与静态类型检查的兼容性。 (旁注:我是在充分了解PEP 563和推迟评估注释的情况下进行此操作的)

我已经通过mypy 0.670和pycharm 2019.2.4运行了以下代码。 mypy在value字段的声明中报告“ 错误:无效的类型注释或批注”。但是,pycharm推断value字段是一个整数。

pycharm似乎已确定函数调用的结果its_an_int()是类型int,因此可以将字段视为用于静态类型检查和其他IDE功能的整数。这是理想的我希望Python类型检查可以完成什么。

我主要依靠pycharm,不使用mypy。但是,我很谨慎关于与该设计是否冲突的考虑对于类型注释,“理智”,尤其是在其他类型检查器如mypy对此将失败。

如PEP 563所述,“ 用于与上述PEP不兼容的注释的使用应视为已弃用。”。我想说的是,注释主要是用于指示类型的,但是我看不到任何PEP都以其他方式阻止注释中表达式的使用。据推测,可以静态分析的表达式本身就是可接受的注释。

期望下面的value字段可以由当前为Python定义的静态分析推断为整数3.8(至4.0)? mypy的分析过于严格还是有限?或者是pycharm是自由主义者吗?

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))
python annotations pycharm static-analysis mypy
2个回答
0
投票

以下内容可能会满足您的要求;我不确定。基本上,将存在函数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

0
投票

[您使用的语法似乎不太可能与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中描述的新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的代码与尚未使用此代码的代码互操作。
© www.soinside.com 2019 - 2024. All rights reserved.