Beste Möglichkeit, Funktionsargumente zu überprüfen? [geschlossen]


70

Ich suche nach einer effizienten Möglichkeit, Variablen einer Python-Funktion zu überprüfen. Zum Beispiel möchte ich den Typ und den Wert der Argumente überprüfen. Gibt es dafür ein Modul? Oder sollte ich so etwas wie Dekorateure oder eine bestimmte Redewendung verwenden?

def my_function(a, b, c):
    """An example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

Antworten:


81

Die pythonischste Redewendung besteht darin, klar zu dokumentieren, was die Funktion erwartet, und dann einfach zu versuchen, das zu verwenden, was an Ihre Funktion übergeben wird, und entweder Ausnahmen zu verbreiten oder nur Attributfehler abzufangen und TypeErrorstattdessen a auszulösen. Die Typprüfung sollte so weit wie möglich vermieden werden, da sie gegen die Typisierung von Enten verstößt. Wertetests können je nach Kontext in Ordnung sein.

Der einzige Ort, an dem die Validierung wirklich sinnvoll ist, ist der Einstiegspunkt des Systems oder Subsystems, z. B. Webformulare, Befehlszeilenargumente usw. Überall sonst liegt es in der Verantwortung des Aufrufers, geeignete Argumente zu übergeben, solange Ihre Funktionen ordnungsgemäß dokumentiert sind.


3
@carmellose: Die Verwendung locals()wird wahrscheinlich zu einer nutzlosen Komplikation führen - tatsächlich sehe ich keinen Anwendungsfall dafür, da Sie Ihre benannten Parameternamen (offensichtlich <g>) bereits kennen und direkt darauf zugreifen können argsund kwargswenn Ihre Funktion sie verwendet. Assertion dient hauptsächlich zum Debuggen. Wenn der Vertrag Ihrer Funktion lautet, dass arg 'a' ein int zwischen 0 und 10 und das Argument 'b' eine nicht leere Zeichenfolge sein muss, lösen Sie die entsprechenden Ausnahmetypen aus, dh TypeErroroder ValueError- try int('a')und int(None)in Ihrer Python-Shell.
Bruno Desthuilliers

4
Soweit es mich betrifft, verwende ich die Behauptung nur für Fälle, in denen "dies unmöglich passieren kann" (die, wie wir alle wissen, irgendwann irgendwann passieren). Beachten Sie, dass "optimierter" Bytecode (.pyo-Dateien) die Zusicherungen überspringt, sodass Sie sich für den Produktionscode <g> besser nicht auf AssertionError verlassen sollten.
Bruno Desthuilliers

9
Es ist vielleicht nicht Pythonic, aber ich würde empfehlen, Schnittstellen zwischen Modulen durchzusetzen, insbesondere wenn Sie sie verteilen. Dies erleichtert sowohl die Entwicklung als auch die Verwendung erheblich und gilt für alle Sprachen
Peter R

23
Ich verabscheue autoritäre Nichtantworten, die sich auf Folgendes reduzieren: "Hör auf zu versuchen, das zu tun, was du tun willst, weil ich es besser weiß." Dies ist eine weitere in einer beklagenswert langen Reihe solcher Antworten. Es gibt zahlreiche triftige Gründe, Typen zu überprüfen, auf die diese Nichtantwort sogar anspielt. Unter Python 3.x sind Dekoratoren und Funktionsanmerkungen die optimale (und ehrlich gesagt offensichtliche) Antwort . Siehe auch Sweeneyrods brillanten @checkargsDekorateur unten. tl; dr Weniger Fundamentalismus; aktuellere Antworten.
Cecil Curry

7
Dies sollte nicht die akzeptierte Antwort sein. Ein weiterer sehr wichtiger Ort, an dem die Typen ausgerichtet werden müssen, sind externe APIs. Manchmal ist es unmöglich, einen Fehler aus einer solchen API heraus zu verbreiten, insbesondere aus einer nativen, und sie müssen nur mit präzisen Argumenttypen aufgerufen werden. In diesem Fall wirkt das Entenschreiben aktiv gegen Sie.
Bartek Banachewicz

98

In dieser langgestreckten Antwort implementieren wir einen Python 3.x-spezifischen Dekorator für die Typprüfung, der auf PEP 484- Typ-Hinweisen in weniger als 275 Zeilen reinen Python basiert (die meisten davon sind erklärende Dokumentationen und Kommentare) - stark optimiert für industrielle Stärke in der py.testPraxis mit einer gesteuerten Testsuite, die alle möglichen Randfälle ausübt.

Genießen Sie das unerwartete Fantastische Bärentippen :

>>> @beartype
... def spirit_bear(kermode: str, gitgaata: (str, int)) -> tuple:
...     return (kermode, gitgaata, "Moksgm'ol", 'Ursus americanus kermodei')
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
AssertionError: parameter kermode=0xdeadbeef not of <class "str">

Wie dieses Beispiel zeigt, unterstützt die Bärentypisierung explizit die Typprüfung von Parametern und Rückgabewerten, die entweder als einfache Typen oder als Tupel solcher Typen bezeichnet werden. Golly!

OK, das ist eigentlich nicht beeindruckend. @beartypeähnelt jedem anderen Python 3.x-spezifischen Dekorator für die Typprüfung, der auf Typhinweisen im PEP 484- Stil in weniger als 275 Zeilen Pure-Python basiert . Also, was ist das Problem, Bub?

Reine Bruteforce Hardcore-Effizienz

Das Eingeben von Bären ist räumlich und zeitlich erheblich effizienter als alle vorhandenen Implementierungen der Typprüfung in Python nach bestem Wissen und Gewissen. ( Dazu später mehr. )

Effizienz spielt in Python jedoch normalerweise keine Rolle. Wenn dies der Fall wäre, würden Sie Python nicht verwenden. Weicht die Typprüfung tatsächlich von der etablierten Norm ab, eine vorzeitige Optimierung in Python zu vermeiden? Ja. Ja tut es.

Betrachten Sie die Profilerstellung, die jeder interessierenden profilierten Metrik (z. B. Funktionsaufrufe, Zeilen) unvermeidbaren Overhead hinzufügt. Um genaue Ergebnisse zu gewährleisten, wird dieser Overhead durch die Nutzung optimierter C-Erweiterungen (z. B. der _lsprofvom cProfileModul genutzten C-Erweiterung ) anstelle von nicht optimiertem Pure-Python (z. B. dem profileModul) verringert . Effizienz ist bei der Profilerstellung wirklich wichtig.

Die Typprüfung ist nicht anders. Die Typprüfung erhöht den Aufwand für jeden von Ihrer Anwendung geprüften Funktionsaufruftyp - im Idealfall für alle . Um zu verhindern, dass gut gemeinte (aber leider kleinmütige) Mitarbeiter die Typprüfung entfernen, die Sie nach dem koffeinhaltigen Allnighter am vergangenen Freitag stillschweigend zu Ihrer geriatrischen Django-Web-App hinzugefügt haben , muss die Typprüfung schnell erfolgen. So schnell, dass niemand merkt, dass es da ist, wenn Sie es hinzufügen, ohne es jemandem zu sagen. Ich mache das die ganze Zeit! Hören Sie auf, dies zu lesen, wenn Sie ein Mitarbeiter sind.

Wenn selbst lächerliche Geschwindigkeit für Ihre gefräßige Anwendung nicht ausreicht, kann die Bärentypisierung durch Deaktivieren von Python-Optimierungen (z. B. durch Übergeben der -OOption an den Python-Interpreter) global deaktiviert werden :

$ python3 -O
# This succeeds only when type checking is optimized away. See above!
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
(0xdeadbeef, 'People of the Cane', "Moksgm'ol", 'Ursus americanus kermodei')

Nur weil. Willkommen beim Bärentippen.

Was zum...? Warum "Bär"? Du bist ein Nackenbart, oder?

Bei der Bärentypisierung handelt es sich um eine Bare-Metal-Typprüfung, dh eine Typprüfung, die dem manuellen Ansatz der Typprüfung in Python so nahe wie möglich kommt. Die Bärentypisierung soll keine Leistungseinbußen, Kompatibilitätsbeschränkungen oder Abhängigkeiten von Drittanbietern nach sich ziehen (über die ohnehin durch den manuellen Ansatz auferlegten hinaus). Die Bärentypisierung kann ohne Änderung nahtlos in vorhandene Codebasen und Testsuiten integriert werden.

Jeder kennt wahrscheinlich den manuellen Ansatz. Sie übergeben manuell assertjeden Parameter, der an jede Funktion in Ihrer Codebasis zurückgegeben wird , und / oder geben einen Wert zurück . Welche Kesselplatte könnte einfacher oder banaler sein? Wir haben es alle hundertmal bei Googleplex gesehen und uns jedes Mal ein wenig im Mund übergeben. Wiederholung wird schnell alt. TROCKEN , yo.

Bereiten Sie Ihre Erbrochenenbeutel vor. Nehmen wir der Kürze halber eine vereinfachte easy_spirit_bear()Funktion an, die nur einen einzigen strParameter akzeptiert . So sieht der manuelle Ansatz aus:

def easy_spirit_bear(kermode: str) -> str:
    assert isinstance(kermode, str), 'easy_spirit_bear() parameter kermode={} not of <class "str">'.format(kermode)
    return_value = (kermode, "Moksgm'ol", 'Ursus americanus kermodei')
    assert isinstance(return_value, str), 'easy_spirit_bear() return value {} not of <class "str">'.format(return_value)
    return return_value

Python 101, richtig? Viele von uns haben diese Klasse bestanden.

Die Bear-Typisierung extrahiert die vom obigen Ansatz manuell durchgeführte Typprüfung in eine dynamisch definierte Wrapper-Funktion, die automatisch dieselben Überprüfungen durchführt - mit dem zusätzlichen Vorteil, dass TypeErroreher granulare als mehrdeutige AssertionErrorAusnahmen ausgelöst werden . So sieht der automatisierte Ansatz aus:

def easy_spirit_bear_wrapper(*args, __beartype_func=easy_spirit_bear, **kwargs):
    if not (
        isinstance(args[0], __beartype_func.__annotations__['kermode'])
        if 0 < len(args) else
        isinstance(kwargs['kermode'], __beartype_func.__annotations__['kermode'])
        if 'kermode' in kwargs else True):
            raise TypeError(
                'easy_spirit_bear() parameter kermode={} not of {!r}'.format(
                args[0] if 0 < len(args) else kwargs['kermode'],
                __beartype_func.__annotations__['kermode']))

    return_value = __beartype_func(*args, **kwargs)

    if not isinstance(return_value, __beartype_func.__annotations__['return']):
        raise TypeError(
            'easy_spirit_bear() return value {} not of {!r}'.format(
                return_value, __beartype_func.__annotations__['return']))

    return return_value

Es ist langatmig. Aber es ist im Grunde auch * so schnell wie der manuelle Ansatz. * Schielen empfohlen.

Beachten Sie das völlige Fehlen einer Funktionsprüfung oder -iteration in der Wrapper-Funktion, die eine ähnliche Anzahl von Tests wie die ursprüngliche Funktion enthält - allerdings mit den zusätzlichen (möglicherweise vernachlässigbaren) Kosten für das Testen, ob und wie die zu überprüfenden Parameter an die übergeben werden aktueller Funktionsaufruf. Sie können nicht jede Schlacht gewinnen.

Können solche Wrapper-Funktionen tatsächlich zuverlässig generiert werden, um beliebige Funktionen in weniger als 275 Zeilen reinem Python zu überprüfen? Schlange Plisskin sagt: "Wahre Geschichte. Haben Sie einen Rauch?"

Und ja. Ich kann einen Nackenbart haben.

Nein, Srsly. Warum "Bär"?

Bär schlägt Ente. Ente kann fliegen, aber Bär kann Lachs auf Ente werfen. In Kanada kann Sie die Natur überraschen.

Nächste Frage.

Was ist überhaupt so heiß an Bären?

Bestehende Lösungen führen keine Bare-Metal-Typprüfung durch - zumindest keine, auf die ich gestoßen bin. Sie alle überprüfen iterativ die Signatur der typgeprüften Funktion bei jedem Funktionsaufruf . Während der Aufwand für die erneute Überprüfung für einen einzelnen Anruf vernachlässigbar ist, ist er normalerweise nicht vernachlässigbar, wenn er über alle Anrufe hinweg aggregiert wird. Wirklich, wirklich nicht zu vernachlässigen.

Es geht jedoch nicht nur um Effizienz. Bestehende Lösungen berücksichtigen häufig auch keine häufigen Randfälle. Dies schließt die meisten, wenn nicht alle Spielzeugdekorateure ein, die hier und anderswo als Stackoverflow-Antworten bereitgestellt werden. Klassische Fehler sind:

  • Fehler beim Eingeben von Check-Keyword-Argumenten und / oder Rückgabewerten (z. B. Sweeneyrods @checkargsDekorator ).
  • Keine Unterstützung von Tupeln (dh Gewerkschaften) von Typen, die vom isinstance()eingebauten System akzeptiert werden .
  • Fehler beim Übertragen des Namens, der Dokumentzeichenfolge und anderer identifizierender Metadaten von der ursprünglichen Funktion auf die Wrapper-Funktion.
  • Es ist nicht gelungen, mindestens einen Anschein von Komponententests zu liefern. ( Irgendwie kritisch. )
  • Auslösen generischer AssertionErrorAusnahmen anstelle spezifischer TypeErrorAusnahmen bei fehlgeschlagenen Typprüfungen. Aus Gründen der Granularität und Vernunft sollte die Typprüfung niemals generische Ausnahmen auslösen.

Das Eingeben von Bären ist dort erfolgreich, wo Nichtbären versagen. Alle eins, alle tragen!

Bear Typing Unbared

Durch die Typisierung von Bären werden die Raum- und Zeitkosten für die Überprüfung von Funktionssignaturen von der Funktionsaufrufzeit zur Funktionsdefinitionszeit verschoben, dh von der vom @beartypeDekorateur zurückgegebenen Wrapper-Funktion in den Dekorator selbst. Da der Dekorateur nur einmal pro Funktionsdefinition aufgerufen wird, bringt diese Optimierung Freude für alle.

Bärentippen ist ein Versuch, Ihren Typ Kuchen überprüfen zu lassen und ihn auch zu essen. Um dies zu tun @beartype:

  1. Überprüft die Signatur und Anmerkungen der ursprünglichen Funktion.
  2. Konstruiert dynamisch den Hauptteil des Wrapper-Funktionstyps und überprüft die ursprüngliche Funktion. Thaaat hat recht. Python-Code, der Python-Code generiert.
  3. exec()Deklariert diese Wrapper-Funktion dynamisch über das eingebaute System.
  4. Gibt diese Wrapper-Funktion zurück.

Sollen wir? Tauchen wir in das tiefe Ende ein.

# If the active Python interpreter is *NOT* optimized (e.g., option "-O" was
# *NOT* passed to this interpreter), enable type checking.
if __debug__:
    import inspect
    from functools import wraps
    from inspect import Parameter, Signature

    def beartype(func: callable) -> callable:
        '''
        Decorate the passed **callable** (e.g., function, method) to validate
        both all annotated parameters passed to this callable _and_ the
        annotated value returned by this callable if any.

        This decorator performs rudimentary type checking based on Python 3.x
        function annotations, as officially documented by PEP 484 ("Type
        Hints"). While PEP 484 supports arbitrarily complex type composition,
        this decorator requires _all_ parameter and return value annotations to
        be either:

        * Classes (e.g., `int`, `OrderedDict`).
        * Tuples of classes (e.g., `(int, OrderedDict)`).

        If optimizations are enabled by the active Python interpreter (e.g., due
        to option `-O` passed to this interpreter), this decorator is a noop.

        Raises
        ----------
        NameError
            If any parameter has the reserved name `__beartype_func`.
        TypeError
            If either:
            * Any parameter or return value annotation is neither:
              * A type.
              * A tuple of types.
            * The kind of any parameter is unrecognized. This should _never_
              happen, assuming no significant changes to Python semantics.
        '''

        # Raw string of Python statements comprising the body of this wrapper,
        # including (in order):
        #
        # * A "@wraps" decorator propagating the name, docstring, and other
        #   identifying metadata of the original function to this wrapper.
        # * A private "__beartype_func" parameter initialized to this function.
        #   In theory, the "func" parameter passed to this decorator should be
        #   accessible as a closure-style local in this wrapper. For unknown
        #   reasons (presumably, a subtle bug in the exec() builtin), this is
        #   not the case. Instead, a closure-style local must be simulated by
        #   passing the "func" parameter to this function at function
        #   definition time as the default value of an arbitrary parameter. To
        #   ensure this default is *NOT* overwritten by a function accepting a
        #   parameter of the same name, this edge case is tested for below.
        # * Assert statements type checking parameters passed to this callable.
        # * A call to this callable.
        # * An assert statement type checking the value returned by this
        #   callable.
        #
        # While there exist numerous alternatives (e.g., appending to a list or
        # bytearray before joining the elements of that iterable into a string),
        # these alternatives are either slower (as in the case of a list, due to
        # the high up-front cost of list construction) or substantially more
        # cumbersome (as in the case of a bytearray). Since string concatenation
        # is heavily optimized by the official CPython interpreter, the simplest
        # approach is (curiously) the most ideal.
        func_body = '''
@wraps(__beartype_func)
def func_beartyped(*args, __beartype_func=__beartype_func, **kwargs):
'''

        # "inspect.Signature" instance encapsulating this callable's signature.
        func_sig = inspect.signature(func)

        # Human-readable name of this function for use in exceptions.
        func_name = func.__name__ + '()'

        # For the name of each parameter passed to this callable and the
        # "inspect.Parameter" instance encapsulating this parameter (in the
        # passed order)...
        for func_arg_index, func_arg in enumerate(func_sig.parameters.values()):
            # If this callable redefines a parameter initialized to a default
            # value by this wrapper, raise an exception. Permitting this
            # unlikely edge case would permit unsuspecting users to
            # "accidentally" override these defaults.
            if func_arg.name == '__beartype_func':
                raise NameError(
                    'Parameter {} reserved for use by @beartype.'.format(
                        func_arg.name))

            # If this parameter is both annotated and non-ignorable for purposes
            # of type checking, type check this parameter.
            if (func_arg.annotation is not Parameter.empty and
                func_arg.kind not in _PARAMETER_KIND_IGNORED):
                # Validate this annotation.
                _check_type_annotation(
                    annotation=func_arg.annotation,
                    label='{} parameter {} type'.format(
                        func_name, func_arg.name))

                # String evaluating to this parameter's annotated type.
                func_arg_type_expr = (
                    '__beartype_func.__annotations__[{!r}]'.format(
                        func_arg.name))

                # String evaluating to this parameter's current value when
                # passed as a keyword.
                func_arg_value_key_expr = 'kwargs[{!r}]'.format(func_arg.name)

                # If this parameter is keyword-only, type check this parameter
                # only by lookup in the variadic "**kwargs" dictionary.
                if func_arg.kind is Parameter.KEYWORD_ONLY:
                    func_body += '''
    if {arg_name!r} in kwargs and not isinstance(
        {arg_value_key_expr}, {arg_type_expr}):
        raise TypeError(
            '{func_name} keyword-only parameter '
            '{arg_name}={{}} not a {{!r}}'.format(
                {arg_value_key_expr}, {arg_type_expr}))
'''.format(
                        func_name=func_name,
                        arg_name=func_arg.name,
                        arg_type_expr=func_arg_type_expr,
                        arg_value_key_expr=func_arg_value_key_expr,
                    )
                # Else, this parameter may be passed either positionally or as
                # a keyword. Type check this parameter both by lookup in the
                # variadic "**kwargs" dictionary *AND* by index into the
                # variadic "*args" tuple.
                else:
                    # String evaluating to this parameter's current value when
                    # passed positionally.
                    func_arg_value_pos_expr = 'args[{!r}]'.format(
                        func_arg_index)

                    func_body += '''
    if not (
        isinstance({arg_value_pos_expr}, {arg_type_expr})
        if {arg_index} < len(args) else
        isinstance({arg_value_key_expr}, {arg_type_expr})
        if {arg_name!r} in kwargs else True):
            raise TypeError(
                '{func_name} parameter {arg_name}={{}} not of {{!r}}'.format(
                {arg_value_pos_expr} if {arg_index} < len(args) else {arg_value_key_expr},
                {arg_type_expr}))
'''.format(
                    func_name=func_name,
                    arg_name=func_arg.name,
                    arg_index=func_arg_index,
                    arg_type_expr=func_arg_type_expr,
                    arg_value_key_expr=func_arg_value_key_expr,
                    arg_value_pos_expr=func_arg_value_pos_expr,
                )

        # If this callable's return value is both annotated and non-ignorable
        # for purposes of type checking, type check this value.
        if func_sig.return_annotation not in _RETURN_ANNOTATION_IGNORED:
            # Validate this annotation.
            _check_type_annotation(
                annotation=func_sig.return_annotation,
                label='{} return type'.format(func_name))

            # Strings evaluating to this parameter's annotated type and
            # currently passed value, as above.
            func_return_type_expr = (
                "__beartype_func.__annotations__['return']")

            # Call this callable, type check the returned value, and return this
            # value from this wrapper.
            func_body += '''
    return_value = __beartype_func(*args, **kwargs)
    if not isinstance(return_value, {return_type}):
        raise TypeError(
            '{func_name} return value {{}} not of {{!r}}'.format(
                return_value, {return_type}))
    return return_value
'''.format(func_name=func_name, return_type=func_return_type_expr)
        # Else, call this callable and return this value from this wrapper.
        else:
            func_body += '''
    return __beartype_func(*args, **kwargs)
'''

        # Dictionary mapping from local attribute name to value. For efficiency,
        # only those local attributes explicitly required in the body of this
        # wrapper are copied from the current namespace. (See below.)
        local_attrs = {'__beartype_func': func}

        # Dynamically define this wrapper as a closure of this decorator. For
        # obscure and presumably uninteresting reasons, Python fails to locally
        # declare this closure when the locals() dictionary is passed; to
        # capture this closure, a local dictionary must be passed instead.
        exec(func_body, globals(), local_attrs)

        # Return this wrapper.
        return local_attrs['func_beartyped']

    _PARAMETER_KIND_IGNORED = {
        Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD,
    }
    '''
    Set of all `inspect.Parameter.kind` constants to be ignored during
    annotation- based type checking in the `@beartype` decorator.

    This includes:

    * Constants specific to variadic parameters (e.g., `*args`, `**kwargs`).
      Variadic parameters cannot be annotated and hence cannot be type checked.
    * Constants specific to positional-only parameters, which apply to non-pure-
      Python callables (e.g., defined by C extensions). The `@beartype`
      decorator applies _only_ to pure-Python callables, which provide no
      syntactic means of specifying positional-only parameters.
    '''

    _RETURN_ANNOTATION_IGNORED = {Signature.empty, None}
    '''
    Set of all annotations for return values to be ignored during annotation-
    based type checking in the `@beartype` decorator.

    This includes:

    * `Signature.empty`, signifying a callable whose return value is _not_
      annotated.
    * `None`, signifying a callable returning no value. By convention, callables
      returning no value are typically annotated to return `None`. Technically,
      callables whose return values are annotated as `None` _could_ be
      explicitly checked to return `None` rather than a none-`None` value. Since
      return values are safely ignorable by callers, however, there appears to
      be little real-world utility in enforcing this constraint.
    '''

    def _check_type_annotation(annotation: object, label: str) -> None:
        '''
        Validate the passed annotation to be a valid type supported by the
        `@beartype` decorator.

        Parameters
        ----------
        annotation : object
            Annotation to be validated.
        label : str
            Human-readable label describing this annotation, interpolated into
            exceptions raised by this function.

        Raises
        ----------
        TypeError
            If this annotation is neither a new-style class nor a tuple of
            new-style classes.
        '''

        # If this annotation is a tuple, raise an exception if any member of
        # this tuple is not a new-style class. Note that the "__name__"
        # attribute tested below is not defined by old-style classes and hence
        # serves as a helpful means of identifying new-style classes.
        if isinstance(annotation, tuple):
            for member in annotation:
                if not (
                    isinstance(member, type) and hasattr(member, '__name__')):
                    raise TypeError(
                        '{} tuple member {} not a new-style class'.format(
                            label, member))
        # Else if this annotation is not a new-style class, raise an exception.
        elif not (
            isinstance(annotation, type) and hasattr(annotation, '__name__')):
            raise TypeError(
                '{} {} neither a new-style class nor '
                'tuple of such classes'.format(label, annotation))

# Else, the active Python interpreter is optimized. In this case, disable type
# checking by reducing this decorator to the identity decorator.
else:
    def beartype(func: callable) -> callable:
        return func

Und Leycec sprach: Lass den @beartypeTyp schnell prüfen! Und es war so.

Vorsichtsmaßnahmen, Flüche und leere Versprechen

Nichts ist perfekt. Sogar Bären tippen.

Vorsichtsmaßnahme I: Standardwerte deaktiviert

Bei der Bärentypisierung werden keine nicht übergebenen Parameter überprüft, denen Standardwerte zugewiesen wurden. Theoretisch könnte es. Aber nicht in 275 Zeilen oder weniger und schon gar nicht als Stackoverflow-Antwort.

Die sichere (... wahrscheinlich völlig unsichere ) Annahme ist, dass Funktionsimplementierer behaupten, sie wüssten, was sie taten, als sie Standardwerte definierten. Da Standardwerte normalerweise Konstanten sind (... besser! ), Würde eine erneute Überprüfung der Konstantentypen, die sich bei jedem Funktionsaufruf, der einem oder mehreren Standardwerten zugewiesen wurde, nie ändern, gegen den Grundgedanken der Bärentypisierung verstoßen: "Nicht wiederholen dich vorbei und oooover und oooo-oooover wieder. "

Zeigen Sie mir falsch und ich werde Sie mit Upvotes überschütten.

Vorsichtsmaßnahme II: Kein PEP 484

PEP 484 ( "Type Hints" ) formalisierte die Verwendung von Funktionsanmerkungen, die zuerst von PEP 3107 ( "Function Annotations" ) eingeführt wurden. Python 3.5 unterstützt oberflächlich diese Formalisierung mit einem neuen Top-Level - typingModul , ein Standard - API für beliebig komplexe Typen von einfacheren Typen Komponieren ( zum Beispiel Callable[[Arg1Type, Arg2Type], ReturnType], eine Art , die eine Funktion mit zwei Argumenten des Typs beschreibt , Arg1Typeund Arg2Typeund einen Wert vom Typ der Rückkehr ReturnType).

Die Bärentippung unterstützt keine von ihnen. Theoretisch könnte es. Aber nicht in 275 Zeilen oder weniger und schon gar nicht als Stackoverflow-Antwort.

Die isinstance()Bärentypisierung unterstützt jedoch Gewerkschaften von Typen auf dieselbe Weise wie die integrierte Typvereinigung: als Tupel. Dies entspricht oberflächlich dem typing.UnionTyp - mit der offensichtlichen Einschränkung, dass typing.Unionbeliebig komplexe Typen unterstützt werden, während Tupel, die von der @beartypeUnterstützung akzeptiert werden , nur einfache Klassen unterstützen. Zu meiner Verteidigung 275 Zeilen.

Tests oder es ist nicht passiert

Hier ist der Kern davon. Erhalten Sie es, Kern ? Ich werde jetzt aufhören.

Wie beim @beartypeDekorateur selbst können diese py.testTests ohne Änderung nahtlos in vorhandene Testsuiten integriert werden. Wertvoll, nicht wahr?

Jetzt hat niemand nach dem obligatorischen Nackenbart-Rant gefragt.

Eine Geschichte der API-Gewalt

Python 3.5 bietet keine tatsächliche Unterstützung für die Verwendung von PEP 484-Typen. wat?

Es ist wahr: keine Typprüfung, keine Typinferenz, keine Typnuss. Stattdessen wird von Entwicklern erwartet, dass sie ihre gesamte Codebasis routinemäßig über schwergewichtige CPython-Interpreter-Wrapper von Drittanbietern ausführen, die ein Faksimile dieser Unterstützung (z . B. mypy ) implementieren . Natürlich schreiben diese Wrapper vor:

  • Eine Kompatibilitätsstrafe. Wie die offiziellen mypy-FAQ als Antwort auf die häufig gestellte Frage "Kann ich mit mypy meinen vorhandenen Python-Code überprüfen?" Zugeben: " Das hängt davon ab. Die Kompatibilität ist ziemlich gut, aber einige Python-Funktionen sind noch nicht implementiert oder werden nicht vollständig unterstützt." Eine nachfolgende FAQ-Antwort verdeutlicht diese Inkompatibilität, indem Folgendes angegeben wird:
    • "... Ihr Code muss Attribute explizit machen und eine explizite Protokolldarstellung verwenden." Die Grammatikpolizei sieht Ihr "explizites" und bringt Sie zu einem impliziten Stirnrunzeln.
    • "Mypy unterstützt die modulare, effiziente Typprüfung, und dies scheint die Typprüfung einiger Sprachfunktionen auszuschließen, z. B. das willkürliche Hinzufügen von Methoden zur Laufzeit. Es ist jedoch wahrscheinlich, dass viele dieser Funktionen in eingeschränkter Form unterstützt werden (z. B.) Die Laufzeitänderung wird nur für Klassen oder Methoden unterstützt, die als dynamisch oder 'patchbar' registriert sind. "
    • Eine vollständige Liste der syntaktischen Inkompatibilitäten finden Sie unter "Umgang mit häufig auftretenden Problemen" . Es ist nicht schön. Sie wollten nur die Typprüfung und jetzt haben Sie Ihre gesamte Codebasis überarbeitet und zwei Tage nach der Veröffentlichung des Kandidaten alle Builds gebrochen, und der hübsche HR-Zwerg in lässiger Geschäftskleidung schiebt einen rosa Slip durch den Spalt in Ihrer Kabine. Vielen Dank, mypy.
  • Eine Leistungsminderung trotz Interpretation von statisch typisiertem Code. Vierzig Jahre hartgesottener Informatik sagen uns, dass (... alles andere gleich ist ) das Interpretieren von statisch typisiertem Code schneller und nicht langsamer sein sollte als das Interpretieren von dynamisch typisiertem Code. In Python ist up das neue down.
  • Zusätzliche nicht triviale Abhängigkeiten, die zunehmen:
    • Die fehlerbehaftete Fragilität der Projektbereitstellung, insbesondere plattformübergreifend.
    • Die Wartungslast der Projektentwicklung.
    • Mögliche Angriffsfläche.

Ich frage Guido: "Warum? Warum eine abstrakte API erfinden, wenn Sie nicht bereit waren, eine konkrete API aufzubauen, die tatsächlich etwas mit dieser Abstraktion macht?" Warum das Schicksal einer Million Pythonisten der arthritischen Hand des freien Open-Source-Marktplatzes überlassen? Warum noch ein Techno-Problem erstellen, das mit einem 275-Zeilen-Dekorator in der offiziellen Python-Stdlib trivial hätte gelöst werden können?

Ich habe kein Python und ich muss schreien.


12
Behalten Sie das Meta bitte im Meta.
Meagar

33
Ich hatte auf einen Anschein von inhaltlichen Kommentaren gehofft. Ich werde stattdessen von einer disziplinarischen Normalisierung begrüßt. Für die uneingeschränkte Sprache, die hauptsächlich von den veröffentlichten Drehbüchern von „Monty Pythons Flying Circus“ inspiriert ist, ist das schmutzige Fenster akzeptablen Verhaltens unter Pythonisten überraschend ... eng. Unnötig zu sagen, dass ich im Ganzen nicht einverstanden bin: Wir brauchen wesentlich mehr Bewusstseinsstrom, Meme, Witze, aufgeklärte Gnosis und noetische Poesie. Mehr mehrsilbige Neuheit. Weniger einsilbige Normalität.
Cecil Curry

2
Dies ist ein äußerst nützlicher Dekorateur - es könnte sich lohnen, auf Github zu hosten, damit wir über spätere Verbesserungen auf dem Laufenden bleiben können
user2682863

5
Vielen Dank für die Mühe, aber diese Antwort ist viel zu lang für eine einfache Frage. Die meisten von uns suchen nach der Antwort "Google".
Izik

3
@Izik: Ich suche nach qualitativ hochwertigen Antworten und komme zu einem Punkt, an dem ich in ein oder zwei Wochen nicht mehr suchen muss. Wenn das Problem eine kurze Antwort erfordert, großartig, wenn es mehr Worte erfordert, dann soll es so sein. Dies ist auf lange Sicht viel nützlicher als Hunderte von Einzeilern, die mein Verständnis nicht verbessern und im Grunde alle gleich sind.
Make42

27

Bearbeiten: Ab 2019 gibt es mehr Unterstützung für die Verwendung von Typanmerkungen und statischen Überprüfungen in Python. überprüfen Sie die aus der Eingabe - Modul und mypy . Die Antwort von 2013 lautet:


Die Typprüfung ist im Allgemeinen nicht Pythonic. In Python ist es üblicher, Enten zu tippen . Beispiel:

Nehmen Sie in Ihrem Code an, dass das Argument (in Ihrem Beispiel a) wie ein läuft und wie ein intquakt int. Zum Beispiel:

def my_function(a):
    return a + 7

Dies bedeutet, dass Ihre Funktion nicht nur mit Ganzzahlen funktioniert, sondern auch mit Floats und jeder benutzerdefinierten Klasse mit der __add__definierten Methode. Daher muss weniger (manchmal nichts) getan werden, wenn Sie oder eine andere Person Ihre Funktion erweitern möchten arbeite mit etwas anderem. In einigen Fällen benötigen Sie jedoch möglicherweise eine int, sodass Sie Folgendes tun können:

def my_function(a):
    b = int(a) + 7
    c = (5, 6, 3, 123541)[b]
    return c

und die Funktion funktioniert immer noch für alle a, die das definieren__int__ Methode .

Als Antwort auf Ihre anderen Fragen halte ich es für das Beste (wie andere Antworten gesagt haben, tun Sie dies entweder:

def my_function(a, b, c):
    assert 0 < b < 10
    assert c        # A non-empty string has the Boolean value True

oder

def my_function(a, b, c):
    if 0 < b < 10:
        # Do stuff with b
    else:
        raise ValueError
    if c:
        # Do stuff with c
    else:
        raise ValueError

Einige von mir erstellte Dekoratoren zur Typprüfung:

import inspect

def checkargs(function):
    def _f(*arguments):
        for index, argument in enumerate(inspect.getfullargspec(function)[0]):
            if not isinstance(arguments[index], function.__annotations__[argument]):
                raise TypeError("{} is not of type {}".format(arguments[index], function.__annotations__[argument]))
        return function(*arguments)
    _f.__doc__ = function.__doc__
    return _f

def coerceargs(function):
    def _f(*arguments):
        new_arguments = []
        for index, argument in enumerate(inspect.getfullargspec(function)[0]):
            new_arguments.append(function.__annotations__[argument](arguments[index]))
        return function(*new_arguments)
    _f.__doc__ = function.__doc__
    return _f

if __name__ == "__main__":
    @checkargs
    def f(x: int, y: int):
        """
        A doc string!
        """
        return x, y

    @coerceargs
    def g(a: int, b: int):
        """
        Another doc string!
        """
        return a + b

    print(f(1, 2))
    try:
        print(f(3, 4.0))
    except TypeError as e:
        print(e)

    print(g(1, 2))
    print(g(3, 4.0))

1
checkargs und coerceargs funktionieren nicht für Funktionen, bei denen nicht für alle Parameter der Standardtyp angegeben ist. Beispiel: g (a: int, b)
Igor Malin

16

Eine Möglichkeit ist zu verwenden assert:

def myFunction(a,b,c):
    "This is an example function I'd like to check arguments of"
    assert isinstance(a, int), 'a should be an int'
    # or if you want to allow whole number floats: assert int(a) == a
    assert b > 0 and b < 10, 'b should be betwen 0 and 10'
    assert isinstance(c, str) and c, 'c should be a non-empty string'

9
Ich erwarte nicht, dass ein Callable einen AssertionError auslöst, wenn ich seinen Vertrag nicht respektiere, und dies ist nicht das, was Sie in der Standardbibliothek finden. Versuchen Sie int ('a') und int (None) in Ihrer Python-Shell ... Ja, ValueErrorund TypeError.
Bruno Desthuilliers

3
danke, ich finde Behauptungen bequem. Leute benutzen Python aus verschiedenen Gründen. Einige verwenden Produktionscode, andere nur Prototypen. Dies ist eine schnelle Möglichkeit, die Eingabe Ihrer Funktionen einzuschränken. Wenn ich eine Funktion für die Standardbibliothek schreiben würde, wäre ich wahrscheinlich expliziter.
Matthew Plourde

9
Behauptungen sollten als einfache Option betrachtet werden, die oft weitaus besser ist als nichts - sie führt zu frühen Fehlern und kann helfen, Code zu dokumentieren. Ich denke, sie haben einen ausgezeichneten Platz in unserem Code.
KenFar

1
+1 für besser als nichts, aber vermeiden Sie es, sie für die externe Eingabevalidierung zu verwenden, und verwenden Sie sie für Intra-Code-Prüfungen.
Christophe Roussy

4
Achten Sie auf die Verwendung assertim Produktionscode. Es kann ignoriert werden, je nachdem, in welcher Umgebung Ihr Code ausgeführt wird. Schauen Sie sich diese Antwort an: stackoverflow.com/a/1838411/345290
Renan Ivo

7

Sie können Type Enforcement verwenden. Dekoratoren akzeptieren / zurückgeben von PythonDecoratorLibrary Es ist sehr einfach und lesbar:

@accepts(int, int, float)
def myfunc(i1, i2, i3):
    pass

2
Unter Python 3.x Funktionsanmerkungen (z. def myfunc(i1: int, i2: int, i3: float) ) sind ein zutiefst mehr Pythonic mittels Typen deklarieren. Siehe sweeneyrod ‚s @checkargsDekorateur für eine robuste Art Überprüfung Lösung mit Funktion Anmerkungen in weniger als 10 (!) Zeilen Code.
Cecil Curry

Wie importierst du das PythonDecoratorLibrary?
Pablo

5

Es gibt verschiedene Möglichkeiten, um zu überprüfen, was eine Variable in Python ist. Um einige zu nennen:

  • isinstance(obj, type)Die Funktion nimmt Ihre Variable objund gibt Ihnen an, dass Truees sich um denselben Typ handelt, den typeSie aufgelistet haben.

  • issubclass(obj, class)Funktion, die eine Variable aufnimmt objund Ihnen gibt, Trueob objes sich um eine Unterklasse von handelt class. So issubclass(Rabbit, Animal)würde Ihnen zum Beispiel ein TrueWert geben

  • hasattrist ein weiteres Beispiel, das durch diese Funktion demonstriert wird super_len:


def super_len(o):
    if hasattr(o, '__len__'):
        return len(o)

    if hasattr(o, 'len'):
        return o.len

    if hasattr(o, 'fileno'):
        try:
            fileno = o.fileno()
        except io.UnsupportedOperation:
            pass
        else:
            return os.fstat(fileno).st_size

    if hasattr(o, 'getvalue'):
        # e.g. BytesIO, cStringIO.StringI
        return len(o.getvalue())

hasattrneigt eher zur Ententypisierung und zu etwas, das normalerweise pythonischer ist, aber dieser Begriff hat eine hohe Meinung.

Nur als Hinweis: assertAnweisungen werden normalerweise zum Testen verwendet, andernfalls werden nur if/elseAnweisungen verwendet.


5

Ich habe in letzter Zeit einige Nachforschungen zu diesem Thema angestellt, da ich mit den vielen Bibliotheken, die ich dort herausgefunden habe, nicht zufrieden war .

Am Ende habe ich eine Bibliothek entwickelt, um dies zu beheben . Sie heißt valid8 . Wie in der Dokumentation erläutert, dient es hauptsächlich der Wertüberprüfung (obwohl es auch mit einfachen Typüberprüfungsfunktionen geliefert wird), und Sie möchten es möglicherweise einem PEP484-basierten Typprüfer wie z erzwingen oder pytypes .

Auf diese Weise würden Sie die Validierung valid8allein (und) durchführenmini_lambda in Ihrem Fall tatsächlich die Validierungslogik definieren - dies ist jedoch nicht obligatorisch):

# for type validation
from numbers import Integral
from valid8 import instance_of

# for value validation
from valid8 import validate_arg
from mini_lambda import x, s, Len

@validate_arg('a', instance_of(Integral))
@validate_arg('b', (0 < x) & (x < 10))
@validate_arg('c', instance_of(str), Len(s) > 0)
def my_function(a: Integral, b, c: str):
    """an example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

# check that it works
my_function(0.2, 1, 'r')  # InputValidationError for 'a' HasWrongType: Value should be an instance of <class 'numbers.Integral'>. Wrong value: [0.2].
my_function(0, 0, 'r')    # InputValidationError for 'b' [(x > 0) & (x < 10)] returned [False]
my_function(0, 1, 0)      # InputValidationError for 'c' Successes: [] / Failures: {"instance_of_<class 'str'>": "HasWrongType: Value should be an instance of <class 'str'>. Wrong value: [0]", 'len(s) > 0': "TypeError: object of type 'int' has no len()"}.
my_function(0, 1, '')     # InputValidationError for 'c' Successes: ["instance_of_<class 'str'>"] / Failures: {'len(s) > 0': 'False'}

Dies ist das gleiche Beispiel, in dem PEP484-Typhinweise verwendet und die Typprüfung an Folgendes delegiert werden enforce:

# for type validation
from numbers import Integral
from enforce import runtime_validation, config
config(dict(mode='covariant'))  # type validation will accept subclasses too

# for value validation
from valid8 import validate_arg
from mini_lambda import x, s, Len

@runtime_validation
@validate_arg('b', (0 < x) & (x < 10))
@validate_arg('c', Len(s) > 0)
def my_function(a: Integral, b, c: str):
    """an example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

# check that it works
my_function(0.2, 1, 'r')  # RuntimeTypeError 'a' was not of type <class 'numbers.Integral'>
my_function(0, 0, 'r')    # InputValidationError for 'b' [(x > 0) & (x < 10)] returned [False]
my_function(0, 1, 0)      # RuntimeTypeError 'c' was not of type <class 'str'>
my_function(0, 1, '')     # InputValidationError for 'c' [len(s) > 0] returned [False].

Können Sie sagen, wie valid8 mit bear_typing verglichen wird ?
Make42

1
„beartyping“ scheint ähnlich wie die meisten Typen Kontrolleure wie typeguard , pytypes , erzwingen ... außer , dass es nicht eine validierte und dokumentierte Bibliothek ist, ist es nicht PEP484 kompatibel ist (wie PyContracts ) und verwendet execden Wrapper läuft ein wenig zu machen schneller (auf Kosten des Nicht-Debuggens). valid8zielt auf die Validierung des Typs UND des Werts ab und kann mit einem vorhandenen PEP484-Typprüfer kombiniert werden, um sich nur auf die
Wertprüfung

2

Normalerweise machst du so etwas:

def myFunction(a,b,c):
   if not isinstance(a, int):
      raise TypeError("Expected int, got %s" % (type(a),))
   if b <= 0 or b >= 10:
      raise ValueError("Value %d out of range" % (b,))
   if not c:
      raise ValueError("String was empty")

   # Rest of function

2
Die erwarteten Ausnahmen sind TypeError bzw. ValueError.
Bruno Desthuilliers

Recht; Die in der Antwort verwendeten können jedoch von den von Ihnen erwähnten in Unterklassen unterteilt werden.
glglgl

Richtig, aber es waren nur Beispiele. Ich werde das Beispiel aktualisieren.
Mats Kindahl

@ MatsKindahl: Eine Fehlermeldung könnte auch hilfreich sein, dh:raise TypeError("Expected an int, got '%s'" % type(a))
Bruno Desthuilliers

2

Dies überprüft die Art der Eingabeargumente beim Aufrufen der Funktion:

def func(inp1:int=0,inp2:str="*"):

    for item in func.__annotations__.keys():
        assert isinstance(locals()[item],func.__annotations__[item])

    return (something)

first=7
second="$"
print(func(first,second))

Überprüfen Sie auch mit second=9(es muss Assertionsfehler geben)


Dies wird nur mit Python> = 3
Carmellose

1
def someFunc(a, b, c):
    params = locals()
    for _item in params:
        print type(params[_item]), _item, params[_item]

Demo:

>> someFunc(1, 'asd', 1.0)
>> <type 'int'> a 1
>> <type 'float'> c 1.0
>> <type 'str'> b asd

mehr über Einheimische ()


0

Wenn Sie überprüfen möchten **kwargs, *argssowie normale Argumente auf einmal, können Sie die verwendenlocals() Funktion als erste Anweisung in der Funktionsdefinition ein Wörterbuch der Argumente zu bekommen.

Verwenden Sie dann type(), um die Argumente zu untersuchen, z. B. während Sie über das Diktat iterieren.

def myfunc(my, args, to, this, function, **kwargs):
    d = locals()
    assert(type(d.get('x')) == str)
    for x in d:
        if x != 'x':
            assert(type(d[x]) == x
    for x in ['a','b','c']:
        assert(x in d)

    whatever more...

0

Wenn Sie die Validierung für mehrere Funktionen durchführen möchten, können Sie die Logik in einem Dekorator wie folgt hinzufügen:

def deco(func):
     def wrapper(a,b,c):
         if not isinstance(a, int)\
            or not isinstance(b, int)\
            or not isinstance(c, str):
             raise TypeError
         if not 0 < b < 10:
             raise ValueError
         if c == '':
             raise ValueError
         return func(a,b,c)
     return wrapper

und benutze es:

@deco
def foo(a,b,c):
    print 'ok!'

Hoffe das hilft!


3
Wenn Sie wirklich auf Typchecking bestehen, verwenden Sie bitte mindestens isinstanceTypeError und erhöhen Sie es.
Bruno Desthuilliers

@brunodesthuilliers danke für das Heads Up! Ich werde meine Antwort bearbeiten.
Paulo Bu

warum nicht return func(a, b, c)?
glglgl

1
@PauloBu: Was glglgl bedeutete, war, dass Ihr Rapper nicht nur die dekorierte Funktion aufrufen sollte, sondern auch das Ergebnis des Funktionsaufrufs zurückgeben sollte.
Bruno Desthuilliers

1
Ich werde wahrscheinlich in Schwierigkeiten geraten, wenn ich das hier sage, aber Sie können auch in Betracht ziehen, eine andere Sprache zu verwenden, wenn Sie wirklich viel Typprüfung benötigen
Christophe Roussy

0

Dies ist nicht die Lösung für Sie, aber wenn Sie die Funktionsaufrufe auf bestimmte Parametertypen beschränken möchten, müssen Sie den PROATOR {The Python Function Prototype Validator} verwenden. Sie können auf den folgenden Link verweisen. https://github.com/mohit-thakur-721/proator


-1
def myFunction(a,b,c):
"This is an example function I'd like to check arguments of"
    if type( a ) == int:
       #dostuff
    if 0 < b < 10:
       #dostuff
    if type( C ) == str and c != "":
       #dostuff
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.