]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
optimize `@util.decorator`
authorInada Naoki <songofacandy@gmail.com>
Mon, 7 Apr 2025 23:55:48 +0000 (19:55 -0400)
committersqla-tester <sqla-tester@sqlalchemy.org>
Mon, 7 Apr 2025 23:55:48 +0000 (19:55 -0400)
### Description

util.decorator uses code generation + eval to create signature matching wrapper.
It consumes some CPU because we can not use pyc cache.

Additionally, each wrapped function has own globals for function annotations.

By stripping function annotations from eval-ed code, compile time and memory usage are saved.

```python
from sqlalchemy.util import decorator
from sqlalchemy import *
import timeit
import tracemalloc
import sqlalchemy.orm._orm_constructors

@decorator
def with_print(fn, *args, **kwargs):
    res = fn(*args, **kwargs)
    print(f"{fn.__name__}(*{args}, **{kwargs}) => {res}")
    return res

# test
PI = 3.14

def f():
    @with_print
    def add(x: int|float, *, y: int|float=PI) -> int|float:
        return x + y
    return add

add = f()
add(1)
print(add.__annotations__)

# benchmark
print(timeit.timeit(f, number=1000)*1000, "us")

# memory
tracemalloc.start(1)
[f() for _ in range(1000)]
mem, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{mem=}, {peak=}")
```

Result:
```
$ .venv/bin/python -VV
Python 3.14.0a6 (main, Mar 17 2025, 21:27:10) [Clang 20.1.0 ]

$ .venv/bin/python sample.py
add(*(1,), **{'y': 3.14}) => 4.140000000000001
{'x': int | float, 'y': int | float, 'return': int | float}
35.93937499681488 us
mem=9252896, peak=9300808

$ git switch -
Switched to branch 'opt-decorator'

$ .venv/bin/python sample.py
add(*(1,), **{'y': 3.14}) => 4.140000000000001
{'x': int | float, 'y': int | float, 'return': int | float}
23.32574996398762 us
mem=1439032, peak=1476423
```

### Checklist
<!-- go over following points. check them with an `x` if they do apply, (they turn into clickable checkboxes once the PR is submitted, so no need to do everything at once)

-->

This pull request is:

- [ ] A documentation / typographical / small typing error fix
- Good to go, no issue or tests are needed
- [x] A short code fix
- please include the issue number, and create an issue if none exists, which
  must include a complete example of the issue.  one line code fixes without an
  issue and demonstration will not be accepted.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.   one line code fixes without tests will not be accepted.
- [ ] A new feature implementation
- please include the issue number, and create an issue if none exists, which must
  include a complete example of how the feature would look.
- Please include: `Fixes: #<issue number>` in the commit message
- please include tests.

Closes: #12502
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12502
Pull-request-sha: 34409cbbfd2dee65bf86a85a87e415c9af47dc62

Change-Id: I88b88eb6eb018608bc2881459f58564881d06641

lib/sqlalchemy/util/langhelpers.py

index f7879d55c07820486f5af4b6172b1c86d74f80aa..6c98504445eb8909c9a05ab8d5a6d6a27f6d5ba4 100644 (file)
@@ -244,10 +244,30 @@ def decorator(target: Callable[..., Any]) -> Callable[[_Fn], _Fn]:
         if not inspect.isfunction(fn) and not inspect.ismethod(fn):
             raise Exception("not a decoratable function")
 
-        spec = compat.inspect_getfullargspec(fn)
-        env: Dict[str, Any] = {}
+        # Python 3.14 defer creating __annotations__ until its used.
+        # We do not want to create __annotations__ now.
+        annofunc = getattr(fn, "__annotate__", None)
+        if annofunc is not None:
+            fn.__annotate__ = None  # type: ignore[union-attr]
+            try:
+                spec = compat.inspect_getfullargspec(fn)
+            finally:
+                fn.__annotate__ = annofunc  # type: ignore[union-attr]
+        else:
+            spec = compat.inspect_getfullargspec(fn)
 
-        spec = _update_argspec_defaults_into_env(spec, env)
+        # Do not generate code for annotations.
+        # update_wrapper() copies the annotation from fn to decorated.
+        # We use dummy defaults for code generation to avoid having
+        # copy of large globals for compiling.
+        # We copy __defaults__ and __kwdefaults__ from fn to decorated.
+        empty_defaults = (None,) * len(spec.defaults or ())
+        empty_kwdefaults = dict.fromkeys(spec.kwonlydefaults or ())
+        spec = spec._replace(
+            annotations={},
+            defaults=empty_defaults,
+            kwonlydefaults=empty_kwdefaults,
+        )
 
         names = (
             tuple(cast("Tuple[str, ...]", spec[0]))
@@ -292,43 +312,23 @@ def decorator(target: Callable[..., Any]) -> Callable[[_Fn], _Fn]:
                 % metadata
             )
 
-        mod = sys.modules[fn.__module__]
-        env.update(vars(mod))
-        env.update({targ_name: target, fn_name: fn, "__name__": fn.__module__})
+        env: Dict[str, Any] = {
+            targ_name: target,
+            fn_name: fn,
+            "__name__": fn.__module__,
+        }
 
         decorated = cast(
             types.FunctionType,
             _exec_code_in_env(code, env, fn.__name__),
         )
-        decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__
-
-        decorated.__wrapped__ = fn  # type: ignore[attr-defined]
+        decorated.__defaults__ = fn.__defaults__
+        decorated.__kwdefaults__ = fn.__kwdefaults__  # type: ignore
         return update_wrapper(decorated, fn)  # type: ignore[return-value]
 
     return update_wrapper(decorate, target)  # type: ignore[return-value]
 
 
-def _update_argspec_defaults_into_env(spec, env):
-    """given a FullArgSpec, convert defaults to be symbol names in an env."""
-
-    if spec.defaults:
-        new_defaults = []
-        i = 0
-        for arg in spec.defaults:
-            if type(arg).__module__ not in ("builtins", "__builtin__"):
-                name = "x%d" % i
-                env[name] = arg
-                new_defaults.append(name)
-                i += 1
-            else:
-                new_defaults.append(arg)
-        elem = list(spec)
-        elem[3] = tuple(new_defaults)
-        return compat.FullArgSpec(*elem)
-    else:
-        return spec
-
-
 def _exec_code_in_env(
     code: Union[str, types.CodeType], env: Dict[str, Any], fn_name: str
 ) -> Callable[..., Any]: