PEP 362 – Funktionssignatur-Objekt
- Autor:
- Brett Cannon <brett at python.org>, Jiwon Seo <seojiwon at gmail.com>, Yury Selivanov <yury at edgedb.com>, Larry Hastings <larry at hastings.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 21-Aug-2006
- Python-Version:
- 3.3
- Post-History:
- 04-Jun-2012
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Python hat schon immer leistungsstarke Introspektionsfähigkeiten unterstützt, einschließlich der Introspektion von Funktionen und Methoden (im Folgenden bezieht sich „Funktion“ auf sowohl Funktionen als auch Methoden). Durch die Untersuchung eines Funktions-Objekts kann man die Signatur der Funktion vollständig rekonstruieren. Leider sind diese Informationen auf unbequeme Weise gespeichert und über ein halbes Dutzend tief verschachtelter Attribute verteilt.
Dieser PEP schlägt eine neue Darstellung für Funktionssignaturen vor. Die neue Darstellung enthält alle notwendigen Informationen über eine Funktion und ihre Parameter und macht die Introspektion einfach und geradlinig.
Dieses Objekt ersetzt jedoch nicht die bestehenden Funktionsmetadaten, die von Python selbst zur Ausführung dieser Funktionen verwendet werden. Das neue Metadaten-Objekt ist ausschließlich dazu bestimmt, die Funktionsintrospektion für Python-Programmierer zu erleichtern.
Signatur-Objekt
Ein Signature-Objekt repräsentiert die Aufrufsignatur einer Funktion und ihre Rückgabeannotation. Für jeden von der Funktion akzeptierten Parameter speichert es ein Parameter-Objekt in seiner parameters Sammlung.
Ein Signature-Objekt hat die folgenden öffentlichen Attribute und Methoden
- return_annotation : object
- Die „return“-Annotation für die Funktion. Wenn die Funktion keine „return“-Annotation hat, ist dieses Attribut auf
Signature.emptygesetzt.
- parameters : OrderedDict
- Eine geordnete Abbildung von Parameternamen auf die entsprechenden Parameter-Objekte.
- bind(*args, **kwargs) -> BoundArguments
- Erstellt eine Abbildung von positionsbezogenen und Schlüsselwortargumenten auf Parameter. Löst einen
TypeErroraus, wenn die übergebenen Argumente nicht mit der Signatur übereinstimmen.
- bind_partial(*args, **kwargs) -> BoundArguments
- Funktioniert genauso wie
bind(), erlaubt aber das Weglassen einiger erforderlicher Argumente (ahmt das Verhalten vonfunctools.partialnach). Löst einenTypeErroraus, wenn die übergebenen Argumente nicht mit der Signatur übereinstimmen.
- replace(parameters=<optional>, *, return_annotation=<optional>) -> Signature
- Erstellt eine neue Signature-Instanz basierend auf der Instanz, auf der
replaceaufgerufen wurde. Es ist möglich, unterschiedlicheparametersund/oderreturn_annotationzu übergeben, um die entsprechenden Eigenschaften der Basis-Signatur zu überschreiben. Um diereturn_annotationaus der kopiertenSignaturezu entfernen, übergeben SieSignature.empty.Beachten Sie, dass die Notation „=<optional>“ bedeutet, dass das Argument optional ist. Diese Notation gilt für den Rest dieses PEP.
Signature-Objekte sind unveränderlich. Verwenden Sie Signature.replace(), um eine modifizierte Kopie zu erstellen.
>>> def foo() -> None:
... pass
>>> sig = signature(foo)
>>> new_sig = sig.replace(return_annotation="new return annotation")
>>> new_sig is not sig
True
>>> new_sig.return_annotation != sig.return_annotation
True
>>> new_sig.parameters == sig.parameters
True
>>> new_sig = new_sig.replace(return_annotation=new_sig.empty)
>>> new_sig.return_annotation is Signature.empty
True
Es gibt zwei Möglichkeiten, eine Signature-Klasse zu instanziieren
- Signature(parameters=<optional>, *, return_annotation=Signature.empty)
- Standard-Konstruktor für Signature. Akzeptiert eine optionale Sequenz von
Parameter-Objekten und eine optionalereturn_annotation. Die Parametersequenz wird validiert, um zu prüfen, ob keine Parameter mit doppelten Namen vorhanden sind und ob die Parameter in der richtigen Reihenfolge sind, d. h. zuerst positionsbezogene, dann positions- oder schlüsselwortbezogene usw.
- Signature.from_function(function)
- Gibt ein Signature-Objekt zurück, das die Signatur der übergebenen Funktion widerspiegelt.
Signaturen können auf Gleichheit getestet werden. Zwei Signaturen sind gleich, wenn ihre Parameter gleich sind, ihre positionsbezogenen und positions- oder schlüsselwortbezogenen Parameter in der gleichen Reihenfolge erscheinen und sie gleiche Rückgabeannotationen haben.
Änderungen am Signature-Objekt oder an seinen Datenmitgliedern wirken sich nicht auf die Funktion selbst aus.
Signature implementiert auch __str__
>>> str(Signature.from_function((lambda *args: None)))
'(*args)'
>>> str(Signature())
'()'
Parameter-Objekt
Die ausdrucksstarke Syntax von Python bedeutet, dass Funktionen viele verschiedene Arten von Parametern mit vielen subtilen semantischen Unterschieden akzeptieren können. Wir schlagen ein reichhaltiges Parameter-Objekt vor, das entwickelt wurde, um jeden möglichen Funktionsparameter darzustellen.
Ein Parameter-Objekt hat die folgenden öffentlichen Attribute und Methoden
- name : str
- Der Name des Parameters als Zeichenkette. Muss ein gültiger Python-Bezeichnername sein (mit Ausnahme von
POSITIONAL_ONLYParametern, bei denen er aufNonegesetzt werden kann).
- default : object
- Der Standardwert für den Parameter. Wenn der Parameter keinen Standardwert hat, ist dieses Attribut auf
Parameter.emptygesetzt.
- annotation : object
- Die Annotation für den Parameter. Wenn der Parameter keine Annotation hat, ist dieses Attribut auf
Parameter.emptygesetzt.
- kind
- Beschreibt, wie Argumentwerte an den Parameter gebunden werden. Mögliche Werte
Parameter.POSITIONAL_ONLY- Wert muss als positionsbezogenes Argument übergeben werden.Python hat keine explizite Syntax zur Definition von positionsbezogenen Parametern, aber viele integrierte und Erweiterungsmodul-Funktionen (insbesondere solche, die nur ein oder zwei Parameter akzeptieren) akzeptieren sie.
Parameter.POSITIONAL_OR_KEYWORD- Wert kann entweder als Schlüsselwort- oder als positionsbezogenes Argument übergeben werden (dies ist das Standardbindungsverhalten für in Python implementierte Funktionen).Parameter.KEYWORD_ONLY- Wert muss als Schlüsselwortargument übergeben werden. Nur-Schlüsselwort-Parameter sind solche, die in einer Python-Funktionsdefinition nach einem „*“- oder „*args“-Eintrag erscheinen.Parameter.VAR_POSITIONAL- ein Tupel von positionsbezogenen Argumenten, die keinem anderen Parameter zugeordnet sind. Dies entspricht einem „*args“-Parameter in einer Python-Funktionsdefinition.Parameter.VAR_KEYWORD- ein Dictionary von Schlüsselwortargumenten, die keinem anderen Parameter zugeordnet sind. Dies entspricht einem „**kwargs“-Parameter in einer Python-Funktionsdefinition.
Verwenden Sie immer
Parameter.*Konstanten zum Setzen und Prüfen des Werts deskindAttributs.
- replace(*, name=<optional>, kind=<optional>, default=<optional>, annotation=<optional>) -> Parameter
- Erstellt eine neue Parameter-Instanz basierend auf der Instanz, auf der
replacedaufgerufen wurde. Um ein Parameter-Attribut zu überschreiben, übergeben Sie das entsprechende Argument. Um ein Attribut aus einemParameterzu entfernen, übergeben SieParameter.empty.
Parameter-Konstruktor
- Parameter(name, kind, *, annotation=Parameter.empty, default=Parameter.empty)
- Instanziiert ein Parameter-Objekt.
nameundkindsind erforderlich, währendannotationunddefaultoptional sind.
Zwei Parameter sind gleich, wenn sie gleiche Namen, Arten, Standardwerte und Annotationen haben.
Parameter-Objekte sind unveränderlich. Anstatt ein Parameter-Objekt zu ändern, können Sie Parameter.replace() verwenden, um eine modifizierte Kopie zu erstellen, wie folgt:
>>> param = Parameter('foo', Parameter.KEYWORD_ONLY, default=42)
>>> str(param)
'foo=42'
>>> str(param.replace())
'foo=42'
>>> str(param.replace(default=Parameter.empty, annotation='spam'))
"foo:'spam'"
BoundArguments Objekt
Ergebnis eines Signature.bind Aufrufs. Enthält die Abbildung von Argumenten auf die Parameter der Funktion.
Hat die folgenden öffentlichen Attribute
- arguments : OrderedDict
- Eine geordnete, veränderliche Abbildung von Parameternamen auf Argumentwerte. Enthält nur explizit gebundene Argumente. Argumente, für die
bind()auf einen Standardwert zurückgegriffen hat, werden übersprungen.
- args : tuple
- Tupel von positionsbezogenen Argumentwerten. Dynamisch aus dem ‚arguments‘-Attribut berechnet.
- kwargs : dict
- Dictionary von Schlüsselwortargumentwerten. Dynamisch aus dem ‚arguments‘-Attribut berechnet.
Das arguments Attribut sollte in Verbindung mit Signature.parameters für jegliche Argumentverarbeitungszwecke verwendet werden.
args und kwargs Eigenschaften können verwendet werden, um Funktionen aufzurufen.
def test(a, *, b):
...
sig = signature(test)
ba = sig.bind(10, b=20)
test(*ba.args, **ba.kwargs)
Argumente, die als Teil von *args oder **kwargs übergeben werden könnten, werden nur im Attribut BoundArguments.args enthalten sein. Betrachten Sie das folgende Beispiel:
def test(a=1, b=2, c=3):
pass
sig = signature(test)
ba = sig.bind(a=10, c=13)
>>> ba.args
(10,)
>>> ba.kwargs:
{'c': 13}
Implementierung
Die Implementierung fügt dem Modul inspect eine neue Funktion signature() hinzu. Die Funktion ist die bevorzugte Methode, um eine Signature für ein aufrufbares Objekt zu erhalten.
Die Funktion implementiert den folgenden Algorithmus
- Wenn das Objekt nicht aufrufbar ist – löse einen TypeError aus
- Wenn das Objekt ein
__signature__Attribut hat und es nichtNoneist – gib es zurück - Wenn es ein
__wrapped__Attribut hat, gibsignature(object.__wrapped__)zurück - Wenn das Objekt eine Instanz von
FunctionTypeist, konstruiere und gib eine neueSignaturedafür zurück - Wenn das Objekt eine gebundene Methode ist, konstruiere und gib ein neues
SignatureObjekt zurück, wobei sein erster Parameter (normalerweiseselfodercls) entfernt wird. (classmethodundstaticmethodwerden ebenfalls unterstützt. Da beides Deskriptoren sind, gibt erstere eine gebundene Methode und letztere ihre verpackte Funktion zurück.) - Wenn das Objekt eine Instanz von
functools.partialist, konstruiere eine neueSignatureaus seinempartial.funcAttribut und berücksichtige bereits gebundenepartial.argsundpartial.kwargs - Wenn das Objekt eine Klasse oder Metaklasse ist
- Wenn die Klasse des Objekts eine in ihrer MRO definierte
__call__Methode hat, gib eine Signatur dafür zurück - Wenn das Objekt eine in seiner MRO definierte
__new__Methode hat, gib ein Signature-Objekt dafür zurück - Wenn das Objekt eine in seiner MRO definierte
__init__Methode hat, gib ein Signature-Objekt dafür zurück
- Wenn die Klasse des Objekts eine in ihrer MRO definierte
- Gib
signature(object.__call__)zurück
Beachten Sie, dass das Signature Objekt aufgerufen wird, wenn es benötigt wird und ist nicht automatisch gecached. Der Benutzer kann jedoch eine Signatur manuell cachen, indem er sie im __signature__ Attribut speichert.
Eine Implementierung für Python 3.3 finden Sie unter [1]. Das Python-Issue, das den Patch verfolgt, ist [2].
Designüberlegungen
Kein implizites Caching von Signature-Objekten
Das erste PEP-Design hatte eine Bestimmung für implizites Caching von Signature Objekten in der Funktion inspect.signature(). Dies hat jedoch folgende Nachteile
- Wenn das
SignatureObjekt gecached wird, werden alle Änderungen an der Funktion, die es beschreibt, nicht darin reflektiert. Wenn Caching benötigt wird, kann es immer manuell und explizit erfolgen. - Es ist besser, das
__signature__Attribut für Fälle zu reservieren, in denen die Notwendigkeit besteht, explizit einSignatureObjekt festzulegen, das vom tatsächlichen abweicht.
Einige Funktionen sind möglicherweise nicht introspektionsfähig
Einige Funktionen sind in bestimmten Implementierungen von Python möglicherweise nicht introspektionsfähig. In CPython beispielsweise liefern eingebaute Funktionen, die in C definiert sind, keine Metadaten über ihre Argumente. Die Unterstützung dafür liegt außerhalb des Umfangs dieses PEP.
Signatur- und Parameter-Äquivalenz
Wir gehen davon aus, dass Parameternamen eine semantische Bedeutung haben – zwei Signaturen sind nur dann gleich, wenn ihre entsprechenden Parameter gleich sind und exakt die gleichen Namen haben. Benutzer, die lockerere Äquivalenztests wünschen, die möglicherweise Namen von VAR_KEYWORD- oder VAR_POSITIONAL-Parametern ignorieren, müssen diese selbst implementieren.
Beispiele
Visualisierung der Signatur von aufrufbaren Objekten
Definieren wir einige Klassen und Funktionen
from inspect import signature
from functools import partial, wraps
class FooMeta(type):
def __new__(mcls, name, bases, dct, *, bar:bool=False):
return super().__new__(mcls, name, bases, dct)
def __init__(cls, name, bases, dct, **kwargs):
return super().__init__(name, bases, dct)
class Foo(metaclass=FooMeta):
def __init__(self, spam:int=42):
self.spam = spam
def __call__(self, a, b, *, c) -> tuple:
return a, b, c
@classmethod
def spam(cls, a):
return a
def shared_vars(*shared_args):
"""Decorator factory that defines shared variables that are
passed to every invocation of the function"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
full_args = shared_args + args
return f(*full_args, **kwargs)
# Override signature
sig = signature(f)
sig = sig.replace(tuple(sig.parameters.values())[1:])
wrapper.__signature__ = sig
return wrapper
return decorator
@shared_vars({})
def example(_state, a, b, c):
return _state, a, b, c
def format_signature(obj):
return str(signature(obj))
Nun, in der Python REPL
>>> format_signature(FooMeta)
'(name, bases, dct, *, bar:bool=False)'
>>> format_signature(Foo)
'(spam:int=42)'
>>> format_signature(Foo.__call__)
'(self, a, b, *, c) -> tuple'
>>> format_signature(Foo().__call__)
'(a, b, *, c) -> tuple'
>>> format_signature(Foo.spam)
'(a)'
>>> format_signature(partial(Foo().__call__, 1, c=3))
'(b, *, c=3) -> tuple'
>>> format_signature(partial(partial(Foo().__call__, 1, c=3), 2, c=20))
'(*, c=20) -> tuple'
>>> format_signature(example)
'(a, b, c)'
>>> format_signature(partial(example, 1, 2))
'(c)'
>>> format_signature(partial(partial(example, 1, b=2), c=3))
'(b=2, c=3)'
Annotationsprüfer
import inspect
import functools
def checktypes(func):
'''Decorator to verify arguments and return types
Example:
>>> @checktypes
... def test(a:int, b:str) -> int:
... return int(a * b)
>>> test(10, '1')
1111111111
>>> test(10, 1)
Traceback (most recent call last):
...
ValueError: foo: wrong type of 'b' argument, 'str' expected, got 'int'
'''
sig = inspect.signature(func)
types = {}
for param in sig.parameters.values():
# Iterate through function's parameters and build the list of
# arguments types
type_ = param.annotation
if type_ is param.empty or not inspect.isclass(type_):
# Missing annotation or not a type, skip it
continue
types[param.name] = type_
# If the argument has a type specified, let's check that its
# default value (if present) conforms with the type.
if param.default is not param.empty and not isinstance(param.default, type_):
raise ValueError("{func}: wrong type of a default value for {arg!r}". \
format(func=func.__qualname__, arg=param.name))
def check_type(sig, arg_name, arg_type, arg_value):
# Internal function that encapsulates arguments type checking
if not isinstance(arg_value, arg_type):
raise ValueError("{func}: wrong type of {arg!r} argument, " \
"{exp!r} expected, got {got!r}". \
format(func=func.__qualname__, arg=arg_name,
exp=arg_type.__name__, got=type(arg_value).__name__))
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Let's bind the arguments
ba = sig.bind(*args, **kwargs)
for arg_name, arg in ba.arguments.items():
# And iterate through the bound arguments
try:
type_ = types[arg_name]
except KeyError:
continue
else:
# OK, we have a type for the argument, lets get the corresponding
# parameter description from the signature object
param = sig.parameters[arg_name]
if param.kind == param.VAR_POSITIONAL:
# If this parameter is a variable-argument parameter,
# then we need to check each of its values
for value in arg:
check_type(sig, arg_name, type_, value)
elif param.kind == param.VAR_KEYWORD:
# If this parameter is a variable-keyword-argument parameter:
for subname, value in arg.items():
check_type(sig, arg_name + ':' + subname, type_, value)
else:
# And, finally, if this parameter a regular one:
check_type(sig, arg_name, type_, arg)
result = func(*ba.args, **ba.kwargs)
# The last bit - let's check that the result is correct
return_type = sig.return_annotation
if (return_type is not sig._empty and
isinstance(return_type, type) and
not isinstance(result, return_type)):
raise ValueError('{func}: wrong return type, {exp} expected, got {got}'. \
format(func=func.__qualname__, exp=return_type.__name__,
got=type(result).__name__))
return result
return wrapper
Akzeptanz
PEP 362 wurde von Guido angenommen, Freitag, 22. Juni 2012 [3]. Die Referenzimplementierung wurde später am selben Tag in den Trunk übernommen.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0362.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT