【问题标题】:Function for restricting the type of attributes in an object (Python, mypy)用于限制对象中属性类型的函数(Python、mypy)
【发布时间】:2021-11-27 07:40:27
【问题描述】:

示例设置:

from typing import Optional


class A(object):
    def __init__(self):
        self.a: Optional[int] = None
        
    def check_a(self) -> bool:
        return self.a is not None
        
a = A()
if a.check_a():
    print(a.a + 1)  # error: Unsupported operand types for + ("None" and "int")

check_a 方法检查a 是什么类型的变量,但mypy 没有看到这一点并写入错误。 TypeGuard 不会有帮助,因为它可以创建一个检查类型的函数,而不是一个检查对象变量类型的函数

是否有可能以某种方式让 mypy 注意到这一点,以便使用该函数检查变量 self.a 的类型,而无需在检查中明确引用它? (使用if a.a_check 而不是if a.a is not None)?

【问题讨论】:

  • 你知道错误永远不会发生,因为在运行期间,只有当a.a 不是None 时才能执行该段代码。但是,mypy 没有考虑运行时值,从编译器的角度来看,a.a 可以是intNone。而None + 1 应该会产生错误。
  • from typing import cast print(cast(int, a.a) + 1)
  • 你确实有一个类型检查器不理解的“手动”检查,所以你还需要让类型检查器手动理解你断言的状态,这就是 cast 的用途。我认为没有其他方法可以让类型检查器理解a.check_a();但我可能错了……
  • 我尝试通过 TypeGuard 来实现,但是这里可以使用self吗?

标签: python type-hinting mypy python-typing


【解决方案1】:

使用the new TypeGuard feature 可以通过两种方式完成此操作,在 Python 3.10 中可以从 typing 导入,在早期 Python 版本中可以从 the PyPI typing_extensions package 获得。请注意,typing_extensions 已经是 Mypy 的依赖项,因此如果您使用的是 Mypy,您可能已经拥有它。

第一个选项是将您的check_a 方法更改为staticmethod,它接受一个可能是intNone 的变量作为输入,并验证它是否是int。 (抱歉,我已经更改了一些变量的名称,因为我发现有一个类 A 也有一个 a 属性非常令人困惑。)

from typing import TypeGuard, Optional

class Foo1:
    def __init__(self, bar: Optional[int] = None) -> None:
        self.bar = bar
    
    @staticmethod
    def check_bar(bar: Optional[int]) -> TypeGuard[int]:
        return bar is not None
        

f1 = Foo1()
if f1.check_bar(f1.bar):
    print(f1.bar + 1)

第二个选项是使用structural subtyping 断言Foo 类(或您的原始问题中的A 类)的实例在某个时间点具有某些属性。这需要更改测试方法,使其变为classmethod,并且设置起来稍微复杂一些,但一旦设置好,就会进行更好的检查。

from typing import TypeGuard, Optional, Protocol, TypeVar

class HasIntBar(Protocol):
    bar: int


F = TypeVar('F', bound='Foo2')


class Foo2:
    def __init__(self, bar: Optional[int] = None) -> None:
        self.bar = bar
    
    @classmethod
    def check_bar(cls: type[F], instance: F) -> TypeGuard[HasIntBar]:
        return instance.bar is not None
        

f2 = Foo2()
if Foo2.check_bar(f2): # could also write this as `if f2.check_bar(f2)`
    print(f2.bar + 1)

您可以在 Mypy Playground here 上尝试这两个选项。

【讨论】:

  • 我现在正在这样做,我想知道是否有可能以不同的方式进行,但显然不可能,谢谢
  • @Lev145 是的,我同意它不完美,但我认为这是目前可能的最好的 :)
  • 也许他们稍后会添加它
  • 似乎narrowing down self with TypeGuard is not intended on purpose:“有人担心可能存在需要对 self 和 cls 应用缩小逻辑的情况。这是一个不寻常的用例,[...] 因此决定不为此做出特殊规定。如果需要缩小 self 或 cls,则可以将该值作为显式参数传递给类型保护函数。”因此,上述解决方案可能会保留唯一可能的解决方案。
【解决方案2】:

问题在于,就类型检查器而言,您的bool 没有说明您的.a 的类型。例如,你可以写

class A:
    ...

    def check_a(self) -> bool:
        return True

但是,您可能知道,mypy 可以在 if x is not None: 中排除 None。您的问题是您在函数定义和调用站点之间拆分了 if x is not None:,因此 mypy 无法使用它来推断值不是 None

解决此问题的方法是将要执行的操作传递给A,在这种情况下,您会得到类似于其他语言中称为foreach 的内容。在Optional 的上下文中(并且A 是一个可变Optional 的包装器),这可以将函数应用于值如果它们存在

class A:
    ...

    def foreach(self, f: Callable[[int], None]) -> None:
        if self.a is not None:
            f(self.a)

a = A()
a.foreach(lambda x: print(x + 1))

注意我这里没有check_a

【讨论】:

  • 这个我明白了,但是不解决问题,在代码中使用会不方便
  • @Lev145 您可能需要添加更多信息,说明您为什么要这样做。事实上,尚不清楚您为什么不只使用if a.a is not None:。我假设您不会这样做,因为您的实际问题的某些部分不能很好地处理那些在使其成为最小示例时丢失的部分。了解更多会很有帮助。无论哪种方式,就目前而言,这个问题可能是重复的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-25
  • 1970-01-01
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 2019-02-23
相关资源
最近更新 更多