Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

Inhaltsverzeichnis

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.empty gesetzt.
  • 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 TypeError aus, 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 von functools.partial nach). Löst einen TypeError aus, 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 replace aufgerufen wurde. Es ist möglich, unterschiedliche parameters und/oder return_annotation zu übergeben, um die entsprechenden Eigenschaften der Basis-Signatur zu überschreiben. Um die return_annotation aus der kopierten Signature zu entfernen, übergeben Sie Signature.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 optionale return_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_ONLY Parametern, bei denen er auf None gesetzt werden kann).
  • default : object
    Der Standardwert für den Parameter. Wenn der Parameter keinen Standardwert hat, ist dieses Attribut auf Parameter.empty gesetzt.
  • annotation : object
    Die Annotation für den Parameter. Wenn der Parameter keine Annotation hat, ist dieses Attribut auf Parameter.empty gesetzt.
  • 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 des kind Attributs.

  • replace(*, name=<optional>, kind=<optional>, default=<optional>, annotation=<optional>) -> Parameter
    Erstellt eine neue Parameter-Instanz basierend auf der Instanz, auf der replaced aufgerufen wurde. Um ein Parameter-Attribut zu überschreiben, übergeben Sie das entsprechende Argument. Um ein Attribut aus einem Parameter zu entfernen, übergeben Sie Parameter.empty.

Parameter-Konstruktor

  • Parameter(name, kind, *, annotation=Parameter.empty, default=Parameter.empty)
    Instanziiert ein Parameter-Objekt. name und kind sind erforderlich, während annotation und default optional 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 nicht None ist – gib es zurück
  • Wenn es ein __wrapped__ Attribut hat, gib signature(object.__wrapped__) zurück
  • Wenn das Objekt eine Instanz von FunctionType ist, konstruiere und gib eine neue Signature dafür zurück
  • Wenn das Objekt eine gebundene Methode ist, konstruiere und gib ein neues Signature Objekt zurück, wobei sein erster Parameter (normalerweise self oder cls) entfernt wird. ( classmethod und staticmethod werden 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.partial ist, konstruiere eine neue Signature aus seinem partial.func Attribut und berücksichtige bereits gebundene partial.args und partial.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
  • 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 Signature Objekt 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 ein Signature Objekt 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


Quelle: https://github.com/python/peps/blob/main/peps/pep-0362.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT