web-dev-qa-db-fra.com

Plusieurs constructeurs: à la manière pythonique?

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

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

En Java, je créerais trois constructeurs. Voici à quoi cela 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 aucune d’entre elles 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: Utilisation des 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: Fournit uniquement le constructeur pour créer des conteneurs vides. Fournir des méthodes pour remplir les conteneurs 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 le if/else, surtout depuis 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 être étendu par une quatrième méthode pour ajouter des données.

La solution C semble être la plus agréable, mais l'utilisateur doit savoir quelle méthode il a besoin. Par exemple: il ne peut pas utiliser c = Container(args) s'il ne sait pas ce que args est.

Quelle est la solution la plus Pythonic?

57
Johannes

Vous ne pouvez pas avoir plusieurs méthodes avec le même nom dans Python. La surcharge de fonctions - contrairement à Java - n'est pas prise en charge.

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

Vous pouvez créer des méthodes statiques ou des méthodes de classe avec le décorateur @staticmethod ou @classmethod pour renvoyer une instance de votre classe ou 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

⚠ Ne jamais avoir de types mutables comme valeurs par défaut en python. Voir ici .

75
glegoux

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.)

26
9000

La plupart des Pythonic seraient ce que la bibliothèque standard Python fait déjà. Le développeur principal, Raymond Hettinger (le type collections) a donné une conférence à ce sujet , ainsi que des directives générales sur la manière d’écrire des cours.

Utilisez des fonctions distinctes au niveau de la classe pour initialiser des instances, comme par exemple, comment dict.fromkeys() n'est pas l'initialiseur de la classe, mais renvoie néanmoins une instance de dict. Cela vous permet de faire preuve de souplesse quant aux arguments dont vous avez besoin sans modifier les signatures de méthode à mesure que les exigences changent.

16
Arya McCarthy

Quels sont les objectifs 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? Cela devrait guider la conception de l'interface.

Passons maintenant à 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, même si les deux se cassent facilement en petits blocs de code pour le traitement.

4
Prune

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 à Jon Kiparsky, il existe un meilleur moyen d’éviter les déclarations globales sur data et metadata. C’est 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 {}
4
Gabriel Ecker

Si vous êtes sur Python 3.4+, vous pouvez utiliser le functools.singledispatch décorateur pour le faire (avec un peu d'aide supplémentaire du décorateur methoddispatch que @ZeroPiraeus é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
Sean Vieira

La manière la plus pythonique est de s’assurer que tous les arguments optionnels ont des valeurs par défaut. Donc, incluez tous les arguments dont vous savez que vous avez besoin et attribuez-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 pas avoir des valeurs par défaut car vous voulez qu'une erreur soit générée s'ils ne sont pas inclus.

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
Soviut