简体   繁体   English

Python 允许可选属性的协议

[英]Python Protocol Allowing Optional Attributes

Suppose I have a protocol Foo .假设我有一个协议Foo

from typing import Protocol, Optional


class Foo(Protocol):
    val: Optional[int]

And a function like the following:和 function 如下所示:

def func(obj: Foo) -> None:
    if obj.val is not None:
        print(obj.val)

I would expect that either ImplA and ImplB (shown below) should be an acceptable argument for func , since val: int is a more specific than val: Optional[int] .我希望ImplAImplB (如下所示)应该是func的可接受参数,因为val: intval: Optional[int]更具体。

class ImplA:
    val: int


class ImplB:
    val: Optional[int]

a = ImplA()
b = ImplB()

func(a)  # Argument 1 to "func" has incompatible type "ImplA"; expected "Foo"
func(b)  # It works

As it doesn't work, I have to define Bar and take the union of both classes.由于它不起作用,我必须定义Bar并采用两个类的联合。 However, is there a way to construct a protocol such that both val: Optional[int] and val: int will be accepted without Union ?但是,有没有一种方法可以构建一个协议,使得val: Optional[int]val: int都可以在没有Union的情况下被接受?

class Bar(Protocol):
    val: int

def func(obj: Union[Foo, Bar]): ...

EDIT:编辑:

To achieve this, we need to make sure that the type of val is covariant.为此,我们需要确保 val 的类型是协变的。 As far as I see now, it looks like that Protocol defines it to be an invariant.据我现在所见, Protocol似乎将其定义为不变量。 Is it thus impossible to achieve my requirements?这样就无法实现我的要求了吗?

The problem is that you have defined not only a covariant getter, but a contravariant setter.问题是您不仅定义了协变 getter,还定义了逆变 setter。 Consider the following:考虑以下:

def func2(obj: Foo) -> None:
    if obj.val is not None:
        print(obj.val)
        obj.val = None  # Fine: val is optional in Foo

x = ImplA(val = 10)
func2(x)  # is x a valid Foo?
print(x.val + 1)  # runtime error!

By the type definition of ImplA, x.val is int so the typechecker must allow the final print.根据 ImplA 的类型定义, x.valint因此类型检查器必须允许最终打印。 But func2 is valid, as it's only using protocol methods.但是func2是有效的,因为它只使用协议方法。 The only way to prevent this code typechecking is to prevent x being passed to func2.防止此代码类型检查的唯一方法是防止将 x 传递给 func2。 This makes sense as it doesn't have the None-accepting val setter used in func2.这是有道理的,因为它没有 func2 中使用的 None-accepting val setter。

The following will work as you intended:以下将按您的预期工作:

class Foo(Protocol):
  @property
  def val(self) -> Optional[int]:
    ...

With this definition, the typechecker will complain at the final line of func2 , as it is no longer able to access a setter.使用此定义,类型检查器将在func2的最后一行抱怨,因为它不再能够访问 setter。 Your original example will now typecheck.您的原始示例现在将进行类型检查。


By the way, I used the terms "covariant" and "contravariant" because you did, and they do feel right, but technically your type isn't covariant or contravariant at all because it's not generic.顺便说一句,我使用了术语“协变”和“逆变”,因为您确实这样做了,而且它们确实感觉不错,但是从技术上讲,您的类型根本不是协变或逆变的,因为它不是通用的。 We could fix that with explicit use of a TypeVar:我们可以通过显式使用 TypeVar 来解决这个问题:

T_co = TypeVar("T_co", covariant=True)

class Foo(Protocol, Generic[T_co]):
  val: T_co

This would fail typechecking right here as, now your intent is obvious, the typechecker can tell you your definition is not covariant.这会在此处进行类型检查失败,因为现在您的意图很明显,类型检查器可以告诉您您的定义不是协变的。

I ran your code and it raised an error for both func(a) and func(b).我运行了你的代码,它对 func(a) 和 func(b) 都产生了错误。 The error is related to该错误与

if obj.val is not none:

because none of the objects have the attribute "val".因为没有一个对象具有属性“val”。 I tested the code with Python 3.9.9.我用 Python 3.9.9 测试了代码。

I modified the clause to test if the object has the attribute val, hopefully this helps.我修改了该子句以测试对象是否具有属性 val,希望这会有所帮助。

from typing import Protocol, Optional


class Foo(Protocol):
    val: Optional[int]


def func(obj: Foo) -> None:
    if hasattr(obj, 'val'):
        print(obj.val)
    else:
        print(f"{obj} variable val is undefined")


class ImplA:
    val: int


class ImplB:
    val: Optional[int]


print("Accessing func with ImplA and ImplB before initializing val")
a = ImplA()
b = ImplB()
func(a)
func(b)

print("Accessing func with ImplA and ImplB after initializing val")
a = ImplA()
a.val = 2
b = ImplB()
b.val = 20
func(a)
func(b)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM