You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:51:05