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:

if arg.isidentifier() and not keyword.iskeyword(arg):
if arg in locals:
return locals[arg]
elif arg in globals:
return globals[arg]
elif hasattr(builtins, arg):
return getattr(builtins, arg)
elif is_forwardref_format:
return self
else:
raise NameError(_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):
if parent_fwdref is not None:
if parent_fwdref.__forward_module__ is not None:
kwargs['module'] = parent_fwdref.__forward_module__
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__
return forward_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

import sys
import timeit
import typing


class MyClass:
    pass


def f(a: "int", b: "str", c: "MyClass", d: "MyClass") -> "MyClass":
    pass


def bench(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

Benchmark Before After Speedup
_make_forward_ref('MyClass').evaluate(...) 9120.3 ns/call 1401.8 ns/call 6.5x (-84.6%)
get_type_hints(f) (identifier-only forward refs) 63221.9 ns/call 22211.0 ns/call 2.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