16. Dezember 2020

Bessere Python-Schnittstellen mit reinen Schlüsselwortargumenten entwerfen

Mitwirkende
Robbe Sneyders
Leitender ML-Ingenieur
Keine Artikel gefunden.
Newsletter abonnieren
Diesen Beitrag teilen

Nur-Schlüsselwort-Argumente sind eine meiner Lieblingsfunktionen, die in Python 3 eingeführt wurden. Aber obwohl sie seit der Veröffentlichung von Python 3 im Jahr 2008 zur Verfügung stehen, werden sie auch heute noch selten verwendet. Wahrscheinlich, weil ihr wahrer Wert nicht immer vollständig verstanden wird. Das ist auch nicht weiter verwunderlich, denn die einzige Begründung in PEP 3102, in dem die Funktion eingeführt wurde, besteht darin, mehr Flexibilität bei der Anordnung der Argumente zu bieten. Allerdings können reine Schlüsselwortargumente dabei helfen, bessere Schnittstellen zu entwerfen.

Positions- und Schlüsselwortargumente

Beginnen wir mit dem Aufbau der Szene und werfen wir einen Blick auf die beiden Arten von Argumenten, die Python in Funktionssignaturen unterstützt: Positions- und Schlüsselwortargumente. Wir werden sie anhand des copytree-Beispiels diskutieren, das wir im Laufe dieses Blogposts entwickeln werden. copytree kopiert rekursiv einen ganzen Verzeichnisbaum vom src- zum dest-Verzeichnis.

def copytree(src, dest):  

 ...

Wir können diese Funktion mit Positionsargumenten aufrufen, die implizit auf der Grundlage ihrer Position zugewiesen werden.

copytree('foo/', 'bar/')

Oder wir können sie mit Schlüsselwortargumenten aufrufen, die explizit auf der Grundlage ihrer jeweiligen Schlüsselwörter zugewiesen werden. Die Reihenfolge der Schlüsselwortargumente spielt keine Rolle und kann beliebig geändert werden.

copytree(src='foo/', dest='bar/)

Beide Funktionsaufrufe sind gleichwertig, und der Aufrufer kann entscheiden, welche Version er verwenden möchte. Die Verwendung von Positionsargumenten scheint hier am natürlichsten zu sein, da Schlüsselwörter überflüssig erscheinen können, wenn die Bedeutung der Argumente aus ihrer Position abgeleitet werden kann. Wenn jedoch zusätzliche Argumente hinzugefügt werden, ändert sich die Situation und die Verwendung von Positionsargumenten kann schnell unklar werden.

def copytree(src, dest, dirs_exist_ok, symlinks):    

...

copytree('foo/', 'bar/', True, False)

Ohne einen Blick in die Funktionsdefinition zu werfen, ist es unmöglich zu sagen, was die Argumente True und False hier bewirken. Ein besserer Weg, die Funktion aufzurufen, wäre:

copytree('foo/', 'bar/', dirs_exist_ok=True, symlinks=False)

Jeder, der sich diesen Funktionsaufruf ansieht, kann sofort erkennen, was er tut, ohne sich die Funktionsdefinition ansehen zu müssen.

Jedem Aufrufer steht es jedoch frei, diese Funktion mit Positionsargumenten aufzurufen, was zu Unklarheiten und Inkonsistenzen in der gesamten Codebasis führen kann. Nur-Schlüsselwort-Argumente können helfen, die korrekte Verwendung Ihrer Funktionen durchzusetzen.

Nur-Schlüsselwort-Argumente

Um die Syntax von Nur-Schlüsselwort-Argumenten zu verstehen, müssen wir zunächst einen kleinen Umweg über die "varargs"-Argumente machen.

def f(x, y, *args):  

 ...

In dieser Funktionssignatur wird *args als varargs-Argument bezeichnet. Es schluckt alle Positionsargumente, die zusätzlich zu x und y angegeben werden, und ermöglicht so eine dynamische Anzahl von Argumenten. Dies wird leicht verständlich, wenn man sich den folgenden Beispielaufruf und die daraus resultierenden Werte für jedes Argument ansieht.

f(1, 2, 3, 4, 5)

x = 1, y = 2, args = [3, 4, 5]

Seit Python 3 können wir zusätzliche Argumente nach dem varargs-Argument definieren. Da das varargs-Argument alle Positionsargumente schluckt, ist es unmöglich, diese zusätzlichen Argumente mit Positionswerten zu füllen.

def f(x, y, *args, flag):    

...

In diesem Fall muss das Argument flag daher per Schlüsselwort angegeben werden. Es ist ein reines Schlüsselwort-Argument.

Ein varargs-Argument sollten wir allerdings nicht verwenden. Wenn unsere Funktion nur eine begrenzte Anzahl von Argumenten erwartet, kann ein varargs-Argument fälschlicherweise angegebene Argumente verschlucken, was zu unerwartetem Verhalten der Funktion führen kann. Glücklicherweise können wir das Nur-Schlüsselwort-Verhalten beibehalten und das varargs-Argument weglassen.

def f(x, y, *, flag):  

 ...

Diese Funktion nimmt nur zwei Positionsargumente entgegen und verlangt, dass das Flag-Argument per Schlüsselwort angegeben wird. Wendet man dies auf unser vorheriges Beispiel an, ergibt sich die folgende Funktionsdefinition.

def copytree(src, dest, *, dirs_exist_ok, symlinks):  

 ...

Das erzwingt, dass der Aufrufer die Argumente dirs_exist_ok und symlinks per Schlüsselwort angibt.

copytree('foo/', 'bar/', dirs_exist_ok=True, symlinks=False)

Zufriedenstellend, nicht wahr?

Bonus: Argumente, die nur die Position betreffen

Die Verwendung von Schlüsselwortargumenten verhindert, dass wir Funktionen mit allen Positionsargumenten aufrufen können.

copytree('foo/', 'bar/', True, False)

Es ist aber immer noch möglich, die Funktion mit allen Schlüsselwortargumenten aufzurufen.

copytree(src='foo/', dest='bar/', dirs_exist_ok=True,

        symlinks=False)

Dies ist zwar weniger problematisch, aber ich würde immer noch argumentieren, dass dies weniger klar ist als eine Kombination aus Positions- und Schlüsselwortargumenten. Positionsargumente bieten implizit zusätzliche Klarheit darüber, wie sich die Funktion verhält. Sie beruhen auf der natürlichen Logik der Reihenfolge, die auf einen Blick zu verstehen ist. Sehen Sie sich nur dieses Beispiel an, in dem wir die Schlüsselwortargumente neu anordnen... und weinen.

copytree(dest='bar/', src='foo/', dirs_exist_ok=True,

        symlinks=False)

Zusätzlich zu dem Problem der Neuordnung können die Namen der src- und dest-Argumente nun unmöglich geändert werden, ohne den aufrufenden Code zu beschädigen, obwohl sie nie namentlich aufgerufen werden sollten.

Nehmen wir zum Beispiel an, dass wir das Argument dest in dst umbenennen wollen, um mit dem Namen des Arguments src konsistent zu sein. Diese Änderung würde jeden aufrufenden Code, der dest mit einem Schlüsselwortargument angibt, zerstören.

def copytree(src, dst, *, dirs_exist_ok, symlinks):

   ...

copytree(src='foo/', dest='bar/', dirs_exist_ok=True,

      symlinks=False)

TypeError: copytree() hat ein unerwartetes Schlüsselwortargument 'dest' erhalten

Dies kann vor allem bei öffentlichen Schnittstellen, z.B. in Bibliotheken, ein Problem darstellen. Deshalb wurden mit PEP 570 Nur-Positions-Argumente eingeführt, deren Verhalten gleichwertig, aber entgegengesetzt zu Nur-Schlüsselwort-Argumenten ist.

def copytree(src, dest, /, *, dirs_exist_ok, symlinks):

   ...

Hier sind alle Argumente links vom / nur Positionsargumente. Damit ist unsere Suche nach der perfekten Funktionsdefinition abgeschlossen, die nun nur noch mit aufgerufen werden kann:

copytree('foo/', 'bar/', dirs_exist_ok=True, symlinks=False)

Leider sind positionsbezogene Argumente erst ab Python 3.8 verfügbar. Wenn Sie also eine niedrigere Version verwenden, müssen Sie ohne sie auskommen. Sie sind zwar ein großartiges zusätzliches Werkzeug, um sauberere Schnittstellen zu erstellen, aber reine Schlüsselwortargumente sind bereits ein großer Fortschritt!

Gestaltung von Python-Schnittstellen

Mit diesen Werkzeugen in unserem Werkzeugkasten wollen wir uns nun ansehen, wie wir sie für die Gestaltung schöner Schnittstellen einsetzen können. Was folgt, ist eine Reihe von Leitlinien. Sie sind nicht allumfassend und es kann sein, dass Sie gelegentlich von ihnen abweichen möchten.

Zunächst einmal hängt die Bedeutung von Positionsargumenten von einem eindeutigen Funktionsnamen ab, den Sie also mit Bedacht wählen sollten. Gute Funktionssignaturen bestehen aus denselben Komponenten wie Sätze in natürlicher Sprache. Der Funktionsname ist das Verb, das die Aktion der Funktion beschreibt. Die positionalen Schlüsselwörter sind das Objekt oder Subjekt der Aktion. Und die Schlüsselwortargumente sind die Adjektive, die die Aktion der Funktion beschreiben und modifizieren.

Im Allgemeinen sind erforderliche Argumente gute Positionsargumente, während Schlüsselwortargumente meist optional sein sollten. Dies passt gut zu unserem Vergleich mit der natürlichen Sprache, wo ein Satz noch Sinn macht, wenn man die Adjektive weglässt, aber nicht mehr so sehr, wenn man das Objekt oder Subjekt weglässt.

Beachten Sie, dass dies meist nur in eine Richtung funktioniert. Während erforderliche Argumente fast immer positional sein sollten, müssen positionale Argumente nicht immer erforderlich sein. Insbesondere wenn klare Vorgaben gemacht werden können, können sie stattdessen optional gemacht werden. Sehen Sie sich nur einige der Funktionen im Python-Zeitmodul an. Beim Aufruf der Funktion gmtime beispielsweise ist das secs-Argument optional und wird standardmäßig auf None gesetzt, wenn es nicht explizit angegeben wird, was dazu führt, dass die aktuelle Zeit zurückgegeben wird.

time.gmtime([secs])

Für Funktionen, die viele Argumente benötigen, kann es eine Option sein, einige dieser erforderlichen Argumente in Schlüsselwortargumente umzuwandeln, wenn ihre Bedeutung nicht eindeutig durch ihre Position impliziert ist. Dies wird auch von pylint erkannt, das in diesem Fall keine Warnungen vor zu vielen Argumenten mehr ausgibt. Wenn Sie sich jedoch in dieser Situation befinden, ist es vielleicht besser zu prüfen, ob für einige der Argumente logische Standardwerte bereitgestellt werden können, die sie optional machen, oder ob der Code so umgestaltet werden kann, dass weniger Argumente benötigt werden.

Es gibt wahrscheinlich unendlich viele Fälle, in denen Sie von diesen Richtlinien abweichen wollen, was auch in Ordnung ist. Solange Sie wissen, warum es sinnvoll ist, davon abzuweichen, werden sie Ihnen helfen, klare Funktionssignaturen zu schreiben.

Fazit

Ich verwende und befürworte reine Schlüsselwortargumente in den Codebasen, mit denen ich zu tun habe, schon seit geraumer Zeit, und es freut mich zu sehen, dass viele meiner Kollegen ihre Nützlichkeit anerkennen und ihre Verwendung in ML6 zunimmt. Für alle anderen hoffe ich, dass dieser Blogpost auch Sie davon überzeugen konnte, dass Nur-Schlüsselwort-Argumente Ihnen helfen können, bessere Schnittstellen zu entwickeln.

Verwandte Beiträge

Alle anzeigen
Keine Ergebnisse gefunden.
Es gibt keine Ergebnisse mit diesen Kriterien. Versuchen Sie, Ihre Suche zu ändern.
Stiftung Modelle
Unternehmen
Unser Team
Verantwortungsvolle und ethische KI
Strukturierte Daten
Chat GPT
Nachhaltigkeit
Stimme und Ton
Front-End-Entwicklung
Schutz und Sicherheit von Daten
Verantwortungsvolle/ethische KI
Infrastruktur
Hardware und Sensoren
MLOps
Generative KI
Verarbeitung natürlicher Sprache
Computer Vision