Description
In python/cpython#103034, we switched to using inspect.getattr_static
in typing._ProtocolMeta
. This fixed a longstanding bug where properties and __getattr__
methods with side effects would unexpectedly be "called" during isinstance()
checks against runtime-checkable protocols. Here's a demonstration of the bug, which is now fixed on the CPython main
branch:
>>> class Bar:
... @property
... def x(self):
... raise RuntimeError("what were you thinking?")
...
>>> isinstance(Bar(), HasX)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\alexw\AppData\Local\Programs\Python\Python311\Lib\typing.py", line 1968, in __instancecheck__
if all(hasattr(instance, attr) and
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\alexw\AppData\Local\Programs\Python\Python311\Lib\typing.py", line 1968, in <genexpr>
if all(hasattr(instance, attr) and
^^^^^^^^^^^^^^^^^^^^^^^
File "<stdin>", line 4, in x
RuntimeError: what were you thinking?
>>> import time
>>> class Baz:
... @property
... def x(self):
... time.sleep(3600)
... return 42
...
>>> isinstance(Baz(), HasX) # oh no, now we have to wait for an hour
Following recent changes to the implementation of typing_extensions.Protocol
in this repo, the backport would now just be a one-line change:
Diff:
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index c28680c..fc02392 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -524,7 +524,7 @@ else:
if is_protocol_cls:
for attr in cls.__protocol_attrs__:
try:
- val = getattr(instance, attr)
+ val = inspect.getattr_static(instance, attr)
except AttributeError:
break
if val is None and callable(getattr(cls, attr, None)):
However, this leads to a performance degradation for runtime-checkable protocols with non-callable members, and a fairly awful performance degradation for classes with lots of non-callable members. This isinstance()
check would become 3x slower than it is in the latest release of typing_extensions
:
from typing_extensions import Protocol, runtime_checkable
@runtime_checkable
class Foo(Protocol):
a: int
b: int
c: int
d: int
e: int
f: int
class Bar:
def __init__(self):
for attrname in 'abcdef':
setattr(self, attrname, 42)
isinstance(Bar(), Foo)
On the CPython main
branch, we've implemented several optimisations to getattr_static
that have substantially mitigated the performance penalty of using getattr_static
in _ProtocolMeta.__instancecheck__
: see python/cpython#103193 for details. So, one way of avoiding the performance hit could be to vendor inspect.getattr_static
as it exists on the CPython main
branch.
Thoughts?