Wie vermeide ich das "self.x = x; self.y = y; self.z = z ”Muster in __init__?

170

Ich sehe Muster wie

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

ziemlich häufig, oft mit viel mehr Parametern. Gibt es eine gute Möglichkeit, diese Art von langwieriger Wiederholung zu vermeiden? Sollte die Klasse namedtuplestattdessen erben ?

MaxB
quelle
31
Nicht jede Aufnahmefähigkeit ist schlecht. Beachten Sie, dass das Klassenmodell von Python keine explizite Definition von Instanzattributen enthält, sodass diese Zuweisungen die selbstdokumentierenden Entsprechungen sind.
Chepper
4
@chepner: Nun, nicht erfordert explizite Definition. Sie können jedoch __slots__für diesen Zweck verwenden ; Es ist leicht unpythonisch (ausführlicher, um Speichereinsparungen zu erzielen), aber ich mag es weitgehend, um das Risiko einer automatischen Belebung eines ganz neuen Attributs zu vermeiden, wenn ich den Namen tippe.
ShadowRanger
2
Jeder gute Editor hat Vorlagen. Sie tippen ini <shortcut> x, y, z): <shortcut>und Sie sind fertig.
Gerenuk
3
Namedtuples sind fantastisch, wenn Sie ein unveränderliches Wertobjekt möchten. Wenn Sie eine reguläre, veränderbare Klasse möchten, können Sie sie nicht verwenden.
RemcoGerlich
4
"Nicht" ist eine gute Option. Jede verfügbare Option zerstört die Methodensignatur (und damit möglicherweise die gesamte Schnittstelle). Wenn Ihre Klassen eine unerträgliche Anzahl von Feldern zum Initialisieren haben, sollten Sie sie möglicherweise aufteilen.
Kroltan

Antworten:

87

Bearbeiten: Wenn Sie Python 3.7+ haben, verwenden Sie einfach Datenklassen

Eine Dekorationslösung, die die Signatur behält:

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))
Siphor
quelle
2
nette Antwort, funktioniert aber nicht mit Python2.7: Neinsignature
MaxB
3
@alexis der "decorator.decorator" Dekorateur
schließt
4
Ich bin ziemlich hin und her gerissen, ob ich das lieben oder hassen soll. Ich schätze es, die Unterschrift zu bewahren.
Kyle Strand
14
"... Explizit ist besser als implizit. Einfach ist besser als komplex. ..." -Zen of Python
Jack Stout
9
-1 Ehrlich gesagt ist das schrecklich. Ich habe keine Ahnung, was dieser Code auf einen Blick tut, und es ist buchstäblich zehnmal so viel Code. Klug zu sein fühlt sich cool an, aber das ist ein Missbrauch Ihrer offensichtlichen Klugheit.
Ian Newson
108

Haftungsausschluss: Es scheint, dass mehrere Personen besorgt sind, diese Lösung vorzustellen, daher werde ich einen sehr klaren Haftungsausschluss geben. Sie sollten diese Lösung nicht verwenden. Ich gebe es nur als Information, damit Sie wissen, dass die Sprache dazu in der Lage ist. Der Rest der Antwort zeigt nur die Sprachfähigkeiten und unterstützt sie nicht auf diese Weise.


Es ist eigentlich nichts Falsches daran, Parameter explizit in Attribute zu kopieren. Wenn Sie zu viele Parameter im ctor haben, wird dies manchmal als Codegeruch angesehen, und Sie sollten diese Parameter möglicherweise in weniger Objekte gruppieren. In anderen Fällen ist es notwendig und es ist nichts falsch daran. Auf jeden Fall ist es der richtige Weg, dies explizit zu tun.

Da Sie jedoch fragen, wie dies getan werden kann (und nicht, ob es getan werden soll), lautet eine Lösung:

class A:
    def __init__(self, **kwargs):
        for key in kwargs:
          setattr(self, key, kwargs[key])

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2
gruszczy
quelle
16
gute Antwort +1 ... obwohl self.__dict__.update(kwargs)etwas pythonischer sein könnte
Joran Beasley
44
Das Problem bei diesem Ansatz besteht darin, dass nicht A.__init__aufgezeichnet wird, welche Argumente tatsächlich erwartet werden, und keine Fehlerprüfung auf falsch eingegebene Argumentnamen erfolgt.
MaxB
7
@JoranBeasley Wenn Sie das Instanzwörterbuch blind aktualisieren, bleiben kwargsSie offen für das Äquivalent eines SQL-Injection-Angriffs. Wenn Ihr Objekt eine Methode mit dem Namen hat my_methodund Sie ein Argument mit dem Namen my_methodan den Konstruktor und update()das Wörterbuch übergeben, haben Sie die Methode einfach überschrieben.
Pedro
3
Wie andere sagten, ist der Vorschlag wirklich schlechter Programmierstil. Es verbirgt wichtige Informationen. Sie können es zeigen, aber Sie sollten das OP ausdrücklich davon abhalten, es zu verwenden.
Gerenuk
3
@Pedro Gibt es einen semantischen Unterschied zwischen der Syntax von gruzczy und JoranBeasley?
Gerrit
29

Wie andere bereits erwähnt haben, ist die Wiederholung nicht schlecht, aber in einigen Fällen kann ein benanntes Tupel gut zu dieser Art von Problem passen. Dies vermeidet die Verwendung von Einheimischen () oder Kwargs, was normalerweise eine schlechte Idee ist.

from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")

# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z

Ich habe eine begrenzte Verwendung dafür gefunden, aber Sie können ein benanntes Tupel wie bei jedem anderen Objekt erben (Beispiel wird fortgesetzt):

class MySuperXYZ(XYZ):
    """ I add a helper function which returns the original properties """
    def properties(self):
        return self.x, self.y, self.z

abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()
Ein kleines Shell-Skript
quelle
5
Da es sich um Tupel handelt, kann Ihre propertiesMethode als gerecht geschrieben werden. Dies return tuple(self)ist besser zu warten, wenn in Zukunft mehr Felder zur Definition des benannten Tupels hinzugefügt werden.
PaulMcG
1
Außerdem erfordert Ihre Namedtuple-Deklarationszeichenfolge keine Kommas zwischen den Feldnamen, XYZ = namedtuple("XYZ", "x y z")funktioniert genauso gut.
PaulMcG
Danke @PaulMcGuire. Ich habe versucht, mir ein wirklich einfaches Add-On auszudenken, um die Vererbung und die Art der Abstände zu zeigen. Sie haben 100% Recht und es ist auch eine gute Abkürzung für andere geerbte Objekte! Ich erwähne, dass die Feldnamen durch Kommas oder Leerzeichen getrennt sein können - ich bevorzuge CSV aus Gewohnheit
Ein kleines Shell-Skript
1
Ich verwende namedtuples oft genau für diesen Zweck, insbesondere in mathematischem Code, in dem eine Funktion möglicherweise stark parametrisiert ist und eine Reihe von Koeffizienten aufweist, die nur zusammen Sinn ergeben.
Detly
Das Problem dabei namedtupleist, dass sie schreibgeschützt sind. Sie können nicht abc.x += 1oder so etwas tun .
Hamstergen
29

explizit ist besser als implizit ... so sicher, dass Sie es prägnanter gestalten können:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

Die bessere Frage ist, sollten Sie?

... das heißt, wenn Sie ein benanntes Tupel möchten, würde ich die Verwendung eines benannten Tupels empfehlen (denken Sie daran, dass an Tupel bestimmte Bedingungen geknüpft sind) ... vielleicht möchten Sie ein geordnetes Diktat oder sogar nur ein Diktat ...

Joran Beasley
quelle
Dann benötigt das Objekt eine zyklische Speicherbereinigung, da es sich selbst als Attribut hat
John La Rooy
3
@bernie (oder ist es bemie?), manchmal ist ke r ning schwer
Katze
4
Für etwas effizientere Tests if k != "self":könnte ein if v is not self:billiger Identitätstest anstelle eines Zeichenfolgenvergleichs verwendet werden. Ich nehme an, technisch __init__könnte ein zweites Mal nach dem Bau aufgerufen und selfals nachfolgendes Argument übergeben werden, aber ich möchte wirklich nicht darüber nachdenken, welche Art von Monster das tun würde. :-)
ShadowRanger
Dies könnte zu einer Funktion gemacht werden, die den Rückgabewert von locals: annimmt set_fields_from_locals(locals()). Dann ist es nicht länger als die magischeren Dekorationslösungen.
Lii
20

Um die gruszczyAntwort zu erweitern , habe ich ein Muster wie das folgende verwendet:

class X:
    x = None
    y = None
    z = None
    def __init__(self, **kwargs):
        for (k, v) in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise TypeError('Unknown keyword argument: {:s}'.format(k))

Ich mag diese Methode, weil es:

  • vermeidet Wiederholungen
  • ist beim Erstellen eines Objekts gegen Tippfehler resistent
  • funktioniert gut mit Unterklassen (kann nur super().__init(...))
  • Ermöglicht die Dokumentation der Attribute auf Klassenebene (wo sie hingehören) und nicht in X.__init__

Vor Python 3.6 gibt dies keine Kontrolle über die Reihenfolge, in der die Attribute festgelegt werden. Dies kann ein Problem sein, wenn einige Attribute Eigenschaften mit Setzern sind, die auf andere Attribute zugreifen.

Es könnte wahrscheinlich ein bisschen verbessert werden, aber ich bin der einzige Benutzer meines eigenen Codes, sodass ich mir keine Sorgen über irgendeine Form der Eingabesanierung mache. Vielleicht AttributeErrorwäre ein passender.

gerrit
quelle
10

Sie könnten auch tun:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

Natürlich müssten Sie das inspectModul importieren .

Zondo
quelle
8

Dies ist eine Lösung ohne zusätzliche Importe.

Hilfsfunktion

Eine kleine Hilfsfunktion macht es bequemer und wiederverwendbarer:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    self = local_name_space.pop('self')
    for name, value in local_name_space.items():
        setattr(self, name, value)

Anwendung

Sie müssen es aufrufen mit locals():

class A:
    def __init__(self, x, y, z):
        auto_init(locals())

Prüfung

a = A(1, 2, 3)
print(a.__dict__)

Ausgabe:

{'y': 2, 'z': 3, 'x': 1}

Ohne Veränderung locals()

Wenn Sie nicht ändern möchten, locals()verwenden Sie diese Version:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    for name, value in local_name_space.items():
        if name != 'self': 
            setattr(local_name_space['self'], name, value)
Mike Müller
quelle
docs.python.org/2/library/functions.html#locals locals() sollte nicht geändert werden (dies kann sich auf den Interpreter auswirken, in Ihrem Fall selfaus dem Bereich der aufrufenden Funktion entfernen )
MaxB
@MaxB In den von Ihnen zitierten Dokumenten heißt es: ... Änderungen wirken sich möglicherweise nicht auf die Werte lokaler und freier Variablen aus, die vom Interpreter verwendet werden. selfist noch verfügbar in __init__.
Mike Müller
Richtig, der Leser erwartet, dass es die lokalen Variablen beeinflusst, aber es kann abhängig von einer Reihe von Umständen sein oder auch nicht . Der Punkt ist, dass es UB ist.
MaxB
Zitat: "Der Inhalt dieses Wörterbuchs sollte nicht geändert werden"
MaxB
@MaxB Ich habe eine Version hinzugefügt, die local () nicht ändert.
Mike Müller
7

Eine interessante Bibliothek, die dies handhabt (und viele andere Boilerplates vermeidet), ist attrs . Ihr Beispiel könnte zum Beispiel darauf reduziert werden (vorausgesetzt, die Klasse heißt MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

Sie brauchen nicht einmal __init__mehr eine Methode, es sei denn, sie erledigt auch andere Dinge. Hier ist eine schöne Einführung von Glyph Lefkowitz .

RafG
quelle
Inwieweit wird die Funktion von attrüberflüssig gemacht dataclasses?
Gerrit
1
@gerrit Dies wird in der Dokumentation des attrs-Pakets erläutert . Tbh, die Unterschiede scheinen nicht mehr so ​​groß zu sein.
Ivo Merchiers
5

Meine 0,02 $. Es ist sehr nah an Joran Beasleys Antwort, aber eleganter:

def __init__(self, a, b, c, d, e, f):
    vars(self).update((k, v) for k, v in locals().items() if v is not self)

Zusätzlich kann die Antwort von Mike Müller (die beste nach meinem Geschmack) mit dieser Technik reduziert werden:

def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

Und der gerade Anruf auto_init(locals())von Ihrem__init__

bgusach
quelle
1
docs.python.org/2/library/functions.html#locals locals() sollte nicht geändert werden (undefiniertes Verhalten)
MaxB
4

Es ist eine natürliche Art, Dinge in Python zu tun. Versuchen Sie nicht, etwas Klügeres zu erfinden, da dies zu übermäßig cleverem Code führt, den niemand in Ihrem Team verstehen wird. Wenn du ein Teamplayer sein willst und es dann so weiter schreibst.

Peter Krumins
quelle
4

Ab Python 3.7

In Python 3.7 können Sie (ab) den dataclassDekorator verwenden, der im dataclassesModul verfügbar ist . Aus der Dokumentation:

Dieses Modul bietet einen Dekorator und Funktionen zum automatischen Hinzufügen generierter Spezialmethoden wie __init__()und__repr__() zu benutzerdefinierten Klassen. Es wurde ursprünglich in PEP 557 beschrieben.

Die in diesen generierten Methoden zu verwendenden Elementvariablen werden mithilfe von Annotationen vom Typ PEP 526 definiert. Zum Beispiel dieser Code:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Fügt unter anderem __init__()Folgendes hinzu:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
      self.name = name
      self.unit_price = unit_price
      self.quantity_on_hand = quantity_on_hand

Beachten Sie, dass diese Methode automatisch zur Klasse hinzugefügt wird: Sie wird in der oben gezeigten InventoryItem-Definition nicht direkt angegeben.

Wenn Ihre Klasse groß und komplex ist, kann es unangemessen sein, a zu verwenden dataclass. Ich schreibe dies am Tag der Veröffentlichung von Python 3.7.0, daher sind die Verwendungsmuster noch nicht gut etabliert.

gerrit
quelle