web-dev-qa-db-ger.com

Mehrere Konstruktoren: der pythonische Weg?

Ich habe eine Containerklasse, die Daten enthält. Beim Erstellen des Containers gibt es verschiedene Methoden, um Daten zu übergeben.

  1. Übergeben Sie eine Datei, die die Daten enthält
  2. Übergeben Sie die Daten direkt über Argumente
  3. Daten nicht weitergeben; Erstellen Sie einfach einen leeren Behälter

In Java würde ich drei Konstruktoren erstellen. So würde es aussehen, wenn es in Python möglich wäre:

class Container:

    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

In Python sehe ich drei offensichtliche Lösungen, aber keine davon ist hübsch:

A: Schlüsselwortargumente verwenden:

def __init__(self, **kwargs):
    if 'file' in kwargs:
        ...
    Elif 'timestamp' in kwargs and 'data' in kwargs and 'metadata' in kwargs:
        ...
    else:
        ... create empty container

B: Standardargumente verwenden:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        ...
    Elif timestamp and data and metadata:
        ...
    else:
        ... create empty container

C: Nur Konstruktor zum Erstellen leerer Container angeben. Stellen Sie Methoden bereit, um Container mit Daten aus verschiedenen Quellen zu füllen.

def __init__(self):
    self.timestamp = 0
    self.data = []
    self.metadata = {}

def add_data_from_file(file):
    ...

def add_data(timestamp, data, metadata):
    ...

Die Lösungen A und B sind grundsätzlich gleich. Ich mache das if/else nicht gern, zumal ich überprüfen muss, ob alle für diese Methode erforderlichen Argumente angegeben wurden. A ist etwas flexibler als B, wenn der Code jemals um eine vierte Methode zum Hinzufügen von Daten erweitert werden soll.

Lösung C scheint die schönste zu sein, aber der Benutzer muss wissen, welche Methode er benötigt. Zum Beispiel: Er kann c = Container(args) nicht tun, wenn er nicht weiß, was args ist.

Was ist die Pythonic-Lösung?

57
Johannes

In Python können nicht mehrere Methoden mit demselben Namen vorhanden sein. Das Überladen von Funktionen wird - anders als in Java - nicht unterstützt.

Verwenden Sie Standardparameter oder **kwargs- und *args-Argumente.

Sie können statische Methoden oder Klassenmethoden mit dem Dekorator @staticmethod oder @classmethod erstellen, um eine Instanz Ihrer Klasse zurückzugeben oder andere Konstruktoren hinzuzufügen.

Ich rate dir zu tun:

class F:

    def __init__(self, timestamp=0, data=None, metadata=None):
        self.timestamp = timestamp
        self.data = list() if data is None else data
        self.metadata = dict() if metadata is None else metadata

    @classmethod
    def from_file(cls, path):
       _file = cls.get_file(path)
       timestamp = _file.get_timestamp()
       data = _file.get_data()
       metadata = _file.get_metadata()       
       return cls(timestamp, data, metadata)

    @classmethod
    def from_metadata(cls, timestamp, data, metadata):
        return cls(timestamp, data, metadata)

    @staticmethod
    def get_file(path):
        # ...
        pass

⚠ Niemals veränderliche Typen als Standardwerte in Python verwenden. ⚠ Siehe hier .

75
glegoux

Sie können nicht mehrere Konstruktoren haben, aber Sie können mehrere Factory-Methoden mit genau benannten Namen haben. 

class Document(object):

    def __init__(self, whatever args you need):
        """Do not invoke directly. Use from_NNN methods."""
        # Implementation is likely a mix of A and B approaches. 

    @classmethod
    def from_string(cls, string):
        # Do any necessary preparations, use the `string`
        return cls(...)

    @classmethod
    def from_json_file(cls, file_object):
        # Read and interpret the file as you want
        return cls(...)

    @classmethod
    def from_docx_file(cls, file_object):
        # Read and interpret the file as you want, differently.
        return cls(...)

    # etc.

Sie können den Benutzer jedoch nicht ohne weiteres daran hindern, den Konstruktor direkt zu verwenden. (Wenn dies kritisch ist, können Sie aus Sicherheitsgründen während der Entwicklung den Aufrufstapel im Konstruktor analysieren und prüfen, ob der Aufruf mit einer der erwarteten Methoden erfolgt.)

26
9000

Die meisten Pythonic-Programme sind die Funktionen der Python-Standardbibliothek. Kernentwickler Raymond Hettinger (der collections-Typ) hielt einen Vortrag dazu sowie allgemeine Richtlinien für das Schreiben von Klassen.

Verwenden Sie separate Funktionen auf Klassenebene, um Instanzen zu initialisieren, z. B. wie dict.fromkeys() kein Klasseninitialisierer ist, aber dennoch eine Instanz von dict zurückgibt. Dadurch können Sie flexibel auf die benötigten Argumente reagieren, ohne die Methodensignaturen ändern zu müssen, wenn sich die Anforderungen ändern.

16
Arya McCarthy

Was sind die Systemziele für diesen Code? Aus meiner Sicht lautet Ihr kritischer Ausdruck but the user has to know which method he requires. Welche Erfahrungen sollen Ihre Benutzer mit Ihrem Code haben? Das sollte das Interface-Design bestimmen.

Gehen Sie jetzt zur Wartbarkeit: Welche Lösung lässt sich am einfachsten lesen und warten? Auch hier finde ich, dass Lösung C minderwertig ist. Für die meisten Teams, mit denen ich zusammengearbeitet habe, ist Lösung B besser als A: Es ist etwas einfacher zu lesen und zu verstehen, obwohl beide leicht in kleine Codeblöcke zur Behandlung aufbrechen.

4
Prune

Ich bin nicht sicher, ob ich das richtig verstanden habe, aber würde das nicht funktionieren?

def __init__(self, file=None, timestamp=0, data=[], metadata={}):
    if file:
        ...
    else:
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata

Oder Sie könnten sogar tun:

def __init__(self, file=None, timestamp=0, data=[], metadata={}):
    if file:
        # Implement get_data to return all the stuff as a Tuple
        timestamp, data, metadata = f.get_data()

    self.timestamp = timestamp
    self.data = data
    self.metadata = metadata

Dank der Empfehlung von Jon Kiparsky gibt es einen besseren Weg, globale Deklarationen zu data und metadata zu vermeiden. Dies ist also der neue Weg:

def __init__(self, file=None, timestamp=None, data=None, metadata=None):
    if file:
        # Implement get_data to return all the stuff as a Tuple
        with open(file) as f:
            timestamp, data, metadata = f.get_data()

    self.timestamp = timestamp or 0
    self.data = data or []
    self.metadata = metadata or {}
4
Gabriel Ecker

Wenn Sie Python 3.4 oder höher verwenden, können Sie dies mit dem functools.singledispatch -Dekorator tun (mit etwas zusätzlicher Hilfe des methoddispatch-Dekors, den @ZeroPiraeus für seine Antwort schrieb):

class Container:

    @methoddispatch
    def __init__(self):
        self.timestamp = 0
        self.data = []
        self.metadata = {}

    @__init__.register(File)
    def __init__(self, file):
        f = file.open()
        self.timestamp = f.get_timestamp()
        self.data = f.get_data()
        self.metadata = f.get_metadata()

    @__init__.register(Timestamp)
    def __init__(self, timestamp, data, metadata):
        self.timestamp = timestamp
        self.data = data
        self.metadata = metadata
3
Sean Vieira

Die Pythonic-Methode besteht darin, sicherzustellen, dass optionale Argumente über Standardwerte verfügen. Fügen Sie also alle Argumente hinzu, die Sie benötigen, und weisen Sie ihnen entsprechende Standardwerte zu. 

def __init__(self, timestamp=None, data=[], metadata={}):
    timestamp = time.now()

Es ist wichtig zu beachten, dass alle erforderlichen Argumente nicht Standardwerte haben sollten, da ein Fehler ausgelöst werden soll, wenn sie nicht enthalten sind.

Sie können noch mehr optionale Argumente akzeptieren, indem Sie *args und **kwargs am Ende Ihrer Argumentliste verwenden.

def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
    if 'something' in kwargs:
        # do something
0
Soviut