65 votes

Plusieurs constructeurs: à la manière pythonique?

J'ai une classe de conteneur qui contient des données. Lorsque le conteneur est créé, il existe différentes méthodes pour transmettre des données.

  1. Passer un fichier qui contient les données
  2. Transmettre les données directement via des arguments
  3. Ne pas transmettre les données; il suffit de créer un conteneur vide

En Java, je voudrais créer trois constructeurs. Voici à quoi il ressemblerait s'il était possible en Python:

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

En Python, je vois trois solutions évidentes, mais aucun d'eux n'est assez:

Un: à l'Aide de mots clés arguments:

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: à l'Aide d'arguments par défaut:

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

C: Seulement de fournir le constructeur pour créer des conteneurs vides. Fournir des méthodes pour remplir des récipients avec des données provenant de différentes sources.

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

def add_data_from_file(file):
    ...

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

Les Solutions A et B sont fondamentalement les mêmes. Je n'aime pas faire de la si/d'autre, surtout depuis que je dois vérifier si tous les arguments nécessaires pour cette méthode ont été fournis. Un est un peu plus souple que B si le code n'est jamais à être prolongée par une quatrième méthode pour ajouter des données.

La Solution C semble être le plus beau, mais l'utilisateur doit connaître la méthode qu'il exige. Par exemple: il ne peux pas faire de c = Container(args) si il ne sait pas ce qu' args est.

Quel est le plus Pythonic solution?

82voto

glegoux Points 2469

Vous ne pouvez pas avoir plusieurs méthodes avec le même nom en Python. Surcharge de fonctions - contrairement à l' Java - n'est pas pris en charge.

Utiliser les paramètres par défaut ou **kwargs et *args arguments.

Vous pouvez prendre les méthodes statiques ou les méthodes de la classe avec l' @staticmethod ou @classmethod décorateur à retourner une instance de votre classe, ou pour ajouter d'autres constructeurs.

Je vous conseille de le faire:

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

⚠ N'ont jamais mutable types de valeurs par défaut de python. ⚠ Voir ici.

30voto

9000 Points 13242

Vous ne pouvez pas avoir plusieurs constructeurs, mais vous pouvez avoir plusieurs méthodes de fabrique bien nommées.

 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.
 

Cependant, vous ne pouvez pas empêcher l'utilisateur d'utiliser directement le constructeur. (Si cela est essentiel, par mesure de sécurité pendant le développement, vous pouvez analyser la pile d'appels du constructeur et vérifier que l'appel est effectué à l'aide de l'une des méthodes attendues.)

17voto

Arya McCarthy Points 4554

La plupart des Pythonic serait ce que le Python standard library déjà fait. Core developer Raymond Hettinger (l' collections guy) a donné une conférence sur ce, en plus des lignes directrices générales pour la façon d'écrire des classes.

L'utilisation séparée, au niveau de la classe de fonctions pour initialiser les instances, comme la façon dont dict.fromkeys() n'est pas la classe d'initialiseur mais renvoie toujours une instance de dict. Cela vous permet de souplesse à l'égard des arguments que vous avez besoin sans avoir à changer de méthode de signatures que les besoins changent.

4voto

Prune Points 4656

Quels sont les objectifs de ce code? De mon point de vue, vos critiques phrase est - but the user has to know which method he requires. de Ce que l'expérience voulez-vous vos utilisateurs d'avoir avec votre code? Qui doit conduire à la conception de l'interface.

Maintenant, placez-vous à la maintenabilité: quelle solution est la plus facile à lire et à maintenir? Encore une fois, j'ai l'impression que la solution C est de qualité inférieure. Pour la plupart des équipes avec qui j'ai travaillé, la solution B est préférable à Un: c'est un peu plus facile à lire et à comprendre, bien que les deux facilement se casser en petits blocs de code pour le traitement.

3voto

Gabriel Ecker Points 107

Je ne suis pas sûr d'avoir bien compris mais cela ne fonctionnerait-il pas?

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

Ou vous pourriez même faire:

 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
 

Grâce aux conseils de Jon Kiparsky, il existe un meilleur moyen d’éviter les déclarations globales sur data et metadata donc le nouveau moyen:

 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 {}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X