Wann sollte ich in Python eine Funktion anstelle einer Methode verwenden?

118

Das Zen von Python besagt, dass es nur einen Weg geben sollte, Dinge zu tun - dennoch stoße ich häufig auf das Problem, zu entscheiden, wann eine Funktion verwendet werden soll und wann eine Methode verwendet werden soll.

Nehmen wir ein triviales Beispiel - ein ChessBoard-Objekt. Nehmen wir an, wir brauchen einen Weg, um alle legalen King-Moves auf dem Brett verfügbar zu machen. Schreiben wir ChessBoard.get_king_moves () oder get_king_moves (chess_board)?

Hier sind einige verwandte Fragen, die ich mir angesehen habe:

Die Antworten, die ich erhielt, waren größtenteils nicht schlüssig:

Warum verwendet Python Methoden für einige Funktionen (z. B. list.index ()), aber Funktionen für andere (z. B. len (list))?

Der Hauptgrund ist die Geschichte. Funktionen wurden für Operationen verwendet, die für eine Gruppe von Typen generisch waren und die auch für Objekte funktionieren sollten, die überhaupt keine Methoden hatten (z. B. Tupel). Es ist auch praktisch, eine Funktion zu haben, die leicht auf eine amorphe Sammlung von Objekten angewendet werden kann, wenn Sie die Funktionsmerkmale von Python verwenden (map (), apply () et al.).

Tatsächlich ist die Implementierung von len (), max (), min () als integrierte Funktion weniger Code als die Implementierung als Methoden für jeden Typ. Man kann über Einzelfälle streiten, aber es ist ein Teil von Python, und es ist zu spät, um jetzt solche grundlegenden Änderungen vorzunehmen. Die Funktionen müssen erhalten bleiben, um einen massiven Codebruch zu vermeiden.

Das oben Gesagte ist zwar interessant, sagt aber nicht viel darüber aus, welche Strategie zu verfolgen ist.

Dies ist einer der Gründe - bei benutzerdefinierten Methoden können Entwickler einen anderen Methodennamen auswählen, z. B. getLength (), length (), getlength () oder was auch immer. Python erzwingt eine strikte Benennung, damit die allgemeine Funktion len () verwendet werden kann.

Etwas interessanter. Ich gehe davon aus, dass Funktionen in gewisser Weise die pythonische Version von Schnittstellen sind.

Zuletzt von Guido selbst :

Als ich über die Fähigkeiten / Schnittstellen sprach, dachte ich über einige unserer speziellen Methodennamen nach. In der Sprachreferenz heißt es: "Eine Klasse kann bestimmte Operationen implementieren, die durch spezielle Syntax aufgerufen werden (z. B. arithmetische Operationen oder Subskription und Slicing), indem Methoden mit speziellen Namen definiert werden." Es gibt jedoch alle diese Methoden mit speziellen Namen wie __len__oder, __unicode__die eher zum Nutzen integrierter Funktionen als zur Unterstützung der Syntax bereitgestellt werden. Vermutlich in einem schnittstellenbasierten Python würden diese Methoden zu regelmäßig benannten Methoden in einem ABC werden, so dass __len__dies werden würde

class container:
  ...
  def len(self):
    raise NotImplemented

Wenn ich noch etwas darüber nachdenke, verstehe ich nicht, warum alle syntaktischen Operationen nicht einfach die entsprechende normal benannte Methode für ein bestimmtes ABC aufrufen würden. " <" würde zum Beispiel vermutlich " object.lessthan" (oder vielleicht " comparable.lessthan") aufrufen . Ein weiterer Vorteil wäre die Möglichkeit, Python von dieser seltsamen Seltsamkeit abzusetzen, die mir als HCI-Verbesserung erscheint .

Hm. Ich bin mir nicht sicher, ob ich damit einverstanden bin (Abbildung :-).

Es gibt zwei Teile der "Python-Begründung", die ich zuerst erläutern möchte.

Zunächst habe ich aus HCI-Gründen len (x) gegenüber x.len () gewählt ( def __len__()kam viel später). Es gibt zwei miteinander verflochtene Gründe, beide HCI:

(a) Bei einigen Operationen liest sich die Präfixnotation einfach besser als die Postfix - Präfix (und Infix!) - Operationen haben in der Mathematik eine lange Tradition. Sie mögen Notationen, bei denen die Grafik dem Mathematiker hilft, über ein Problem nachzudenken. Vergleichen Sie die einfach , mit dem wir eine Formel wie umschreiben x*(a+b)in x*a + x*bauf die Ungeschicklichkeit der die gleiche Sache mit einer rohen OO - Notation.

(b) Wenn ich Code lese, der besagt , dass len(x)ich weiß, dass er nach der Länge von etwas fragt. Dies sagt mir zwei Dinge: Das Ergebnis ist eine ganze Zahl, und das Argument ist eine Art Container. Im Gegenteil, wenn ich lese x.len(), muss ich bereits wissen, dass xes sich um eine Art Container handelt, der eine Schnittstelle implementiert oder von einer Klasse mit einem Standard erbt len(). Erleben Sie die Verwirrung, die wir gelegentlich haben, wenn eine Klasse, die kein Mapping implementiert, eine get()oder keys() -Methode hat oder etwas, das keine Datei ist, eine write()Methode hat.

Wenn ich dasselbe auf andere Weise sage, sehe ich 'len' als eine eingebaute Operation . Ich würde es hassen, das zu verlieren. Ich kann nicht sicher sagen, ob Sie das gemeint haben oder nicht, aber 'def len (self): ...' klingt sicherlich so, als ob Sie es auf eine gewöhnliche Methode herabstufen möchten. Da bin ich stark -1.

Das zweite Stück Python-Begründung, das ich zu erklären versprochen habe, ist der Grund, warum ich spezielle Methoden gewählt habe, um zu schauen __special__und nicht nur special. Ich hatte viele Operationen erwartet, die Klassen möglicherweise überschreiben möchten, einige Standardoperationen (z. B. __add__oder __getitem__), andere nicht so Standardoperationen (z. B. Pickles hatten __reduce__lange Zeit überhaupt keine Unterstützung in C-Code). Ich wollte nicht, dass diese speziellen Operationen gewöhnliche Methodennamen verwenden, da dann bereits vorhandene Klassen oder Klassen, die von Benutzern ohne enzyklopädischen Speicher für alle speziellen Methoden geschrieben wurden, versehentlich Operationen definieren würden, die sie nicht implementieren wollten mit möglicherweise katastrophalen Folgen. Ivan Krstić erklärte dies in seiner Nachricht, die eintraf, nachdem ich das alles aufgeschrieben hatte.

- - Guido van Rossum (Homepage: http://www.python.org/~guido/ )

Mein Verständnis davon ist, dass in bestimmten Fällen die Präfixnotation einfach sinnvoller ist (dh Duck.quack ist aus sprachlicher Sicht sinnvoller als Quacksalber (Duck)). Auch hier ermöglichen die Funktionen "Schnittstellen".

In einem solchen Fall würde ich raten, get_king_moves nur basierend auf Guidos erstem Punkt zu implementieren. Es bleiben jedoch noch viele offene Fragen bezüglich der Implementierung einer Stack- und Queue-Klasse mit ähnlichen Push- und Pop-Methoden - sollten es Funktionen oder Methoden sein? (hier würde ich Funktionen erraten, weil ich wirklich eine Push-Pop-Schnittstelle signalisieren möchte)

TLDR: Kann jemand erklären, wie die Strategie für die Entscheidung, wann Funktionen oder Methoden verwendet werden sollen, aussehen sollte?

Ceasar Bautista
quelle
2
Meh, ich habe das immer als völlig willkürlich angesehen. Die Eingabe von Enten ermöglicht implizite "Schnittstellen", es macht keinen großen Unterschied, ob Sie X.froboder X.__frob__und freistehend haben frob.
Cat Plus Plus
2
Obwohl ich Ihnen größtenteils zustimme, ist Ihre Antwort im Prinzip nicht pythonisch. Erinnern Sie sich: "Lehnen Sie angesichts der Zweideutigkeit die Versuchung ab, zu raten." (Natürlich würden Fristen dies ändern, aber ich mache dies zum Spaß / zur Selbstverbesserung.)
Ceasar Bautista
Dies ist eine Sache, die ich an Python nicht mag. Ich denke, wenn Sie die Eingabe von Cast wie ein Int in eine Zeichenfolge erzwingen möchten, machen Sie es einfach zu einer Methode. Es ist ärgerlich, es in Parens und zeitaufwändig einschließen zu müssen.
Matt
1
Dies ist der wichtigste Grund, warum ich Python nicht mag: Sie wissen nie, ob Sie nach einer Funktion oder einer Methode suchen müssen, um etwas zu erreichen. Und es wird noch komplizierter, wenn Sie zusätzliche Bibliotheken mit neuen Datentypen wie Vektoren oder Datenrahmen verwenden.
vonjd
1
"Das Zen von Python besagt, dass es nur einen Weg geben sollte, Dinge zu tun", außer es ist nicht so.
Gented

Antworten:

84

Meine allgemeine Regel lautet: Wird die Operation am Objekt oder vom Objekt ausgeführt?

Wenn dies vom Objekt ausgeführt wird, sollte es sich um eine Mitgliedsoperation handeln. Wenn es auch auf andere Dinge zutreffen könnte oder von etwas anderem für das Objekt ausgeführt wird, sollte es eine Funktion (oder vielleicht ein Mitglied von etwas anderem) sein.

Bei der Einführung der Programmierung ist es traditionell (wenn auch nicht korrekt implementiert), Objekte in Bezug auf reale Objekte wie Autos zu beschreiben. Sie erwähnen eine Ente, also lassen Sie uns damit weitermachen.

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

Im Kontext der Analogie "Objekte sind reale Dinge" ist es "richtig", eine Klassenmethode für alles hinzuzufügen, was das Objekt tun kann. Sagen wir also, ich möchte eine Ente töten. Füge ich der Ente einen .kill () hinzu? Nein ... soweit ich weiß, begehen Tiere keinen Selbstmord. Wenn ich also eine Ente töten möchte, sollte ich Folgendes tun:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

Wenn wir uns von dieser Analogie entfernen, warum verwenden wir Methoden und Klassen? Weil wir Daten enthalten und unseren Code hoffentlich so strukturieren möchten, dass er in Zukunft wiederverwendbar und erweiterbar ist. Dies bringt uns zu dem Begriff der Kapselung, der dem OO-Design so am Herzen liegt.

Das Kapselungsprinzip ist wirklich das, worauf es ankommt: Als Designer sollten Sie alles über die Implementierung und die Interna der Klasse verbergen, auf die kein Benutzer oder anderer Entwickler unbedingt zugreifen muss. Da es sich um Klasseninstanzen handelt, reduziert sich dies auf "welche Operationen für diese Instanz entscheidend sind ". Wenn eine Operation nicht instanzspezifisch ist, sollte sie keine Mitgliedsfunktion sein.

TL; DR : Was @Bryan gesagt hat. Wenn es auf einer Instanz ausgeführt wird und auf Daten zugreifen muss, die sich innerhalb der Klasseninstanz befinden, sollte es eine Mitgliedsfunktion sein.

arrdem
quelle
Kurz gesagt, Nicht-Member-Funktionen arbeiten mit unveränderlichen Objekten, veränderbare Objekte verwenden Member-Funktionen? (Oder ist dies eine zu strenge Verallgemeinerung? Dies gilt für bestimmte Werke nur, weil unveränderliche Typen keinen Zustand haben.)
Ceasar Bautista
1
Unter strengen OOP-Gesichtspunkten halte ich das für fair. Da Python sowohl öffentliche als auch private Variablen hat (Variablen mit Namen, die mit __ beginnen) und im Gegensatz zu Java keinen garantierten Zugriffsschutz bietet, gibt es keine absoluten Werte, nur weil wir über eine zulässige Sprache diskutieren. Denken Sie jedoch in einer weniger freizügigen Sprache wie Java daran, dass die Funktionen getFoo () und setFoo () die Norm sind, sodass Unveränderlichkeit nicht absolut ist. Der Client-Code darf einfach keine Zuweisungen an Mitglieder vornehmen.
Arrdem
1
@Ceasar Das stimmt nicht. Unveränderliche Objekte haben Zustand; Andernfalls gibt es nichts, was eine ganze Zahl von einer anderen unterscheiden könnte. Unveränderliche Objekte ändern ihren Zustand nicht. Dies macht es im Allgemeinen für alle ihre Staaten weniger problematisch, öffentlich zu sein. In dieser Umgebung ist es viel einfacher, ein unveränderliches, vollständig öffentliches Objekt mit Funktionen sinnvoll zu manipulieren. Es gibt kein "Privileg", eine Methode zu sein.
Ben
1
@CeasarBautista Ja, Ben hat einen Punkt. Es gibt drei "große" Schulen für Code-Design: 1) nicht entworfen, 2) OOP und 3) funktional. In einem funktionalen Stil gibt es überhaupt keine Zustände. Auf diese Weise sehe ich, dass der meiste Python-Code entworfen wurde. Er nimmt Eingaben auf und generiert Ausgaben mit wenigen Nebenwirkungen. Der Punkt von OOP ist, dass alles einen Zustand hat. Klassen sind ein Container für Status, und "Member" -Funktionen sind daher zustandsabhängiger und auf Nebeneffekten basierender Code, der bei jedem Aufruf den in der Klasse definierten Status lädt. Python tendiert dazu, funktional zu sein, daher wird Code bevorzugt, der kein Mitglied ist.
Arrdem
1
essen (Selbst), Mist (Selbst), sterben (Selbst). hahahaha
Wapiti
27

Verwenden Sie eine Klasse, wenn Sie:

1) Isolieren Sie den aufrufenden Code von den Implementierungsdetails - nutzen Sie die Vorteile von Abstraktion und Kapselung .

2) Wenn Sie andere Objekte ersetzen möchten - nutzen Sie den Polymorphismus .

3) Wenn Sie Code für ähnliche Objekte wiederverwenden möchten - nutzen Sie die Vererbung .

Verwenden Sie eine Funktion für Aufrufe, die für viele verschiedene Objekttypen sinnvoll sind. Beispielsweise gelten die integrierten Funktionen len und repr für viele Arten von Objekten.

Davon abgesehen hängt die Wahl manchmal vom Geschmack ab. Überlegen Sie, was für typische Anrufe am bequemsten und lesbarsten ist. Was wäre zum Beispiel besser (x.sin()**2 + y.cos()**2).sqrt()oder sqrt(sin(x)**2 + cos(y)**2)?

Raymond Hettinger
quelle
8

Hier ist eine einfache Faustregel: Wenn der Code auf eine einzelne Instanz eines Objekts einwirkt, verwenden Sie eine Methode. Noch besser: Verwenden Sie eine Methode, es sei denn, es gibt einen zwingenden Grund, sie als Funktion zu schreiben.

In Ihrem speziellen Beispiel soll es so aussehen:

chessboard = Chessboard()
...
chessboard.get_king_moves()

Überlege es dir nicht. Verwenden Sie immer Methoden, bis der Punkt erreicht ist, an dem Sie sich sagen: "Es ist nicht sinnvoll, dies zu einer Methode zu machen." In diesem Fall können Sie eine Funktion erstellen.

Bryan Oakley
quelle
2
Können Sie erklären, warum Sie standardmäßig Methoden über Funktionen verwenden? (Und könnten Sie erklären, ob diese Regel im Fall der Stack- und Queue / Pop- und Push-Methoden noch Sinn macht?)
Ceasar Bautista
11
Diese Faustregel macht keinen Sinn. Die Standardbibliothek selbst kann eine Anleitung zur Verwendung von Klassen im Vergleich zu Funktionen sein. heapq und math sind Funktionen, weil sie mit normalen Python-Objekten (Floats und Listen) arbeiten und weil sie keinen komplexen Zustand wie das Zufallsmodul beibehalten müssen.
Raymond Hettinger
1
Wollen Sie damit sagen, dass die Regel keinen Sinn ergibt, weil die Standardbibliothek sie verletzt? Ich halte diese Schlussfolgerung nicht für sinnvoll. Zum einen sind Faustregeln genau das - sie sind keine absoluten Regeln. Plus, ein großer Teil der Standardbibliothek hat diese Daumenregel folgen. Das OP suchte nach einfachen Richtlinien, und ich denke, mein Rat ist perfekt für jemanden, der gerade erst anfängt. Ich schätze jedoch Ihren Standpunkt.
Bryan Oakley
Nun, die STL hat einige gute Gründe dafür. Für Mathe und Co. ist es offensichtlich nutzlos, eine Klasse zu haben, da wir keinen Status dafür haben (in anderen Sprachen sind dies Klassen mit nur statischen Funktionen). Für Dinge, die mit mehreren verschiedenen Containern arbeiten, wie zum Beispiel, len()kann man auch argumentieren, dass das Design sinnvoll ist, obwohl ich persönlich denke, dass Funktionen dort auch nicht schlecht wären - wir hätten nur eine andere Konvention, die len()immer eine zurückgeben muss Ganzzahl (aber mit all den zusätzlichen Problemen von Backcomp würde ich das auch für Python nicht befürworten)
Voo
-1, da diese Antwort völlig willkürlich ist: Sie versuchen im Wesentlichen zu demonstrieren, dass X besser als Y ist, indem Sie annehmen, dass X besser als Y ist.
Gented
7

Normalerweise denke ich an ein Objekt wie eine Person.

Attribute sind der Name, die Größe, die Schuhgröße usw. der Person.

Methoden und Funktionen sind Operationen, die die Person ausführen kann.

Wenn die Operation von nur einer alten Person durchgeführt werden kann, ohne dass etwas Einzigartiges für diese eine bestimmte Person erforderlich ist (und ohne etwas an dieser einen bestimmten Person zu ändern), dann ist dies eine Funktion und sollte als solche geschrieben werden.

Wenn eine Operation auf die Person einwirkt (z. B. Essen, Gehen, ...) oder etwas Einzigartiges für diese Person erfordert (wie Tanzen, Schreiben eines Buches, ...), sollte dies eine Methode sein .

Natürlich ist es nicht immer trivial, dies in das spezifische Objekt zu übersetzen, mit dem Sie arbeiten, aber ich finde, es ist eine gute Möglichkeit, darüber nachzudenken.

Gedeiht
quelle
Aber Sie können die Größe eines alten Menschen messen, also sollte es nach dieser Logik sein height(person), nicht person.height?
Endolith
@endolith Sicher, aber ich würde sagen, dass die Höhe als Attribut besser geeignet ist, da Sie keine ausgefallenen Arbeiten ausführen müssen, um sie abzurufen. Das Schreiben einer Funktion zum Abrufen einer Nummer scheint ein unnötiger Rahmen zum Durchspringen zu sein.
Gedeiht
@endolith Wenn es sich bei der Person hingegen um ein Kind handelt, das wächst und seine Größe ändert, ist es naheliegend, dass sich eine Methode darum kümmert, keine Funktion.
Gedeiht
Dies gilt nicht. Was ist, wenn Sie haben are_married(person1, person2)? Diese Abfrage ist sehr allgemein und sollte daher eine Funktion und keine Methode sein.
Pithikos
@Pithikos Sicher, aber das beinhaltet auch mehr als nur ein Attribut oder eine Aktion, die an einem Objekt (einer Person) ausgeführt wird, sondern vielmehr eine Beziehung zwischen zwei Objekten.
Thriveth
4

Im Allgemeinen verwende ich Klassen, um eine logische Reihe von Funktionen für eine Sache zu implementieren , so dass ich im Rest meines Programms über die Sache nachdenken kann , ohne mich um all die kleinen Bedenken kümmern zu müssen, die ihre Implementierung ausmachen.

Alles, was Teil dieser Kernabstraktion von "Was man mit einer Sache machen kann " ist, sollte normalerweise eine Methode sein. Dies schließt im Allgemeinen alles ein, was eine Sache verändern kann , da der interne Datenstatus normalerweise als privat betrachtet wird und nicht Teil der logischen Idee ist, "was man mit einer Sache machen kann ".

Wenn Sie zu Operationen auf höherer Ebene kommen, insbesondere wenn sie mehrere Dinge beinhalten , werden sie normalerweise am natürlichsten als Funktionen ausgedrückt, wenn sie aus der öffentlichen Abstraktion einer Sache aufgebaut werden können, ohne dass ein besonderer Zugriff auf die Interna erforderlich ist (es sei denn, sie) re Methoden eines anderen Objekts). Dies hat den großen Vorteil, dass ich, wenn ich mich entscheide, die Interna der Funktionsweise meines Dings vollständig neu zu schreiben (ohne die Benutzeroberfläche zu ändern), nur einen kleinen Kernsatz von Methoden zum Umschreiben habe und dann alle externen Funktionen, die in Bezug auf diese Methoden geschrieben wurden wird einfach funktionieren. Ich finde, dass das Beharren darauf, dass alle Operationen, die mit Klasse X zu tun haben, Methoden für Klasse X sind, zu überkomplizierten Klassen führt.

Es hängt jedoch vom Code ab, den ich schreibe. Für einige Programme modelliere ich sie als eine Sammlung von Objekten, deren Interaktionen das Verhalten des Programms hervorrufen. Hier ist die wichtigste Funktionalität eng an ein einzelnes Objekt gekoppelt und wird daher in Methoden mit einer Streuung von Nutzfunktionen implementiert. Für andere Programme ist das Wichtigste eine Reihe von Funktionen, die Daten manipulieren, und Klassen werden nur verwendet, um die natürlichen "Ententypen" zu implementieren, die von den Funktionen manipuliert werden.

Ben
quelle
0

Sie können sagen, "lehnen Sie angesichts der Zweideutigkeit die Versuchung ab, zu raten".

Es ist jedoch nicht einmal eine Vermutung. Sie sind sich absolut sicher, dass die Ergebnisse beider Ansätze insofern gleich sind, als sie Ihr Problem lösen.

Ich glaube, es ist nur eine gute Sache, mehrere Wege zu haben, um Ziele zu erreichen. Ich würde Ihnen demütig sagen, wie andere Benutzer es bereits getan haben, was auch immer "besser schmeckt" / sich in Bezug auf die Sprache intuitiver anfühlt .

João Silva
quelle