Reputation: 924
I thought python Protocol
was only useful for type-hints, without any impact on the runtime behavior (except when using @runtime_checkable
or default method implementation).
But see this example:
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) # prints "some_id"
my_port_b = MyPortB()
my_port_b.port_id = "some_id" # raises "AttributeError: can't set attribute"
print(my_port_b.port_id)
Where line my_port_b.port_id = "some_id"
raises AttributeError: can't set attribute
.
The only difference between MyPortA
and MyPortB
is the inheritance of the Protocol
.
Is it a bug in Python or the intended behavior?
Yes, I know this line is a violation of the getter-only attribute defined in the Protocol, but this is a type-hint problem for tools like mypy, not something for the runtime.
(Or maybe it's not even a violation of the type-hint, because a read-and-write attribute is a subtype of a read-only attribute).
I expected to see no difference between classes that inherit a Protocol
and classes that do not.
Python version: 3.9.7
Upvotes: 2
Views: 127
Reputation: 924
I think it's related to the following behavior of @property
inheritance:
class Port:
@property
def port_id(self) -> str:
"""a read-only port id"""
class MyPortB(Port):
port_id: str
my_port_b = MyPortB()
my_port_b.port_id = "some_id" # raises "AttributeError: can't set attribute"
from abc import ABC, abstractmethod
class PortBase(ABC):
@property
@abstractmethod
def port_id(self) -> str:
"""a read-only port id"""
class MyPortB(PortBase):
port_id: str
my_port_b = MyPortB() # raises "TypeError: Can't instantiate abstract class MyPortB with abstract method port_id"
class MyPortC(PortBase):
def __init__(self) -> None:
self.port_id: str = "test"
my_port_c = MyPortC() # raises "TypeError: Can't instantiate abstract class MyPortC with abstract method port_id"
As you see, the abstract method port_id
is considered not implemented in the child, ignoring lines port_id: str
and self.port_id: str = "test"
.
In the following example we see that the property
has priority over the class variable:
class MyPortD:
port_id: str
private_var: str = "test"
@property
def port_id(self) -> str:
"""a read-only port id"""
return self.private_var
my_port_d = MyPortD()
my_port_d.port_id = "some_id" # raises "AttributeError: can't set attribute"
So how to implement a read-and-write attribute that will be considered a subtype of a read-only property? The answer is a setter property
in the child! see this:
class MyPortG(PortBase):
def __init__(self) -> None:
self._port_id: str = "test"
@property
def port_id(self) -> str:
return self._port_id
@port_id.setter
def port_id(self, value: str) -> None:
self._port_id = value
my_port_g = MyPortG()
my_port_g.port_id = "some_id"
print(my_port_g.port_id) # prints "some_id"
The same principle works for Protocol as well:
class MyPortGUsingProto(PortProto):
def __init__(self) -> None:
self._port_id: str = "test"
@property
def port_id(self) -> str:
return self._port_id
@port_id.setter
def port_id(self, value: str) -> None:
self._port_id = value
my_port_g2 = MyPortGUsingProto()
my_port_g2.port_id = "some_id"
print(my_port_g2.port_id) # prints "some_id"
This solution works, but I still don’t know why a variable is not considered a subtype for a getter-only property when a combination of getter and setter properties does.
Upvotes: 1