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:
- Warum verwendet Python "magische Methoden"?
- Gibt es einen Grund, warum Python-Strings keine String-Längenmethode haben?
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ürdeclass 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)
inx*a + x*b
auf 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 lesex.len()
, muss ich bereits wissen, dassx
es sich um eine Art Container handelt, der eine Schnittstelle implementiert oder von einer Klasse mit einem Standard erbtlen()
. Erleben Sie die Verwirrung, die wir gelegentlich haben, wenn eine Klasse, die kein Mapping implementiert, eineget()
oderkeys()
-Methode hat oder etwas, das keine Datei ist, einewrite()
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 nurspecial
. 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?
quelle
X.frob
oderX.__frob__
und freistehend habenfrob
.Antworten:
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.
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:
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.
quelle
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()
odersqrt(sin(x)**2 + cos(y)**2)
?quelle
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:
Ü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.
quelle
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, dielen()
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)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.
quelle
height(person)
, nichtperson.height
?are_married(person1, person2)
? Diese Abfrage ist sehr allgemein und sollte daher eine Funktion und keine Methode sein.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.
quelle
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 .
quelle