Was ist ein sauberer “pythonischer” Weg, um mehrere Konstruktoren zu implementieren?

Lesezeit: 11 Minuten

Benutzer-Avatar
Winschmied

Ich kann keine endgültige Antwort darauf finden. So viel ich weiß, kann man nicht mehrere haben __init__ Funktionen in einer Python-Klasse. Also wie löse ich dieses Problem?

Angenommen, ich habe eine Klasse angerufen Cheese mit dem number_of_holes Eigentum. Wie kann ich zwei Möglichkeiten haben, Käseobjekte zu erstellen …

  1. Eine, die eine Reihe von Löchern wie diese nimmt: parmesan = Cheese(num_holes = 15).
  2. Und eine, die keine Argumente akzeptiert und die einfach randomisiert number_of_holes Eigentum: gouda = Cheese().

Ich kann mir nur einen Weg vorstellen, dies zu tun, aber das scheint klobig zu sein:

class Cheese():
    def __init__(self, num_holes = 0):
        if (num_holes == 0):
            # Randomize number_of_holes
        else:
            number_of_holes = num_holes

Was sagst du? Gibt es eine andere Art und Weise?

  • Ich finde drin ist kein Konstruktor, sondern ein Initialisierer. Neu wäre ein Konstrukteur

    – Fanny

    20. Februar 2019 um 13:11 Uhr


  • Verwandte (nicht doppelt): Wie kann ich doppelte Methodennamen in einer Python-Klasse erkennen?

    – Peter Mortensen

    6. Januar 2021 um 16:12 Uhr

Benutzer-Avatar
vartec

Eigentlich None ist viel besser für “magische” Werte:

class Cheese():
    def __init__(self, num_holes = None):
        if num_holes is None:
            ...

Wenn Sie nun völlige Freiheit beim Hinzufügen weiterer Parameter wünschen:

class Cheese():
    def __init__(self, *args, **kwargs):
        #args -- tuple of anonymous arguments
        #kwargs -- dictionary of named arguments
        self.num_holes = kwargs.get('num_holes',random_holes())

Um das Konzept besser zu erklären *args und **kwargs (Sie können diese Namen tatsächlich ändern):

def f(*args, **kwargs):
   print 'args: ', args, ' kwargs: ', kwargs

>>> f('a')
args:  ('a',)  kwargs:  {}
>>> f(ar="a")
args:  ()  kwargs:  {'ar': 'a'}
>>> f(1,2,param=3)
args:  (1, 2)  kwargs:  {'param': 3}

http://docs.python.org/reference/expressions.html#calls

  • Für Interessierte, kwargs steht für Schlüsselwortargumente (scheint logisch, wenn man es weiß). 🙂

    – tleb

    3. August 2016 um 6:29 Uhr


  • Es gibt Momente, die *args und **kwargs sind ein Overkill. Bei den meisten Konstruktoren möchten Sie wissen, was Ihre Argumente sind.

    – Benutzer989762

    28. April 2018 um 9:26 Uhr

  • @ user989762 Ja! Mit Sicherheit!

    – Kapitän Jack Sparrow

    15. April 2020 um 0:30 Uhr

  • @ user989762 Ja, dieser Ansatz ist überhaupt nicht selbstdokumentierend (wie oft haben Sie versucht, eine Bibliothek zu verwenden und versucht, die Verwendung von Methodensignaturen zu verstehen, nur um festzustellen, dass Sie einen Code-Tauchgang durchführen müssen, um zu sehen, welche Argumente erwartet werden / erlaubt?) Darüber hinaus übernimmt Ihre Implementierung jetzt die zusätzliche Last der Argumentüberprüfung, einschließlich der Wahl, ob nicht unterstützte Argumente akzeptiert oder ausgeschlossen werden sollen (teehee).

    – GlenRSmith

    22. Juli 2020 um 15:36 Uhr

  • Scrollen Sie für Leute von Google im Jahr 2020 auf dieser Seite ein wenig nach unten. Die Antwort von „Ber“ weiter unten ist solide und für die meisten Szenarien pythonischer als diese Route.

    –Tom H

    12. November 2020 um 13:40 Uhr


Benutzer-Avatar
Ber

Verwenden num_holes=None wie die Voreinstellung ist in Ordnung, wenn Sie gerade haben werden __init__.

Wenn Sie mehrere unabhängige “Konstruktoren” wünschen, können Sie diese als Klassenmethoden bereitstellen. Diese werden normalerweise Fabrikmethoden genannt. In diesem Fall könnten Sie die Vorgabe für haben num_holes sein 0.

class Cheese(object):
    def __init__(self, num_holes=0):
        "defaults to a solid cheese"
        self.number_of_holes = num_holes

    @classmethod
    def random(cls):
        return cls(randint(0, 100))

    @classmethod
    def slightly_holey(cls):
        return cls(randint(0, 33))

    @classmethod
    def very_holey(cls):
        return cls(randint(66, 100))

Erstellen Sie nun ein Objekt wie folgt:

gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()

  • @rmbianchi: Die akzeptierte Antwort entspricht möglicherweise eher anderen Sprachen, ist aber auch weniger pythonisch: @classmethods sind die pythonische Art, mehrere Konstruktoren zu implementieren.

    – Ethan Furmann

    22. März 2012 um 1:34 Uhr

  • @Bepetersn Es gibt Instanzmethoden (die normalen), die ein Instanzobjekt haben, auf das verwiesen wird self. Dann gibt es Klassenmethoden (mit @classmethod), die auf das Klassenobjekt as verweisen cls. Und schließlich gibt es noch statische Methoden (deklariert mit @staticmethod), die keinen dieser Verweise haben. Statische Methoden sind genau wie Funktionen auf Modulebene, außer dass sie im Namensraum der Klasse leben.

    – Ber

    22. April 2013 um 11:10 Uhr

  • Ein Vorteil dieser Methode gegenüber der akzeptierten Lösung besteht darin, dass sie es ermöglicht, abstrakte Konstruktoren einfach anzugeben und deren Implementierung zu erzwingen, insbesondere mit Python 3, in dem die Verwendung von @abstractmethod und @classmethod auf der gleichen werksfunktion ist möglich und in die sprache eingebaut. Ich würde auch argumentieren, dass dieser Ansatz expliziter ist, was damit einhergeht Das Zen von Python.

    – Mach

    4. September 2014 um 13:16 Uhr


  • @ashu Die anderen Konstruktoren rufen die Methode __init__() auf, indem sie die Klasse über cls(…) instanziieren. Daher wird die Anzahl_der_Löcher immer auf die gleiche Weise verwendet.

    – Ber

    23. August 2018 um 11:37 Uhr

  • @RegisMay (1/2) Anstatt ein paar zu haben ifist drin __init__()besteht der Trick darin, dass jede der einzigartigen Factory-Methoden ihre eigenen einzigartigen Aspekte der Initialisierung handhabt und hat __init__() Akzeptieren Sie nur die grundlegenden Daten, die eine Instanz definieren. Zum Beispiel, Cheese könnte Attribute haben volume und average_hole_radius zusätzlich zu number_of_holes. __init__() würde diese drei Werte akzeptieren. Dann könnten Sie eine Klassenmethode haben with_density() das zufällig die grundlegenden Attribute auswählt, die einer bestimmten Dichte entsprechen, und sie anschließend an sie weitergibt __init__().

    – Nathaniel Jones

    22. Juni 2020 um 20:28 Uhr

Benutzer-Avatar
Andrzej Pronobis

Man sollte auf jeden Fall die bereits geposteten Lösungen bevorzugen, da diese Lösung aber noch von niemandem erwähnt wurde, halte ich sie der Vollständigkeit halber für erwähnenswert.

Das @classmethod -Ansatz kann geändert werden, um einen alternativen Konstruktor bereitzustellen, der den Standardkonstruktor nicht aufruft (__init__). Stattdessen wird eine Instanz mit erstellt __new__.

Dies könnte verwendet werden, wenn der Initialisierungstyp nicht basierend auf dem Typ des Konstruktorarguments ausgewählt werden kann und die Konstruktoren keinen gemeinsamen Code verwenden.

Beispiel:

class MyClass(set):

    def __init__(self, filename):
        self._value = load_from_file(filename)

    @classmethod
    def from_somewhere(cls, somename):
        obj = cls.__new__(cls)  # Does not call __init__
        super(MyClass, obj).__init__()  # Don't forget to call any polymorphic base class initializers
        obj._value = load_from_somewhere(somename)
        return obj

  • Dies ist die Lösung, die tatsächlich unabhängige Konstrukteure bietet, anstatt mit ihnen herumzuspielen __init__‘s Argumente. Könnten Sie jedoch bitte einige Referenzen angeben, dass diese Methode irgendwie offiziell genehmigt oder unterstützt wird? Wie sicher und zuverlässig ist es, direkt anzurufen? __new__ Methode?

    – Alexej

    20. Februar 2018 um 14:11 Uhr


  • Ich habe die Dinge so gemacht und bin dann hierher gekommen, um die obige Frage zu stellen, um zu sehen, ob mein Weg richtig war. Sie müssen noch anrufen super Andernfalls funktioniert dies bei kooperativer Mehrfachvererbung nicht, daher habe ich die Zeile zu Ihrer Antwort hinzugefügt.

    – Neil G

    1. März 2020 um 23:22 Uhr

  • Ich frage mich, ob man einen Dekorateur “Konstruktor” definieren könnte (das schließt die Neu und super Sachen) und dann: @constructor def other_init(self, stuff): self.stuff = stuff

    – Tom Winde

    30. Oktober 2020 um 16:36 Uhr

Alle diese Antworten sind hervorragend, wenn Sie optionale Parameter verwenden möchten, aber eine andere pythonische Möglichkeit besteht darin, eine Klassenmethode zu verwenden, um einen Pseudokonstruktor im Factory-Stil zu generieren:

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )

Warum denkst du, dass deine Lösung “klobig” ist? Persönlich würde ich in Situationen wie Ihrer einen Konstruktor mit Standardwerten gegenüber mehreren überladenen Konstruktoren bevorzugen (Python unterstützt sowieso keine Methodenüberladung):

def __init__(self, num_holes=None):
    if num_holes is None:
        # Construct a gouda
    else:
        # custom cheese
    # common initialization

Für wirklich komplexe Fälle mit vielen verschiedenen Konstrukteuren kann es sauberer sein, stattdessen verschiedene Factory-Funktionen zu verwenden:

@classmethod
def create_gouda(cls):
    c = Cheese()
    # ...
    return c

@classmethod
def create_cheddar(cls):
    # ...

In Ihrem Käsebeispiel möchten Sie vielleicht eine Gouda-Unterklasse von Käse verwenden …

  • Verwendung von Werksfunktionen Kl: verwenden Kl Anstatt von Käse. Wenn nicht, welchen Sinn hat es, Klassenmethoden anstelle von statischen Methoden zu verwenden?

    – Rollen

    2. August 2018 um 11:53 Uhr


Benutzer-Avatar
Brad C

Das sind gute Ideen für Ihre Implementierung, aber wenn Sie einem Benutzer eine Schnittstelle zur Käseherstellung präsentieren. Es ist ihnen egal, wie viele Löcher der Käse hat oder welche Innereien in die Käseherstellung einfließen. Der Benutzer Ihres Codes möchte nur “Gouda” oder “Parmesan”, richtig?

Warum also nicht Folgendes tun:

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
paremesean = make_parmesean()

Und dann können Sie eine der oben genannten Methoden verwenden, um die Funktionen tatsächlich zu implementieren:

# cheeses.py
class Cheese(object):
    def __init__(self, *args, **kwargs):
        #args -- tuple of anonymous arguments
        #kwargs -- dictionary of named arguments
        self.num_holes = kwargs.get('num_holes',random_holes())

def make_gouda():
    return Cheese()

def make_paremesean():
    return Cheese(num_holes=15)

Dies ist eine gute Kapselungstechnik, und ich denke, sie ist eher pythonisch. Für mich passt diese Vorgehensweise eher zum Ententippen. Sie fragen einfach nach einem Gouda-Objekt und es ist Ihnen egal, welcher Klasse es angehört.

  • Verwendung von Werksfunktionen Kl: verwenden Kl Anstatt von Käse. Wenn nicht, welchen Sinn hat es, Klassenmethoden anstelle von statischen Methoden zu verwenden?

    – Rollen

    2. August 2018 um 11:53 Uhr


Überblick

Für das spezifische Käsebeispiel stimme ich vielen anderen Antworten über die Verwendung von Standardwerten zu, um eine zufällige Initialisierung zu signalisieren oder eine statische Factory-Methode zu verwenden. Es kann jedoch auch verwandte Szenarien geben, die Sie dort im Sinn hatten ist Wert darauf, alternative, prägnante Möglichkeiten zum Aufrufen des Konstruktors zu haben, ohne die Qualität von Parameternamen oder Typinformationen zu beeinträchtigen.

Seit Python 3.8 und functools.singledispatchmethod kann dabei in vielen Fällen helfen (und desto flexibler multimethod kann in noch mehr Szenarien angewendet werden). (Dieser verwandte Beitrag beschreibt, wie man dasselbe in Python 3.4 ohne eine Bibliothek erreichen könnte.) Ich habe in der Dokumentation für keines dieser Beispiele Beispiele gesehen, die speziell das Überladen zeigen __init__ wie Sie fragen, aber es scheint, dass die gleichen Prinzipien für das Überladen einer Member-Methode gelten (wie unten gezeigt).

“Single Dispatch” (verfügbar in der Standardbibliothek) erfordert, dass es mindestens einen Positionsparameter gibt und dass der Typ des ersten Arguments ausreicht, um zwischen den möglichen überladenen Optionen zu unterscheiden. Für das spezifische Cheese-Beispiel gilt dies nicht, da Sie zufällige Löcher wollten, wenn keine Parameter angegeben wurden, aber multidispatch unterstützt dieselbe Syntax und kann verwendet werden, solange jede Methodenversion basierend auf der Anzahl und dem Typ aller Argumente zusammen unterschieden werden kann.

Beispiel

Hier ist ein Beispiel für die Verwendung einer der beiden Methoden (einige der Details dienen der Bitte mypy was mein Ziel war, als ich das zum ersten Mal zusammenstellte):

from functools import singledispatchmethod as overload
# or the following more flexible method after `pip install multimethod`
# from multimethod import multidispatch as overload


class MyClass:

    @overload  # type: ignore[misc]
    def __init__(self, a: int = 0, b: str="default"):
        self.a = a
        self.b = b

    @__init__.register
    def _from_str(self, b: str, a: int = 0):
        self.__init__(a, b)  # type: ignore[misc]

    def __repr__(self) -> str:
        return f"({self.a}, {self.b})"


print([
    MyClass(1, "test"),
    MyClass("test", 1),
    MyClass("test"),
    MyClass(1, b="test"),
    MyClass("test", a=1),
    MyClass("test"),
    MyClass(1),
    # MyClass(),  # `multidispatch` version handles these 3, too.
    # MyClass(a=1, b="test"),
    # MyClass(b="test", a=1),
])

Ausgabe:

[(1, test), (1, test), (0, test), (1, test), (1, test), (0, test), (1, default)]

Anmerkungen:

  • Normalerweise würde ich den Alias ​​nicht nennen overloadaber es hat geholfen, den Unterschied zwischen der Verwendung der beiden Methoden nur eine Frage des verwendeten Imports zu machen.
  • Das # type: ignore[misc] Kommentare sind nicht notwendig, um zu laufen, aber ich füge sie dort ein, um zu gefallen mypy der nicht gerne dekoriert __init__ auch nicht anrufen __init__ direkt.
  • Wenn Sie mit der Decorator-Syntax noch nicht vertraut sind, machen Sie sich das Putten bewusst @overload vor der Definition von __init__ ist nur Zucker für __init__ = overload(the original definition of __init__). In diesem Fall, overload ist eine Klasse, also das Ergebnis __init__ ist ein Objekt, das a hat __call__ -Methode, sodass es wie eine Funktion aussieht, die aber auch eine hat .register -Methode, die später aufgerufen wird, um eine weitere überladene Version von hinzuzufügen __init__. Das ist ein bisschen chaotisch, aber es freut mich, weil keine Methodennamen doppelt definiert werden. Wenn Sie sich nicht für mypy interessieren und trotzdem vorhaben, die externe Bibliothek zu verwenden, multimethod hat auch einfachere alternative Möglichkeiten, überladene Versionen anzugeben.
  • Definieren __repr__ ist einfach da, um die gedruckte Ausgabe aussagekräftig zu machen (man braucht es im Allgemeinen nicht).
  • Beachte das multidispatch kann drei zusätzliche Eingabekombinationen verarbeiten, die keine Positionsparameter haben.

  • Vielen Dank für diese Antwort und den Hinweis auf multimethod package. In manchen Situationen fühlt sich der Mehrfachversand einfach so natürlich an. Nachdem ich eine Weile in Julia gearbeitet habe, vermisse ich das in Python.

    – Erich

    7. September 2021 um 12:23 Uhr

1142800cookie-checkWas ist ein sauberer “pythonischer” Weg, um mehrere Konstruktoren zu implementieren?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy