Skip to content

Commit 8ba1f31

Browse files
eendebakptclaude
andauthored
gh-150816: Speed up inspect.signature() for Python functions (#157463)
* Speed up inspect.signature() for Python functions For real Python functions the parameter kinds and defaults come straight from the code object, so the validation done by Parameter.__init__ is redundant. Construct the Parameter objects directly, falling back to the regular constructor for names that are not plain identifiers (e.g. the ".0" implicit argument of comprehensions). Parameter subclasses and duck-typed functions keep the full validation. inspect.signature() on a function with 7 parameters: 23.4 us -> 14.5 us. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the fast Parameter constructor a classmethod and add tests Move the helper to Parameter._from_code(). Add tests that a Signature subclass overriding _parameter_cls still gets its own Parameter class (the concern raised on gh-150823), and that parameter names of function-like objects are still validated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 20d5e67 commit 8ba1f31

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎Lib/inspect.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2348,6 +2348,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
23482348
return _signature_fromstr(cls, func, s, skip_bound_arg)
23492349

23502350
Parameter = cls._parameter_cls
2351+
if Parameter is Signature._parameter_cls and not is_duck_function:
2352+
Parameter = Parameter._from_code
23512353

23522354
# Parameter information.
23532355
func_code = func.__code__
@@ -2746,6 +2748,18 @@ def __init__(self, name, kind, *, default=_empty, annotation=_empty):
27462748

27472749
self._name = name
27482750

2751+
@classmethod
2752+
def _from_code(cls, name, kind, *, default=_empty, annotation=_empty):
2753+
# Fast path for Python functions: only the name needs validation.
2754+
if iskeyword(name) or not name.isidentifier():
2755+
return cls(name, kind, default=default, annotation=annotation)
2756+
self = object.__new__(cls)
2757+
self._name = name
2758+
self._kind = kind
2759+
self._default = default
2760+
self._annotation = annotation
2761+
return self
2762+
27492763
def __reduce__(self):
27502764
return (type(self),
27512765
(self._name, self._kind),

‎Lib/test/test_inspect/test_inspect.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3556,6 +3556,48 @@ def __init__(self, marker):
35563556

35573557
self.assertEqual(str(inspect.signature(funclike)), '(marker)')
35583558

3559+
@cpython_only
3560+
def test_signature_functionlike_invalid_names(self):
3561+
# The code object of a function-like object is not guaranteed
3562+
# to have valid parameter names, so they must be validated.
3563+
def func(a, b):
3564+
pass
3565+
3566+
class funclike:
3567+
__name__ = func.__name__
3568+
__code__ = func.__code__.replace(co_varnames=('a', '$b'))
3569+
__annotations__ = {}
3570+
__defaults__ = None
3571+
__kwdefaults__ = None
3572+
3573+
def __call__(self, *args):
3574+
pass
3575+
3576+
with self.assertRaisesRegex(ValueError,
3577+
'is not a valid parameter name'):
3578+
inspect.signature(funclike())
3579+
3580+
def test_signature_parameter_cls_subclass(self):
3581+
# A Signature subclass can override _parameter_cls with a
3582+
# Parameter subclass that has its own constructor.
3583+
class MyParameter(inspect.Parameter):
3584+
def __init__(self, *args, **kwargs):
3585+
super().__init__(*args, **kwargs)
3586+
self.extra = 'spam'
3587+
3588+
class MySignature(inspect.Signature):
3589+
_parameter_cls = MyParameter
3590+
3591+
def f(a, /, b=1, *args, c, d=2, **kwargs):
3592+
pass
3593+
3594+
sig = MySignature.from_callable(f)
3595+
self.assertEqual(len(sig.parameters), 6)
3596+
for param in sig.parameters.values():
3597+
self.assertIs(type(param), MyParameter)
3598+
self.assertEqual(param.extra, 'spam')
3599+
self.assertEqual(sig, inspect.signature(f))
3600+
35593601
def test_signature_on_method(self):
35603602
class Test:
35613603
def __init__(*args):
@@ -5656,6 +5698,20 @@ def test_signature_parameter_implicit(self):
56565698
self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_ONLY)
56575699
self.assertEqual(param.name, 'implicit0')
56585700

5701+
@cpython_only
5702+
def test_signature_from_code_unusual_names(self):
5703+
def f(a, b): pass
5704+
f.__code__ = f.__code__.replace(co_varnames=('.0', 'b'))
5705+
sig = inspect.signature(f)
5706+
self.assertEqual(list(sig.parameters), ['implicit0', 'b'])
5707+
self.assertEqual(sig.parameters['implicit0'].kind,
5708+
inspect.Parameter.POSITIONAL_ONLY)
5709+
5710+
f.__code__ = f.__code__.replace(co_varnames=('if', 'b'))
5711+
with self.assertRaisesRegex(ValueError,
5712+
'is not a valid parameter name'):
5713+
inspect.signature(f)
5714+
56595715
def test_signature_parameter_immutability(self):
56605716
p = inspect.Parameter('spam', kind=inspect.Parameter.KEYWORD_ONLY)
56615717

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up :func:`inspect.signature` for Python functions by skipping
2+
redundant validation when creating :class:`inspect.Parameter` objects.

0 commit comments

Comments
 (0)