Title: gh-74690: typing: Call `_get_protocol_attrs` and `_callable_members_only` at protocol class creation time, not during `isinstance()` checks by AlexWaygood · Pull Request #103160 · python/cpython · GitHub
Open Graph Title: gh-74690: typing: Call `_get_protocol_attrs` and `_callable_members_only` at protocol class creation time, not during `isinstance()` checks by AlexWaygood · Pull Request #103160 · python/cpython
X Title: gh-74690: typing: Call `_get_protocol_attrs` and `_callable_members_only` at protocol class creation time, not during `isinstance()` checks by AlexWaygood · Pull Request #103160 · python/cpython
Description: This PR proposes caching the results of _get_protocol_attrs() and _callable_members_only(), so that they only need to be computed once for each protocol class. This hugely speeds up calling isinstance() against runtime-checkable protocols on the "second call", for all kinds of subtypes of a runtime-checkable protocol. There is, however, a small behaviour change: >>> from typing import * >>> @runtime_checkable ... class Bar(Protocol): ... x: int ... >>> class Foo: ... def __init__(self): ... self.x = 42 ... >>> isinstance(Foo(), Bar) True >>> Bar.__annotations__["y"] = int >>> isinstance(Foo(), Bar) # Evaluates to `False` on `main`; `True` with this PR It seems pretty unlikely that anybody would be doing that, though (monkey-patching methods or the __annotations__ dict on a protocol class itself). Do we care about the behaviour change? Is it worth documenting the behaviour change, if we do decide it's okay? Here's benchmark results on my machine for this PR: Time taken for objects with a property: 1.76 Time taken for objects with a classvar: 1.69 Time taken for objects with an instance var: 2.38 Time taken for objects with no var: 7.28 Time taken for nominal subclass instances: 19.92 Time taken for registered subclass instances: 11.60 And here's the same benchmark on main: Time taken for objects with a property: 3.14 Time taken for objects with a classvar: 3.14 Time taken for objects with an instance var: 11.57 Time taken for objects with no var: 15.26 Time taken for nominal subclass instances: 24.60 Time taken for registered subclass instances: 21.32 (The benchmark is pretty skewed towards showing a good result for caching, since it just calls isinstance() 500,000 times against the same runtime-checkable protocol.) Benchmark script import time from typing import Protocol, runtime_checkable @runtime_checkable class HasX(Protocol): x: int class Foo: @property def x(self) -> int: return 42 class Bar: x = 42 class Baz: def __init__(self): self.x = 42 class Egg: ... class Nominal(HasX): def __init__(self): self.x = 42 class Registered: ... HasX.register(Registered) num_instances = 500_000 foos = [Foo() for _ in range(num_instances)] bars = [Bar() for _ in range(num_instances)] bazzes = [Baz() for _ in range(num_instances)] basket = [Egg() for _ in range(num_instances)] nominals = [Nominal() for _ in range(num_instances)] registereds = [Registered() for _ in range(num_instances)] def bench(objs, title): start_time = time.perf_counter() for obj in objs: isinstance(obj, HasX) elapsed = time.perf_counter() - start_time print(f"{title}: {elapsed:.2f}") bench(foos, "Time taken for objects with a property") bench(bars, "Time taken for objects with a classvar") bench(bazzes, "Time taken for objects with an instance var") bench(basket, "Time taken for objects with no var") bench(nominals, "Time taken for nominal subclass instances") bench(registereds, "Time taken for registered subclass instances") Issue: gh-74690
Open Graph Description: This PR proposes caching the results of _get_protocol_attrs() and _callable_members_only(), so that they only need to be computed once for each protocol class. This hugely speeds up calling isinsta...
X Description: This PR proposes caching the results of _get_protocol_attrs() and _callable_members_only(), so that they only need to be computed once for each protocol class. This hugely speeds up calling isinsta...
Opengraph URL: https://github.com/python/cpython/pull/103160
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/checks(.:format) |
| route-controller | pull_requests |
| route-action | checks |
| fetch-nonce | v2:b5506554-b4fe-e395-073d-6047cc0cc5cb |
| current-catalog-service-hash | 87dc3bc62d9b466312751bfd5f889726f4f1337bdff4e8be7da7c93d6c00a25a |
| request-id | C02E:106A75:AFF4FF:F7F444:696A1D36 |
| html-safe-nonce | 4cf00a7df5418d6afe4191ce9c190c2d8b19464dae95c478e0e041dac4b534a4 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDMDJFOjEwNkE3NTpBRkY0RkY6RjdGNDQ0OjY5NkExRDM2IiwidmlzaXRvcl9pZCI6IjE2MTE2ODk2OTIyMjgwOTkzODIiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | bcaa3b10371f5c4c9a5b989cf25aac61507076cb12d924235b7ef8286d9e8704 |
| hovercard-subject-tag | pull_request:1298479719 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,checks,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/python/cpython/pull/103160/checks |
| twitter:image | https://avatars.githubusercontent.com/u/66076021?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/66076021?s=400&v=4 |
| og:image:alt | This PR proposes caching the results of _get_protocol_attrs() and _callable_members_only(), so that they only need to be computed once for each protocol class. This hugely speeds up calling isinsta... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 014f3d193f36b7d393f88ca22d06fbacd370800b40a547c1ea67291e02dc8ea3 |
| turbo-cache-control | no-preview |
| go-import | github.com/python/cpython git https://github.com/python/cpython.git |
| octolytics-dimension-user_id | 1525981 |
| octolytics-dimension-user_login | python |
| octolytics-dimension-repository_id | 81598961 |
| octolytics-dimension-repository_nwo | python/cpython |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 81598961 |
| octolytics-dimension-repository_network_root_nwo | python/cpython |
| turbo-body-classes | logged-out env-production page-responsive full-width full-width-p-0 |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | d515f6f09fa57a93bf90355cb894eb84ca4f458f |
| ui-target | canary-1 |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width