Wie gehen Python-Funktionen mit den Typen der Parameter um, die Sie übergeben?

305

Wenn ich mich nicht irre, funktioniert das Erstellen einer Funktion in Python folgendermaßen:

def my_func(param1, param2):
    # stuff

Sie geben jedoch nicht die Typen dieser Parameter an. Wenn ich mich recht erinnere, ist Python eine stark typisierte Sprache. Daher sollte Python anscheinend nicht zulassen, dass Sie einen Parameter eines anderen Typs als den erwarteten Funktionsersteller übergeben. Woher weiß Python jedoch, dass der Benutzer der Funktion die richtigen Typen übergibt? Wird das Programm nur sterben, wenn es der falsche Typ ist, vorausgesetzt, die Funktion verwendet tatsächlich den Parameter? Müssen Sie den Typ angeben?

Leif Andersen
quelle
15
Ich denke, die akzeptierte Antwort in dieser Frage sollte aktualisiert werden, um den aktuellen Funktionen von Python besser zu entsprechen . Ich denke, diese Antwort macht den Job.
code_dredd

Antworten:

173

Python ist typisiert stark , weil jedes Objekt hat eine Art, jedes Objekt kennt seine Art, ist es unmöglich oder absichtlich versehentlich ein Objekt eines Typs zu verwenden , „als ob“ es eine Aufgabe eines war anders Art, und alle elementaren Operationen auf dem Objekt an seinen Typ delegiert.

Das hat nichts mit Namen zu tun . Ein Name in Python hat keinen "Typ": Wenn ein Name definiert ist, bezieht sich der Name auf ein Objekt , und das Objekt hat einen Typ (dies erzwingt jedoch keinen Typ für den Namen : a Name ist ein Name).

Ein Name in Python kann durchaus auf verschiedene Objekte zu unterschiedlichen Zeiten verweisen (wie in den meisten Programmiersprachen, wenn auch nicht in allen) - und es gibt keine Einschränkung für den Namen, sodass, wenn er einmal auf ein Objekt vom Typ X verwiesen hat, es ist dann nur auf andere Objekte vom Typ X. Einschränkungen verweisen immerdar eingeschränkt Namen nicht Teil des Konzepts der „starken Typisierung“ ist, obwohl einige Enthusiasten statischer Typisierung (wo Namen sie gezwungen werden, und in einem statischen, AKA compile- Zeit, Mode auch) missbrauchen den Begriff auf diese Weise.

Alex Martelli
quelle
71
Es scheint also, dass eine starke Typisierung nicht so stark ist. In diesem speziellen Fall ist sie schwächer als eine statische Typisierung zu diesem Aspekt. Bitte korrigieren Sie mich, wenn ich falsch liege.
Liang
19
@liang Das ist eine Meinung, also kannst du nicht richtig oder falsch sein. Es ist sicherlich auch meine Meinung, und ich habe viele Sprachen ausprobiert. Die Tatsache, dass ich meine IDE nicht verwenden kann, um den Typ (und damit die Mitglieder) der Parameter herauszufinden, ist ein Hauptnachteil von Python. Ob dieser Nachteil wichtiger ist als die Vorteile der Ententypisierung, hängt von der Person ab, die Sie fragen.
Maarten Bodewes
6
Dies beantwortet jedoch keine der Fragen: "Woher weiß Python jedoch, dass der Benutzer der Funktion die richtigen Typen übergibt? Wird das Programm nur sterben, wenn es der falsche Typ ist, vorausgesetzt, die Funktion verwendet tatsächlich den Parameter?" Müssen Sie den Typ angeben? " oder ..
qPCR4vir
4
@ qPCR4vir, jedes Objekt kann als Argument übergeben werden. Der Fehler (eine Ausnahme, das Programm "stirbt" nicht, wenn es zum Abfangen codiert ist, siehe try/ except) tritt auf, wenn eine Operation versucht wird, die das Objekt nicht unterstützt. In Python 3.5 können Sie jetzt optional Argumenttypen "angeben", aber es tritt per se kein Fehler auf, wenn die Spezifikation verletzt wird. Die Schreibnotation soll nur dazu dienen, Tools zu trennen, die Analysen usw. durchführen. Sie ändert das Verhalten von Python selbst nicht.
Alex Martelli
2
@ AlexMartelli. Danken! Für mich ist dies die richtige Antwort: "Der Fehler (eine Ausnahme, das Programm wird nicht" sterben ", wenn es codiert ist, um es
abzufangen
753

Die anderen Antworten haben gute Arbeit geleistet, um das Tippen von Enten und die einfache Antwort von tzot zu erklären :

Python hat keine Variablen wie andere Sprachen, in denen Variablen einen Typ und einen Wert haben. Es hat Namen, die auf Objekte verweisen, die ihren Typ kennen.

Eine interessante Sache hat sich jedoch seit 2010 (als die Frage zum ersten Mal gestellt wurde) geändert, nämlich die Implementierung von PEP 3107 (implementiert in Python 3). Sie können jetzt den Typ eines Parameters und den Typ des Rückgabetyps einer Funktion wie folgt angeben:

def pick(l: list, index: int) -> int:
    return l[index]

Wir können hier sehen, dass pick2 Parameter, eine Liste lund eine Ganzzahl, benötigt werden index. Es sollte auch eine Ganzzahl zurückgeben.

Hier wird also impliziert, dass les sich um eine Liste von Ganzzahlen handelt, die wir ohne großen Aufwand sehen können. Bei komplexeren Funktionen kann es jedoch etwas verwirrend sein, was die Liste enthalten soll. Wir möchten auch, dass der Standardwert index0 ist. Um dies zu lösen, können Sie pickstattdessen wie folgt schreiben :

def pick(l: "list of ints", index: int = 0) -> int:
    return l[index]

Beachten Sie, dass wir jetzt einen String als Typ eingeben l, der syntaktisch zulässig ist, aber nicht für das programmgesteuerte Parsen geeignet ist (worauf wir später zurückkommen werden).

Es ist wichtig zu beachten, dass Python kein a erhöht, TypeErrorwenn Sie einen Float übergeben index. Der Grund dafür ist einer der Hauptpunkte in Pythons Designphilosophie: "Wir sind alle einverstanden, dass Erwachsene hier sind" , was bedeutet, dass Sie dies erwarten Seien Sie sich bewusst, was Sie an eine Funktion übergeben können und was nicht. Wenn Sie wirklich Code schreiben möchten, der TypeErrors auslöst, können Sie mit der isinstanceFunktion überprüfen, ob das übergebene Argument vom richtigen Typ oder einer Unterklasse davon wie folgt ist:

def pick(l: list, index: int = 0) -> int:
    if not isinstance(l, list):
        raise TypeError
    return l[index]

Mehr darüber, warum Sie dies selten tun sollten und was Sie stattdessen tun sollten, wird im nächsten Abschnitt und in den Kommentaren besprochen.

PEP 3107 verbessert nicht nur die Lesbarkeit des Codes, sondern verfügt auch über mehrere passende Anwendungsfälle, über die Sie hier lesen können .


Die Typanmerkung hat in Python 3.5 mit der Einführung von PEP 484, das ein Standardmodul für Typhinweise einführt, viel mehr Aufmerksamkeit erhalten .

Diese Typhinweise stammen vom Typprüfer mypy ( GitHub ), der jetzt PEP 484- kompatibel ist.

Das Schreibmodul enthält eine ziemlich umfassende Sammlung von Tipphinweisen, darunter:

  • List, Tuple, Set, Map- für list, tuple, setund mapjeweils.
  • Iterable - nützlich für Generatoren.
  • Any - wenn es irgendetwas sein könnte.
  • Union- wenn es sich um etwas innerhalb eines bestimmten Satzes von Typen handeln könnte, im Gegensatz zu Any.
  • Optional- wenn es keine sein könnte . Abkürzung für Union[T, None].
  • TypeVar - mit Generika verwendet.
  • Callable - wird hauptsächlich für Funktionen verwendet, kann aber auch für andere aufrufbare Elemente verwendet werden.

Dies sind die häufigsten Typhinweise. Eine vollständige Auflistung finden Sie in der Dokumentation zum Schreibmodul .

Hier ist das alte Beispiel mit den im Schreibmodul eingeführten Anmerkungsmethoden:

from typing import List

def pick(l: List[int], index: int) -> int:
    return l[index]

Eine leistungsstarke Funktion ist die Callable, mit der Sie Annotationsmethoden eingeben können, die eine Funktion als Argument verwenden. Zum Beispiel:

from typing import Callable, Any, Iterable

def imap(f: Callable[[Any], Any], l: Iterable[Any]) -> List[Any]:
    """An immediate version of map, don't pass it any infinite iterables!"""
    return list(map(f, l))

Das obige Beispiel könnte mit der Verwendung von TypeVaranstelle von präziser werden Any, aber dies wurde dem Leser als Übung überlassen, da ich glaube, dass ich meine Antwort bereits mit zu vielen Informationen über die wunderbaren neuen Funktionen gefüllt habe, die durch Typhinweise ermöglicht werden.


Wenn zuvor Python-Code mit beispielsweise Sphinx dokumentiert wurde, konnten einige der oben genannten Funktionen durch Schreiben von Dokumentstrings erhalten werden, die wie folgt formatiert waren:

def pick(l, index):
    """
    :param l: list of integers
    :type l: list
    :param index: index at which to pick an integer from *l*
    :type index: int
    :returns: integer at *index* in *l*
    :rtype: int
    """
    return l[index]

Wie Sie sehen können, sind einige zusätzliche Zeilen erforderlich (die genaue Anzahl hängt davon ab, wie explizit Sie sein möchten und wie Sie Ihre Dokumentzeichenfolge formatieren). Aber jetzt sollte Ihnen klar sein, wie PEP 3107 eine Alternative bietet, die in vielerlei Hinsicht überlegen ist. Dies gilt insbesondere in Kombination mit PEP 484, das, wie wir gesehen haben, ein Standardmodul bereitstellt, das eine Syntax für diese Typhinweise / Anmerkungen definiert, die so verwendet werden kann, dass sie eindeutig und präzise und dennoch flexibel ist kraftvolle Kombination.

Meiner persönlichen Meinung nach ist dies eine der größten Funktionen in Python, die es je gab. Ich kann es kaum erwarten, dass die Leute anfangen, die Kraft davon zu nutzen. Entschuldigung für die lange Antwort, aber das passiert, wenn ich aufgeregt bin.


Ein Beispiel für Python-Code, der häufig Typhinweise verwendet, finden Sie hier .

erb
quelle
2
@rickfoosusa: Ich vermute, Sie führen nicht Python 3 aus, in dem die Funktion hinzugefügt wurde.
Erb
26
Warte eine Minute! Wenn das Definieren von Parameter und Rückgabetyp nicht a auslöst TypeError, wozu dient dann die pick(l: list, index: int) -> intDefinition einer Zeile? Oder ich habe es falsch verstanden, ich weiß es nicht.
Erdin Eray
24
@Eray Erdin: Das ist ein weit verbreitetes Missverständnis und überhaupt keine schlechte Frage. Es kann zu Dokumentationszwecken verwendet werden, hilft IDEs dabei, die automatische Vervollständigung zu verbessern und Fehler vor der Laufzeit mithilfe statischer Analysen zu finden (genau wie mypy, das ich in der Antwort erwähnt habe). Es besteht die Hoffnung, dass die Laufzeit die Informationen nutzen und Programme tatsächlich beschleunigen könnte, aber die Implementierung wird wahrscheinlich sehr lange dauern. Möglicherweise können Sie auch einen Dekorator erstellen, der die TypeErrors für Sie auslöst (die Informationen werden im __annotations__Attribut des Funktionsobjekts gespeichert ).
Erb
2
@ErdinEray Ich sollte hinzufügen, dass das Werfen von TypeErrors eine schlechte Idee ist (Debuggen macht nie Spaß, egal wie gut beabsichtigte Ausnahmen ausgelöst werden). Aber keine Angst, der Vorteil der in meiner Antwort beschriebenen neuen Funktionen ermöglicht einen besseren Weg: Verlassen Sie sich nicht auf eine Überprüfung zur Laufzeit, erledigen Sie alles vor der Laufzeit mit mypy oder verwenden Sie einen Editor, der die statische Analyse für Sie durchführt, wie z. B. PyCharm .
Erb
2
@ Tony: Wenn Sie zwei oder mehr Objekte zurückgeben, geben Sie tatsächlich ein Tupel zurück, daher sollten Sie die Annotation vom Typ Tupel verwenden, dhdef f(a) -> Tuple[int, int]:
erb
14

Sie geben keinen Typ an. Die Methode schlägt (zur Laufzeit) nur fehl, wenn versucht wird, auf Attribute zuzugreifen, die in den übergebenen Parametern nicht definiert sind.

Also diese einfache Funktion:

def no_op(param1, param2):
    pass

... wird nicht scheitern, egal welche zwei Argumente übergeben werden.

Diese Funktion:

def call_quack(param1, param2):
    param1.quack()
    param2.quack()

... wird zur Laufzeit fehlschlagen , wenn param1und param2nicht beide mit dem Namen aufrufbar Attributen quack.

TM.
quelle
+1: Die Attribute und Methoden werden nicht statisch bestimmt. Das Konzept, wie dieser "richtige Typ" oder "falsche Typ" aussehen würde, hängt davon ab, ob der Typ in der Funktion ordnungsgemäß funktioniert oder nicht.
S.Lott
11

Viele Sprachen haben Variablen, die von einem bestimmten Typ sind und einen Wert haben. Python hat keine Variablen; Es enthält Objekte, und Sie verwenden Namen, um auf diese Objekte zu verweisen.

In anderen Sprachen, wenn Sie sagen:

a = 1

dann ändert eine (normalerweise ganzzahlige) Variable ihren Inhalt auf den Wert 1.

In Python

a = 1

bedeutet "Verwenden Sie den Namen a , um auf das Objekt 1 zu verweisen ". In einer interaktiven Python-Sitzung können Sie Folgendes tun:

>>> type(1)
<type 'int'>

Die Funktion typewird mit dem Objekt aufgerufen 1; Da jedes Objekt seinen Typ kennt, ist es einfach type, diesen Typ herauszufinden und zurückzugeben.

Ebenso, wann immer Sie eine Funktion definieren

def funcname(param1, param2):

die Funktion erhält zwei Objekte, und nennt sie param1und param2, unabhängig von ihrer Art. Wenn Sie sicherstellen möchten, dass die empfangenen Objekte von einem bestimmten Typ sind, codieren Sie Ihre Funktion so, als ob sie vom erforderlichen Typ sind, und fangen Sie die Ausnahmen ab, die ausgelöst werden, wenn dies nicht der Fall ist. Die ausgelösten Ausnahmen sind normalerweise TypeError(Sie haben eine ungültige Operation verwendet) und AttributeError(Sie haben versucht, auf ein nicht vorhandenes Mitglied zuzugreifen (Methoden sind auch Mitglieder)).

tzot
quelle
8

Python ist nicht stark typisiert im Sinne einer statischen Typprüfung oder einer Typprüfung zur Kompilierungszeit.

Der meiste Python-Code fällt unter die sogenannte "Ententypisierung" - Sie suchen beispielsweise nach einer Methode readfür ein Objekt - es ist Ihnen egal, ob das Objekt eine Datei auf der Festplatte oder ein Socket ist, Sie möchten nur N lesen Bytes davon.

Mark Rushakoff
quelle
21
Python ist stark typisiert. Es wird auch dynamisch eingegeben.
Daniel Newby
1
Dies beantwortet jedoch keine der Fragen: "Woher weiß Python jedoch, dass der Benutzer der Funktion die richtigen Typen übergibt? Wird das Programm nur sterben, wenn es der falsche Typ ist, vorausgesetzt, die Funktion verwendet tatsächlich den Parameter?" Müssen Sie den Typ angeben? " oder ..
qPCR4vir
6

Wie Alex Martelli erklärt ,

Die normale, pythonische, bevorzugte Lösung ist fast immer "Ententypisierung": Versuchen Sie, das Argument so zu verwenden, als ob es von einem bestimmten gewünschten Typ wäre, und führen Sie es in einer try / exception-Anweisung aus, wobei alle Ausnahmen erfasst werden, die auftreten könnten, wenn das Argument nicht tatsächlich vorhanden wäre von diesem Typ (oder einem anderen Typ, der es gut nachahmt ;-), und versuchen Sie in der Ausnahmeklausel etwas anderes (mit dem Argument "als ob" es von einem anderen Typ wäre).

Lesen Sie den Rest seines Beitrags für hilfreiche Informationen.

Nick Presta
quelle
5

Python ist es egal, was Sie an seine Funktionen übergeben. Wenn Sie aufrufen my_func(a,b), enthalten die Variablen param1 und param2 die Werte von a und b. Python weiß nicht, dass Sie die Funktion mit den richtigen Typen aufrufen, und erwartet, dass der Programmierer sich darum kümmert. Wenn Ihre Funktion mit verschiedenen Parametertypen aufgerufen wird, können Sie Code, der auf sie zugreift, mit Try / Except-Blöcken umschließen und die Parameter nach Ihren Wünschen auswerten.

Kyle
quelle
11
Python hat keine Variablen wie andere Sprachen, in denen Variablen einen Typ und einen Wert haben. Es hat Namen, die auf Objekte verweisen , die ihren Typ kennen.
Zot
2

Sie geben niemals den Typ an; Python hat das Konzept der Ententypisierung ; Grundsätzlich wird der Code, der die Parameter verarbeitet, bestimmte Annahmen über sie treffen - möglicherweise durch Aufrufen bestimmter Methoden, deren Implementierung von einem Parameter erwartet wird. Wenn der Parameter vom falschen Typ ist, wird eine Ausnahme ausgelöst.

Im Allgemeinen liegt es an Ihrem Code, sicherzustellen, dass Sie Objekte des richtigen Typs weitergeben - es gibt keinen Compiler, der dies im Voraus erzwingt.

Justin Ethier
quelle
2

Es gibt eine berüchtigte Ausnahme von der Ententypisierung, die auf dieser Seite erwähnenswert ist.

Wenn eine strFunktion die __str__Klassenmethode aufruft, überprüft sie subtil ihren Typ:

>>> class A(object):
...     def __str__(self):
...         return 'a','b'
...
>>> a = A()
>>> print a.__str__()
('a', 'b')
>>> print str(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type tuple)

Als ob Guido uns andeutet, welche Ausnahme ein Programm auslösen soll, wenn es auf einen unerwarteten Typ stößt.

Antony Hatchkins
quelle
1

In Python hat alles einen Typ. Eine Python-Funktion führt alles aus, was sie tun soll, wenn die Art der Argumente dies unterstützt.

Beispiel: foofügt alles hinzu, was bearbeitet werden kann __add__;) ohne sich um seinen Typ zu kümmern. Das heißt, um Fehler zu vermeiden, sollten Sie nur die Dinge bereitstellen, die das Hinzufügen unterstützen.

def foo(a,b):
    return a + b

class Bar(object):
    pass

class Zoo(object):
    def __add__(self, other):
        return 'zoom'

if __name__=='__main__':
    print foo(1, 2)
    print foo('james', 'bond')
    print foo(Zoo(), Zoo())
    print foo(Bar(), Bar()) # Should fail
Pratik Deoghare
quelle
1

Ich habe dies in anderen Antworten nicht erwähnt, also werde ich es in den Topf geben.

Wie andere bereits gesagt haben, erzwingt Python keinen Typ für Funktions- oder Methodenparameter. Es wird davon ausgegangen, dass Sie wissen, was Sie tun, und dass Sie, wenn Sie wirklich wissen müssen, welche Art von etwas übergeben wurde, dies überprüfen und selbst entscheiden, was Sie tun möchten.

Eines der Hauptwerkzeuge hierfür ist die Funktion isinstance ().

Wenn ich zum Beispiel eine Methode schreibe, die erwartet, dass rohe binäre Textdaten anstelle der normalen utf-8-codierten Zeichenfolgen abgerufen werden, kann ich den Typ der Parameter auf dem Weg nach innen überprüfen und mich entweder an das anpassen, was ich finde, oder eine auslösen Ausnahme zu verweigern.

def process(data):
    if not isinstance(data, bytes) and not isinstance(data, bytearray):
        raise TypeError('Invalid type: data must be a byte string or bytearray, not %r' % type(data))
    # Do more stuff

Python bietet auch alle Arten von Werkzeugen, um in Objekte zu graben. Wenn Sie mutig sind, können Sie mit importlib sogar Ihre eigenen Objekte beliebiger Klassen im laufenden Betrieb erstellen. Ich habe dies getan, um Objekte aus JSON-Daten neu zu erstellen. So etwas wäre ein Albtraum in einer statischen Sprache wie C ++.

Fürchte Quixadhal
quelle
1

Um das Typisierungsmodul (neu in Python 3.5) effektiv zu nutzen, schließen Sie all ( *) ein.

from typing import *

Und Sie sind bereit zu verwenden:

List, Tuple, Set, Map - for list, tuple, set and map respectively.
Iterable - useful for generators.
Any - when it could be anything.
Union - when it could be anything within a specified set of types, as opposed to Any.
Optional - when it might be None. Shorthand for Union[T, None].
TypeVar - used with generics.
Callable - used primarily for functions, but could be used for other callables.

Doch noch können Sie Typnamen wie verwenden int, list, dict, ...

Prosti
quelle
1

Ich habe einen Wrapper implementiert, wenn jemand Variablentypen angeben möchte.

import functools

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for i in range(len(args)):
            v = args[i]
            v_name = list(func.__annotations__.keys())[i]
            v_type = list(func.__annotations__.values())[i]
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        v = result
        v_name = 'return'
        v_type = func.__annotations__['return']
        error_msg = 'Variable `' + str(v_name) + '` should be type ('
        error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
        if not isinstance(v, v_type):
                raise TypeError(error_msg)
        return result

    return check

Verwenden Sie es als:

@type_check
def test(name : str) -> float:
    return 3.0

@type_check
def test2(name : str) -> str:
    return 3.0

>> test('asd')
>> 3.0

>> test(42)
>> TypeError: Variable `name` should be type (<class 'str'>) but instead is type (<class 'int'>)

>> test2('asd')
>> TypeError: Variable `return` should be type (<class 'str'>) but instead is type (<class 'float'>)

BEARBEITEN

Der obige Code funktioniert nicht, wenn einer der Argumente (oder Rückgabetypen) nicht deklariert ist. Die folgende Bearbeitung kann helfen, funktioniert jedoch nur für kwargs und überprüft keine Argumente.

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for name, value in kwargs.items():
            v = value
            v_name = name
            if name not in func.__annotations__:
                continue

            v_type = func.__annotations__[name]

            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ') '
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        if 'return' in func.__annotations__:
            v = result
            v_name = 'return'
            v_type = func.__annotations__['return']
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                    raise TypeError(error_msg)
        return result

    return check
Gergely Papp
quelle