Python中带@property的Protocol是否应改变运行时行为?
关于Python Protocol继承后影响运行时属性赋值的疑问
我原本以为Python的Protocol仅用于类型提示,不会影响运行时行为(除非使用@runtime_checkable或默认方法实现),但如下示例显示了意外的运行时错误:
from typing import Protocol class PortProto(Protocol): @property def port_id(self) -> str: """a read-only port id""" class MyPortA: port_id: str class MyPortB(PortProto): port_id: str my_port_a = MyPortA() my_port_a.port_id = "some_id" print(my_port_a.port_id) # 输出 "some_id" my_port_b = MyPortB() my_port_b.port_id = "some_id" # 抛出 "AttributeError: can't set attribute" print(my_port_b.port_id)
其中my_port_b.port_id = "some_id"会抛出AttributeError,MyPortA与MyPortB的唯一区别是后者继承了Protocol。这是Python的Bug还是预期行为?
我知道这看似违反了Protocol中定义的只读属性,但我认为这应该是mypy等工具的类型提示问题,而非运行时问题(甚至从类型系统角度,读写属性是只读属性的子类型,不算类型提示违规)。我原本预期继承Protocol和不继承Protocol的类在运行时不会有差异。
Python版本:3.9.7
解答
这是预期行为,并非Python的Bug,原因如下:
- 当类显式继承Protocol时,Python会将Protocol中定义的
@property视为普通的类属性进行继承,而非仅作为类型提示存在。PortProto里的port_id是只读的@property,子类MyPortB仅通过port_id: str声明了类型注解,这并不会覆盖父类的@property定义,因此实例的port_id实际继承了父类的只读属性,赋值时自然触发AttributeError。 - 而
MyPortA没有继承Protocol,port_id: str只是纯类型提示,运行时会被当作普通的实例属性处理,因此可以正常赋值。
补充说明
从类型系统角度,读写属性确实是只读属性的子类型,mypy这类静态检查工具不会对MyPortB的写法报错,但运行时的类继承机制和静态类型提示是相互独立的两个层面。
解决方法
如果想让继承Protocol的子类也能正常赋值port_id,可以在子类中显式定义@property的setter:
class MyPortB(PortProto): _port_id: str @property def port_id(self) -> str: return self._port_id @port_id.setter def port_id(self, value: str) -> None: self._port_id = value
或者如果仅需满足类型提示、不需要严格遵循Protocol的@property结构,也可以重新将其定义为普通属性(不推荐直接修改父类属性,显式定义更稳妥)。
内容的提问来源于stack exchange,提问作者Noam-N
相关产品推荐
相关产品推荐

