969 Stimmen

Was ist eine saubere "Python" Weg, um mehrere Konstruktoren zu implementieren?

Ich kann keine endgültige Antwort darauf finden. Soviel ich weiß, kann man nicht mehrere __init__ Funktionen in einer Python-Klasse. Wie kann ich dieses Problem also lösen?

Angenommen, ich habe eine Klasse namens Cheese mit dem number_of_holes Eigentum. Wie kann ich zwei Möglichkeiten zur Erstellung von Käseobjekten haben...

  1. Einer, der eine Reihe von Löchern wie dieses nimmt: parmesan = Cheese(num_holes = 15) .
  2. Und eine, die keine Argumente benötigt und nur die number_of_holes Eigentum: gouda = Cheese() .

Mir fällt nur eine Möglichkeit ein, dies zu tun, aber das erscheint mir umständlich:

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

Was sagen Sie dazu? Gibt es einen anderen Weg?

11 Stimmen

Ich denke init ist kein Konstruktor, sondern ein Initialisierer. neu wäre ein Konstrukteur

0 Stimmen

19voto

Brad C Punkte 710

Das sind gute Ideen für Ihre Implementierung, aber wenn Sie einem Benutzer eine Schnittstelle für die Käseherstellung präsentieren. Der interessiert sich nicht dafür, wie viele Löcher der Käse hat oder was für Interna in der Käseherstellung stecken. Der Benutzer Ihres Codes will nur "Gouda" oder "Parmesan", richtig?

Warum also nicht dies 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 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 Verkapselungstechnik, und ich denke, sie ist mehr Pythonic. Für mich passt diese Art, Dinge zu tun, eher zum Duck Typing. Sie fragen einfach nach einem Gouda-Objekt und es ist Ihnen egal, um welche Klasse es sich handelt.

2 Stimmen

Ich neige dazu, mich für diesen Ansatz zu entscheiden, weil er der Fabrik-Methoden-Muster .

8 Stimmen

make_gouda, make_parmesan sollten Klassenmethoden sein von class Cheese

15voto

teichert Punkte 2953

Übersicht

Für das spezifische Käsebeispiel stimme ich mit vielen der anderen Antworten über die Verwendung von Standardwerten überein, um eine zufällige Initialisierung zu signalisieren oder eine statische Fabrikmethode zu verwenden. Es kann jedoch auch verwandte Szenarien geben, die Sie im Sinn hatten, wo es est Wert in alternativen, prägnanten Möglichkeiten, den Konstruktor aufzurufen, ohne die Qualität der Parameternamen oder Typinformationen zu beeinträchtigen.

Seit Python 3.8 und functools.singledispatchmethod kann in vielen Fällen helfen, dies zu erreichen (und die flexiblere multimethod kann in noch mehr Szenarien gelten). ( Dieser verwandte Beitrag beschreibt, wie man dasselbe in Python 3.4 ohne eine Bibliothek erreichen kann). Ich habe in der Dokumentation für beide keine Beispiele gesehen, die speziell das Überladen von __init__ wie Sie fragen, aber es scheint, dass die gleichen Grundsätze für das Überladen jeder 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 Käse-Beispiel trifft dies nicht zu, da Sie zufällige Löcher haben wollten, wenn keine Parameter angegeben wurden, aber multidispatch unterstützt dieselbe Syntax und kann verwendet werden, solange jede Methodenversion anhand der Anzahl und des Typs aller Argumente zusammen unterschieden werden kann.

Beispiel

Im Folgenden finden Sie ein Beispiel für die Anwendung beider Methoden (einige Details sind aus Gründen der Übersichtlichkeit angegeben) mypy was mein Ziel war, als ich das Projekt ins Leben rief):

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:

  • Ich würde den Alias normalerweise nicht als overload aber es hat dazu beigetragen, dass der Unterschied zwischen den beiden Methoden nur noch eine Frage des verwendeten Imports ist.
  • Le site # type: ignore[misc] Kommentare sind für die Ausführung nicht erforderlich, aber ich füge sie ein, um die mypy die nicht gerne dekoriert __init__ noch anrufen __init__ direkt.
  • Wenn Sie mit der Dekorator-Syntax nicht vertraut sind, sollten Sie wissen, dass die @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, so dass die resultierende __init__ ist ein Objekt, das eine __call__ Methode, so dass sie wie eine Funktion aussieht, die aber auch eine .register Methode, die später aufgerufen wird, um eine weitere überladene Version von __init__ . Das ist ein bisschen unübersichtlich, aber es gefällt mypy, weil keine Methodennamen doppelt definiert werden. Wenn Sie sich nicht um mypy kümmern und sowieso vorhaben, die externe Bibliothek zu verwenden, multimethod hat auch einfachere alternative Möglichkeiten, überladene Versionen anzugeben.
  • Definition von __repr__ ist nur dazu da, die Druckausgabe sinnvoll zu gestalten (man braucht sie im Allgemeinen nicht).
  • Beachten Sie, dass multidispatch ist in der Lage, drei zusätzliche Eingabekombinationen zu verarbeiten, die keine Positionsparameter haben.

0 Stimmen

Vielen Dank für diese Antwort und den Hinweis auf das Multimethodenpaket. In manchen Situationen fühlt sich die Mehrfachabfertigung einfach so natürlich an. Nachdem in Julia für eine Weile gearbeitet, ist es etwas, das ich in Python vermisse.

10voto

Devin Jeanpierre Punkte 87113

使用方法 num_holes=None als Standardeinstellung. Prüfen Sie dann, ob num_holes is None und wenn ja, randomisieren. So sehe ich es jedenfalls im Allgemeinen.

Grundlegend andere Konstruktionsmethoden können eine Klassenmethode rechtfertigen, die eine Instanz von cls .

0 Stimmen

Ist "classmethod" wörtlich zu nehmen? Oder meinen Sie Klassenmethode ?

9voto

mluebke Punkte 8100

Die beste Antwort ist die obige über Standardargumente, aber es hat mir Spaß gemacht, das hier zu schreiben, und es passt auf jeden Fall zum Thema "mehrere Konstruktoren". Verwendung auf eigene Gefahr.

Was ist mit dem neu método.

"Typische Implementierungen erzeugen eine neue Instanz der Klasse, indem sie die Superclass neu ()-Methode mit super(currentclass, cls). neu (cls[, ...]) mit den entsprechenden Argumenten und modifiziert dann die neu erstellte Instanz nach Bedarf, bevor sie zurückgegeben wird."

So können Sie die neu Methode ändern Sie Ihre Klassendefinition, indem Sie die entsprechende Konstruktormethode anhängen.

class Cheese(object):
    def __new__(cls, *args, **kwargs):

        obj = super(Cheese, cls).__new__(cls)
        num_holes = kwargs.get('num_holes', random_holes())

        if num_holes == 0:
            cls.__init__ = cls.foomethod
        else:
            cls.__init__ = cls.barmethod

        return obj

    def foomethod(self, *args, **kwargs):
        print "foomethod called as __init__ for Cheese"

    def barmethod(self, *args, **kwargs):
        print "barmethod called as __init__ for Cheese"

if __name__ == "__main__":
    parm = Cheese(num_holes=5)

12 Stimmen

Dies ist die Art von Code, die mir Alpträume über die Arbeit in dynamischen Sprachen gibt - nicht zu sagen, dass es etwas inhärent falsch mit ihm, nur, dass es einige wichtige Annahmen, die ich über eine Klasse machen würde verletzt.

1 Stimmen

@javawizard Wäre es einfach zu erklären, in einem Kommentar, was macht es nicht Thread-sicher, oder geben Sie einen Verweis, so dass ich darüber irgendwo anders lesen kann?

12 Stimmen

@Reti43 Angenommen, zwei Threads versuchen gleichzeitig, Käse zu erstellen, einer mit Cheese(0) und einer mit Cheese(1) . Es ist möglich, dass Thread 1 ausgeführt wird cls.__init__ = cls.foomethod , aber dann könnte Thread 2 laufen cls.__init__ = cls.barmethod bevor Thread 1 weitergeht. Beide Threads rufen dann am Ende barmethod was nicht das ist, was Sie wollen.

4voto

Michel Samia Punkte 3953

Ich würde die Vererbung nutzen. Vor allem, wenn es mehr Unterschiede als nur die Anzahl der Löcher geben wird. Vor allem, wenn Gouda einen anderen Satz von Mitgliedern haben muss als Parmesan.

class Gouda(Cheese):
    def __init__(self):
        super(Gouda).__init__(num_holes=10)

class Parmesan(Cheese):
    def __init__(self):
        super(Parmesan).__init__(num_holes=15)

1 Stimmen

Vererbung könnte angemessen sein, aber das ist eigentlich eine andere Frage als die, um die es hier geht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X