Plusieurs constructeurs: le Pythonic? [dupliquer]

cette question a déjà une réponse ici:

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. passer les données directement via les arguments
  3. Ne pas transmettre les données; il suffit de créer un conteneur vide

en Java, je créerais trois constructeurs. Voici à quoi ça ressemblerait si C'é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 aucune n'est jolie:

A : utilisation d'arguments de mots clés:

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 : utilisant les 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 : fournir seulement un constructeur pour créer des conteneurs vides. Fournir des méthodes pour remplir les contenants à l'aide de 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 essentiellement les mêmes. Je n'aime pas faire le SI / AUTREMENT, d'autant plus que je dois vérifier si tous les arguments requis pour cette méthode ont été fournis. A est un peu plus flexible que B si le code doit jamais être étendu par une quatrième méthode pour ajouter des données.

Solution C semble être le plus beau, mais l'utilisateur doit savoir quelle méthode il exige. Par exemple: il ne peut pas faire c = Container(args) s'il ne sait pas ce qu'est args .

Quelle est la solution la plus pythonique?

54
demandé sur smci 2017-06-26 20:40:52

7 réponses

vous ne pouvez pas avoir plusieurs méthodes avec le même nom dans Python . La surcharge de la fonction-contrairement à Java - n'est pas supportée.

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

vous pouvez faire des méthodes statiques ou des méthodes de classe avec le décorateur @staticmethod ou @classmethod pour retourner une instance de votre classe, ou pour ajouter d'autres constructeurs.

je vous conseille de 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 de type mutable comme par défaut en python. ⚠ Voir ici .

72
répondu glegoux 2017-06-26 23:40:32

vous ne pouvez pas avoir plusieurs constructeurs, mais vous pouvez avoir plusieurs méthodes d'usine correctement 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.

Vous ne pouvez pas facilement empêcher l'utilisateur d'utiliser le constructeur directement. (Si c'est critique, comme précaution de sécurité pendant le développement, vous pouvez analyser la pile d'appels dans le constructeur et vérifier que l'appel est fait à partir de l'une des méthodes attendues.)

25
répondu 9000 2017-07-17 12:01:59

la plupart de Pythonic serait ce que la bibliothèque standard de Python fait déjà. Le développeur principal Raymond Hettinger (le collections guy) a donné un exposé sur ce , plus des directives générales pour la façon d'écrire des classes.

utilise des fonctions séparées au niveau de la classe pour initialiser les instances, comme dict.fromkeys() n'est pas la classe initialiseur, mais retourne quand même une instance de dict . Cela vous permet d'être flexible vers les arguments dont vous avez besoin sans changer les signatures de la méthode au fur et à mesure que les exigences changent.

16
répondu Arya McCarthy 2017-07-17 14:29:31

Quels sont les objectifs du système pour ce code? De mon point de vue, votre phrase critique est but the user has to know which method he requires. quelle expérience voulez-vous que vos utilisateurs aient avec votre code? Ça devrait être le moteur de la conception de l'interface.

maintenant, passez à la maintenabilité: quelle solution est la plus facile à lire et à maintenir? Encore une fois, je pense que la solution C est inférieure. Pour la plupart des équipes avec lesquelles j'ai travaillé, la solution B est préférable à la solution A: elle est un peu plus facile à lire et à comprendre, bien que tous deux cassent facilement en petits blocs de code pour le traitement.

4
répondu Prune 2017-06-26 17:46:39

Je ne sais pas si j'ai bien compris, mais ça ne marcherait pas?

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

Ou vous pouvez même le 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

merci à Jon Kiparsky Conseil Il ya une meilleure façon d'éviter les déclarations globales sur data et metadata donc c'est la nouvelle façon:

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
répondu Gabriel Ecker 2017-06-26 18:21:02

si vous êtes sur Python 3.4+ vous pouvez utiliser le functools.singledispatch décorateur pour faire ceci (avec un peu d'aide supplémentaire du methoddispatch décorateur que @ZeroPiraeus a écrit pour sa réponse ):

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
répondu Sean Vieira 2017-06-28 04:52:11

la façon la plus pythonique est de s'assurer que tous les arguments optionnels ont des valeurs par défaut. Incluez donc tous les arguments dont vous savez que vous avez besoin et assignez-leur les valeurs par défaut appropriées.

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

une chose importante à retenir est que tous les arguments requis devraient et non avoir des défauts puisque vous voulez qu'une erreur soit soulevée si elles ne sont pas incluses.

vous pouvez accepter encore plus d'arguments optionnels en utilisant *args et **kwargs à la fin de votre liste d'arguments.

def __init__(self, timestamp=None, data=[], metadata={}, *args, **kwards):
    if 'something' in kwargs:
        # do something
0
répondu Soviut 2017-06-26 17:53:42