Skip to content

Avoid needlessly accessing ForwardRef.__forward_code__ in evaluate_forward_ref() #155278

Description

@Viicos

Feature or enhancement

Proposal:

Following #124337, I think we could also avoid evaluating __forward_code__ in _make_forward_ref() (called by evaluate_forward_ref()).

In ForwardRef.evaluate(), __forward_code__ is accessed only if necessary:

ifarg.isidentifier() andnotkeyword.iskeyword(arg):
ifarginlocals:
returnlocals[arg]
elifarginglobals:
returnglobals[arg]
elifhasattr(builtins, arg):
returngetattr(builtins, arg)
elifis_forwardref_format:
returnself
else:
raiseNameError(_NAME_ERROR_MSG.format(name=arg), name=arg)
else:
code=self.__forward_code__

but in _make_forward_ref():

cpython/Lib/typing.py

Lines 994 to 1003 in fe3a123

def_make_forward_ref(code, *, parent_fwdref=None, **kwargs):
ifparent_fwdrefisnotNone:
ifparent_fwdref.__forward_module__isnotNone:
kwargs['module'] =parent_fwdref.__forward_module__
ifparent_fwdref.__owner__isnotNone:
kwargs['owner'] =parent_fwdref.__owner__
forward_ref=annotationlib.ForwardRef(code, **kwargs)
# For compatibility, eagerly compile the forwardref's code.
forward_ref.__forward_code__
returnforward_ref

We can apply the following:

diff --git a/Lib/typing.py b/Lib/typing.py
index 809c0ff8860..29b858ff836 100644
--- a/Lib/typing.py+++ b/Lib/typing.py@@ -25,6 +25,7 @@
import collections.abc
import copyreg
import functools
+import keyword
import operator
import sys
import types
@@ -998,8 +999,13 @@ def _make_forward_ref(code, *, parent_fwdref=None, **kwargs):
if parent_fwdref.__owner__ is not None:
kwargs['owner'] = parent_fwdref.__owner__
forward_ref = annotationlib.ForwardRef(code, **kwargs)
- # For compatibility, eagerly compile the forwardref's code.- forward_ref.__forward_code__+ # For compatibility, eagerly compile the forwardref's code so that any+ # SyntaxError is raised immediately rather than when the forward+ # reference is evaluated. Similar to 'ForwardRef.evaluate(), we only compile+ # it if necessary:+ if not (code.isidentifier() and not keyword.iskeyword(code)):+ forward_ref.__forward_code__
return forward_ref

Script

importsysimporttimeitimporttypingclassMyClass:
passdeff(a: "int", b: "str", c: "MyClass", d: "MyClass") ->"MyClass":
passdefbench(label, n, stmt):
t=min(timeit.repeat(stmt, number=n, repeat=5, globals=globals()))
print(f"{label:55s}{t/n*1e9:9.1f} ns/call ({n} iters, best of 5)")
if__name__=="__main__":
bench(
"typing._make_forward_ref('MyClass').evaluate(...)",
200_000,
"typing._make_forward_ref('MyClass').evaluate(globals={'MyClass': MyClass})",
)
bench(
"get_type_hints(f) # only identifier forward refs",
20_000,
"typing.get_type_hints(f)",
)

Results

BenchmarkBeforeAfterSpeedup
_make_forward_ref('MyClass').evaluate(...)9120.3 ns/call1401.8 ns/call6.5x (-84.6%)
get_type_hints(f) (identifier-only forward refs)63221.9 ns/call22211.0 ns/call2.85x (-64.9%)
before (main):
typing._make_forward_ref('MyClass').evaluate(...) 9120.3 ns/call (200000 iters, best of 5)
get_type_hints(f) # only identifier forward refs 63221.9 ns/call (20000 iters, best of 5)
after (patch):
typing._make_forward_ref('MyClass').evaluate(...) 1401.8 ns/call (200000 iters, best of 5)
get_type_hints(f) # only identifier forward refs 22211.0 ns/call (20000 iters, best of 5)

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions