web-dev-qa-db-fra.com

Comment éviter le motif "self.x = x; self.y = y; self.z = z" dans __init__?

Je vois des modèles comme

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

assez souvent, souvent avec beaucoup plus de paramètres. Existe-t-il un bon moyen d'éviter ce type de répétition fastidieuse? La classe devrait-elle hériter de namedtuple à la place?

169
MaxB

Edit: Si vous avez python 3.7+, utilisez simplement dataclasses

Une solution de décorateur qui conserve la signature:

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = Tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = Tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in Zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))
89
Siphor

Disclaimer: Il semble que plusieurs personnes s’inquiètent de la présentation de cette solution, je vais donc fournir un disclaimer très clair. Vous ne devriez pas utiliser cette solution. Je ne le fournis qu'à titre d'information, pour que vous sachiez que le langage en est capable. Le reste de la réponse montre simplement les capacités linguistiques, et ne recommande pas de les utiliser de cette manière.


Il n'y a rien de mal à copier explicitement des paramètres dans des attributs. Si vous avez trop de paramètres dans le ctor, cela est parfois considéré comme une odeur de code et vous devriez peut-être regrouper ces paramètres dans un nombre moins élevé d'objets. D'autres fois, c'est nécessaire et il n'y a rien de mal à cela. Quoi qu'il en soit, le faire explicitement est la voie à suivre.

Cependant, puisque vous demandez COMMENT cela peut être fait (et non si cela devrait être fait), alors une solution est la suivante:

class A:
    def __init__(self, **kwargs):
        for key in kwargs:
          setattr(self, key, kwargs[key])

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2
108
gruszczy

Comme d'autres l'ont déjà mentionné, la répétition n'est pas mauvaise, mais dans certains cas, un nom nommé peut être un choix judicieux pour ce type de problème. Cela évite d'utiliser locals () ou kwargs, qui sont généralement une mauvaise idée.

from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")

# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z

J'ai trouvé un usage limité pour cela, mais vous pouvez hériter d'un nom nommé comme avec n'importe quel autre objet (exemple):

class MySuperXYZ(XYZ):
    """ I add a helper function which returns the original properties """
    def properties(self):
        return self.x, self.y, self.z

abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()
29

explicite vaut mieux qu'implicite ... vous pouvez donc le rendre plus concis:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

La meilleure question est devrait vous?

... cela dit que si vous voulez un tuple nommé, je vous recommanderais d'utiliser un nommé (vous rappelez que les n-uplets sont assortis de certaines conditions) ... peut-être voulez-vous un ordre ou une dictée ...

29
Joran Beasley

Pour développer la réponse de gruszczys, j’ai utilisé un modèle tel que:

class X:
    x = None
    y = None
    z = None
    def __init__(self, **kwargs):
        for (k, v) in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise TypeError('Unknown keyword argument: {:s}'.format(k))

J'aime cette méthode car elle:

  • évite les répétitions
  • est résistant aux fautes de frappe lors de la construction d'un objet
  • fonctionne bien avec les sous-classes (peut juste super().__init(...))
  • permet de documenter les attributs au niveau de la classe (là où ils appartiennent) plutôt que dans X.__init__

Avant Python 3.6, cela ne donne aucun contrôle sur l'ordre dans lequel les attributs sont définis, ce qui pourrait poser problème si certains attributs sont des propriétés avec des paramètres qui accèdent à d'autres attributs.

Cela pourrait probablement être un peu amélioré, mais je suis le seul utilisateur de mon propre code et je ne m'inquiète donc d'aucune forme d'assainissement des entrées. Peut-être qu'un AttributeError serait plus approprié.

20
gerrit

Vous pouvez aussi faire:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

Bien entendu, vous devrez importer le module inspect.

10
zondo

C'est une solution sans aucune importation supplémentaire.

Fonction d'assistance

Une petite fonction d'aide le rend plus pratique et réutilisable:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    self = local_name_space.pop('self')
    for name, value in local_name_space.items():
        setattr(self, name, value)

Application

Vous devez l'appeler avec locals():

class A:
    def __init__(self, x, y, z):
        auto_init(locals())

Tester

a = A(1, 2, 3)
print(a.__dict__)

Sortie:

{'y': 2, 'z': 3, 'x': 1}

Sans changer locals()

Si vous n'aimez pas changer locals(), utilisez cette version:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    for name, value in local_name_space.items():
        if name != 'self': 
            setattr(local_name_space['self'], name, value)
8
Mike Müller

Une bibliothèque intéressante qui gère cela (et évite beaucoup d'autres types de passe-partout) est attrs . Votre exemple, par exemple, pourrait être réduit à ceci (supposons que la classe s'appelle MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

Vous n'avez même pas besoin d'un __init__ _ _ _ _ _ _ _ _ _. Voici ne belle introduction de Glyph Lefkowitz .

7
RafG

Mon 0.02 $. C'est très proche de la réponse de Joran Beasley, mais plus élégante:

def __init__(self, a, b, c, d, e, f):
    vars(self).update((k, v) for k, v in locals().items() if v is not self)

De plus, la réponse de Mike Müller (la meilleure à mon goût) peut être réduite avec cette technique:

def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

Et l'appel juste auto_init(locals()) à partir de votre __init__

5
bgusach

C'est une façon naturelle de faire les choses en Python. N'essayez pas d'inventer quelque chose de plus intelligent, cela conduirait à un code trop intelligent que personne ne comprendra. Si vous voulez être un joueur d'équipe et continuez à l'écrire de cette façon.

3
Peter Krumins

Python 3.7 et plus

Dans Python 3.7, vous pouvez (ab) utiliser le décorateur dataclass , disponible dans le module dataclasses. Dans la documentation:

Ce module fournit un décorateur et des fonctions permettant d’ajouter automatiquement des méthodes spéciales générées telles que __init__() et __repr__() à des classes définies par l’utilisateur. Il a été décrit à l'origine dans PEP 557.

Les variables membres à utiliser dans ces méthodes générées sont définies à l'aide d'annotations de type PEP 526. Par exemple ce code:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Ajoutera, entre autres choses, une __init__() ressemblant à ceci:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
      self.name = name
      self.unit_price = unit_price
      self.quantity_on_hand = quantity_on_hand

Notez que cette méthode est automatiquement ajoutée à la classe: elle n'est pas spécifiée directement dans la définition InventoryItem présentée ci-dessus.

Si votre classe est grande et complexe, il est inapproprié d'utiliser un dataclass. J'écris ceci le jour de la sortie de Python 3.7.0, donc les modèles d'utilisation ne sont pas encore bien établis.

3
gerrit