Ist es eine gute Praxis, Instanzvariablen in einer Klasse in Python als None zu deklarieren?

68

Betrachten Sie die folgende Klasse:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Meine Kollegen neigen dazu, es so zu definieren:

class Person:
    name = None
    age = None

    def __init__(self, name, age):
        self.name = name
        self.age = age

Der Hauptgrund dafür ist, dass der Editor seiner Wahl die Eigenschaften für die automatische Vervollständigung anzeigt.

Persönlich mag ich letzteres nicht, weil es keinen Sinn macht, dass eine Klasse diese Eigenschaften hat None.

Welches wäre eine bessere Übung und aus welchen Gründen?

Remco Haszing
quelle
63
Lassen Sie Ihre IDE niemals bestimmen, welchen Code Sie schreiben?
Martijn Pieters
14
Übrigens: Wenn Sie eine geeignete Python-IDE (z. B. PyCharm) verwenden, werden die Attribute in der __init__bereits vorhandenen NoneIDE automatisch vervollständigt. Außerdem verhindert die Verwendung, dass die IDE einen besseren Typ für das Attribut herleitet möglich).
Bakuriu
Wenn es sich nur um die automatische Vervollständigung handelt, können Sie Tipping verwenden, und die zusätzlichen Dokumentzeichenfolgen sind ebenfalls ein Plus.
Bindestrich
3
"Lassen Sie Ihre IDE niemals bestimmen, welchen Code Sie schreiben" ist ein umstrittenes Thema. Ab Python 3.6 gibt es In-Line-Annotationen und ein typingModul, mit dem Sie Hinweise auf die IDE und Linter geben können, wenn Sie so etwas
cz
1
Diese Zuweisungen auf Klassenebene haben keine Auswirkungen auf den Rest des Codes. Sie haben keinen Einfluss auf self. Selbst wenn sie in der Instanz nicht angezeigt werden self.nameoder self.agenicht zugewiesen wurden , werden sie nur in der Klasse angezeigt . __init__selfPerson
Jolvi

Antworten:

70

Ich nenne die letztere schlechte Praxis unter der Regel "das tut nicht, was Sie denken, dass es tut".

Die Position Ihres Mitarbeiters kann wie folgt umgeschrieben werden: "Ich werde eine Reihe von klassenstatischen quasi-globalen Variablen erstellen, auf die nie zugegriffen wird, die jedoch Platz in den Namespace-Tabellen der verschiedenen Klassen belegen ( __dict__), damit meine IDE funktioniert etwas."

StarWeaver
quelle
4
Ein bisschen wie Docstrings dann ;-) Natürlich hat Python einen praktischen Modus, um diese zu entfernen -OO, für die wenigen, die es brauchen.
Steve Jessop
15
Der Platzbedarf ist mit Abstand der unwichtigste Grund, warum diese Konvention stinkt. Es ist ein Dutzend Bytes pro Name (pro Prozess). Ich habe das Gefühl, meine Zeit damit verschwendet zu haben, nur den Teil Ihrer Antwort zu lesen, der sich darauf bezieht. So unwichtig sind die Platzkosten.
8
@delnan Ich stimme zu, dass die Speichergröße der Einträge bedeutungslos ist. Ich habe mehr über den logischen / mentalen Raum nachgedacht, der für ein introspektives Debugging erforderlich ist, und dies erfordert vermutlich mehr Lesen und Sortieren. I ~ LL
StarWeaver
3
Wenn Sie so paranoid in Bezug auf den Weltraum wären, würden Sie feststellen, dass dies Platz spart und Zeit verschwendet. Die nicht initialisierten Member verwenden den Klassenwert, und daher gibt es keine Kopien des Variablennamens, der None zugeordnet ist (eine für jede Instanz). Die Kosten betragen in der Klasse also einige Bytes, und die Einsparungen betragen einige Bytes pro Instanz. Aber jede fehlgeschlagene Suche nach Variablennamen kostet ein wenig Zeit.
Jon Jay Obermark
2
Wenn Sie sich Sorgen um 8 Bytes machen würden, würden Sie Python nicht verwenden.
cz
26

1. Machen Sie Ihren Code leicht verständlich

Code wird viel häufiger gelesen als geschrieben. Erleichtern Sie die Arbeit Ihres Code-Betreuers (möglicherweise auch Sie selbst im nächsten Jahr).

Ich kenne keine strengen Regeln, aber ich bevorzuge es , wenn ein zukünftiger Instanzstatus eindeutig als eindeutig deklariert wird. Absturz mit einem AttributeErrorist schon schlimm genug. Es ist schlimmer, den Lebenszyklus eines Instanzattributs nicht klar zu erkennen. Die Menge an mentaler Gymnastik, die erforderlich ist, um mögliche Anrufsequenzen wiederherzustellen, die zur Zuweisung des Attributs führen, kann leicht nicht mehr trivial werden und zu Fehlern führen.

Daher definiere ich normalerweise nicht nur alles im Konstruktor, sondern bemühe mich auch, die Anzahl der veränderlichen Attribute auf ein Minimum zu beschränken.

2. Mischen Sie keine Mitglieder auf Klassen- und Instanzebene

Alles, was Sie direkt in der classDeklaration definieren, gehört zur Klasse und wird von allen Instanzen der Klasse gemeinsam genutzt. Wenn Sie beispielsweise eine Funktion innerhalb einer Klasse definieren, wird diese zu einer Methode, die für alle Instanzen gleich ist. Gleiches gilt für Datenmitglieder. Dies ist völlig anders als die Instanzattribute, in denen Sie normalerweise definieren __init__.

Datenelemente auf Klassenebene sind am nützlichsten als Konstanten:

class Missile(object):
  MAX_SPEED = 100  # all missiles accelerate up to this speed
  ACCELERATION = 5  # rate of acceleration per game frame

  def move(self):
    self.speed += self.ACCELERATION
    if self.speed > self.MAX_SPEED:
      self.speed = self.MAX_SPEED
    # ...
9000
quelle
2
Ja, aber das Mischen von Mitgliedern auf Klassenebene und auf Instanzebene ist so ziemlich das, was def tut. Es schafft Funktionen, die wir als Attribute von Objekten betrachten, die aber wirklich Mitglieder der Klasse sind. Das Gleiche gilt für das Eigentum und seine Art. Die Illusion zu vermitteln, dass Arbeit zu Objekten gehört, wenn sie wirklich von der Klasse vermittelt wird, ist eine altehrwürdige Methode, nicht verrückt zu werden. Wie kann es so schlimm sein, wenn es für Python selbst in Ordnung ist?
Jon Jay Obermark
Die Reihenfolge der Methodenauflösung in Python (gilt auch für Datenelemente) ist nicht sehr einfach. Was auf Instanzebene nicht gefunden wird, wird auf Klassenebene, dann unter Basisklassen usw. gesucht. Sie können ein Mitglied auf Klassenebene (Daten oder Methode) tatsächlich spiegeln, indem Sie ein Mitglied mit demselben Namen auf Instanzebene zuweisen. Methoden auf Instanzebene sind jedoch an Instanzen gebundenself und müssen nicht selfweitergegeben werden, während Methoden auf Klassenebene ungebunden und aus heutiger Sicht reine Funktionen sind defund eine Instanz als erstes Argument akzeptieren. Das sind also verschiedene Dinge.
9000,
Ich denke, ein AttributeErrorSignal ist ein gutes als ein Fehler. Andernfalls würden Sie None verschlucken und bedeutungslose Ergebnisse erhalten. Besonders wichtig in dem vorliegenden Fall, in dem die Attribute in definiert sind __init__, sodass ein fehlendes Attribut (das jedoch auf Klassenebene vorhanden ist) nur durch fehlerhafte Vererbung verursacht werden kann.
Davidmh
@Davidmh: Ein erkannter Fehler ist in der Tat immer besser als ein nicht erkannter Fehler! Ich würde sagen, wenn Sie ein Attribut erstellen müssen Noneund dieser Wert zum Zeitpunkt der Instanzkonstruktion keinen Sinn ergibt , haben Sie ein Problem in Ihrer Architektur und müssen den Lebenszyklus des Attributwerts oder seines Anfangswerts überdenken. Beachten Sie, dass Sie durch frühzeitiges Definieren Ihrer Attribute ein solches Problem erkennen können, noch bevor Sie den Rest der Klasse geschrieben haben, geschweige denn den Code ausgeführt haben.
9000,
Spaß! Raketen! Jedenfalls bin ich mir ziemlich sicher, dass es in Ordnung ist, Klassenstufen-
Vars zu erstellen
18

Persönlich definiere ich die Mitglieder in der __ init __ () -Methode. Ich habe nie darüber nachgedacht, sie im Klassenteil zu definieren. Aber was ich immer mache: Ich initialisiere alle Member in der __ init__ -Methode, auch die, die in der __ init__ -Methode nicht benötigt werden.

Beispiel:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        self._selected = None

   def setSelected(self, value):
        self._selected = value

Ich halte es für wichtig, alle Mitglieder an einem Ort zu definieren. Dadurch wird der Code besser lesbar. Es ist nicht so wichtig, ob es innerhalb von __ init __ () oder außerhalb von __ ist. Für ein Team ist es jedoch wichtig, sich zu mehr oder weniger demselben Codierungsstil zu verpflichten.

Oh, und Sie werden vielleicht bemerken, dass ich Mitgliedervariablen jemals das Präfix "_" hinzufüge.

das.ich
quelle
13
Sie sollten alle Werte im Konstruktor festlegen. Wenn der Wert in der Klasse selbst festgelegt wurde, wird er zwischen Instanzen geteilt, was für None, aber nicht für die meisten Werte in Ordnung ist. Also nichts daran ändern. ;)
Remco Haszing
4
Standardwerte für Parameter und die Möglichkeit, beliebige Positions- und Schlüsselwortargumente zu verwenden, machen es weniger schmerzhaft als erwartet. (Und es ist tatsächlich möglich, die Konstruktion an andere Methoden oder sogar an eigenständige Funktionen zu delegieren. Wenn Sie also wirklich mehrere Aufgaben benötigen, können Sie dies auch tun.)
Sean Vieira
6
@ Benedict Python hat keine Zugriffskontrolle. Der führende Unterstrich ist die akzeptierte Konvention für Implementierungsdetails. Siehe PEP 8 .
Doval
3
@Doval Es gibt einen außergewöhnlich guten Grund, Attributnamen mit einem Präfix zu versehen _: Um anzuzeigen, dass es privat ist! (Warum verwechseln so viele Leute in diesem Thread Python mit anderen Sprachen?)
1
Ah, also sind Sie eine dieser Eigenschaften oder Funktionen für alle exponierten Interaktionspersonen ... Entschuldigen Sie das Missverständnis. Dieser Stil scheint mir immer übertrieben, aber er hat eine Anhängerschaft.
Jon Jay Obermark
11

Dies ist eine schlechte Praxis. Sie brauchen diese Werte nicht, sie verstopfen den Code und können Fehler verursachen.

Erwägen:

>>> class WithNone:
...   x = None
...   y = None
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> class InitOnly:
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> wn = WithNone(1,2)
>>> wn.x
1
>>> WithNone.x #Note that it returns none, no error
>>> io = InitOnly(1,2)
>>> InitOnly.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class InitOnly has no attribute 'x'
Daenyth
quelle
Es würde mir schwer fallen, das als "Fehlerursache" zu bezeichnen. Es ist nicht eindeutig, was Sie hier mit 'x' meinen, aber Sie möchten möglicherweise den wahrscheinlichsten Anfangswert.
Jon Jay Obermark
Ich hätte herausfinden sollen, dass es bei falscher Verwendung zu Fehlern kommen kann.
Daenyth
Das höhere Risiko besteht weiterhin darin, ein Objekt zu initialisieren. Keiner ist wirklich ziemlich sicher. Der einzige Fehler, den es verursachen wird, ist, wirklich lahme Ausnahmen zu schlucken.
Jon Jay Obermark
1
Die Nichtverursachung von Fehlern ist ein Problem, wenn die Fehler schwerwiegendere Probleme verhindert hätten.
Erik Aronesty
0

Ich gehe dann mit "ein bisschen wie Docstrings" und erkläre dies für harmlos, solange es immer soNone ist oder ein enger Bereich von anderen Werten, die alle unveränderlich sind.

Es riecht nach Atavismus und exzessiver Bindung an statisch typisierte Sprachen. Und es ist kein guter Code. Aber es hat einen kleinen Zweck, der in der Dokumentation verbleibt.

Es dokumentiert die erwarteten Namen. Wenn ich also Code mit jemandem kombiniere und einer von uns 'Benutzername' und der andere 'Benutzername' hat, gibt es einen Hinweis auf den Menschen, dass wir getrennte Wege gegangen sind und nicht dieselben Variablen verwenden.

Durch Erzwingen der vollständigen Initialisierung als Richtlinie wird das Gleiche auf pythonischere Weise erreicht. Wenn sich jedoch tatsächlicher Code in der Richtlinie befindet __init__, können die verwendeten Variablen auf diese Weise besser dokumentiert werden .

Offensichtlich ist das BIG-Problem hier, dass es die Leute dazu verleitet, mit anderen Werten als zu initialisieren None, was schlecht sein kann:

class X:
    v = {}
x = X()
x.v[1] = 2

Hinterlässt einen globalen Trace und erstellt keine Instanz für x.

Aber das ist in Python als Ganzes eher eine Eigenart als in dieser Praxis, und wir sollten bereits paranoid sein.

Jon Jay Obermark
quelle