171 votes

Comment éviter le modèle "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 fréquemment, 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 ?

31 votes

La réceptivité n'est pas toujours mauvaise. Gardez à l'esprit que le modèle de classe de Python n'inclut pas de définition explicite des attributs d'instance, donc ces affectations sont les équivalents auto-documentés.

4 votes

@chepner : Eh bien, n'est-ce pas exiger définition explicite. Vous pouvez utiliser __slots__ pour l'objectif cependant C'est légèrement impythique (plus verbeux pour économiser de la mémoire), mais je l'aime surtout pour éviter le risque d'auto-vivifier un tout nouvel attribut si je fais une faute de frappe dans le nom.

2 votes

Tout bon éditeur aura des modèles. Vous tapez ini <shortcut> x, y, z): <shortcut> et vous avez terminé.

108voto

gruszczy Points 14097

Avis de non-responsabilité : Il semble que plusieurs personnes soient préoccupées par la présentation de cette solution. Je vais donc fournir un avertissement très clair. Vous ne devez pas utiliser cette solution. Je ne la donne qu'à titre d'information, pour que vous sachiez que le langage est capable de faire cela. Le reste de la réponse ne fait que montrer les capacités du langage, et ne recommande pas de l'utiliser de cette manière.


Il n'y a pas vraiment de problème à 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 plus restreint d'objets. D'autres fois, c'est nécessaire et il n'y a rien de mal à cela. Quoi qu'il en soit, il faut le faire de manière explicite.

Toutefois, puisque vous demandez COMMENT cela peut être fait (et non si cela doit être fait), voici une solution :

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

16 votes

Bonne réponse +1 ... bien que self.__dict__.update(kwargs) pourrait être un peu plus pythonique

45 votes

Le problème avec cette approche est qu'il n'y a pas de trace des arguments. A.__init__ attend réellement, et aucune vérification d'erreur pour les noms d'arguments mal saisis.

7 votes

@JoranBeasley Mettre à jour le dictionnaire d'instance aveuglément avec kwargs vous laisse ouvert à l'équivalent d'une attaque par injection SQL. Si votre objet possède une méthode nommée my_method et vous passez un argument nommé my_method au constructeur, alors update() le dictionnaire, vous venez d'écraser la méthode.

87voto

Siphor Points 2188

Modifier : Si vous avez python 3.7+, utilisez simplement classes de données

Une solution décorative 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__))

2 votes

Bonne réponse, mais ne fonctionnera pas avec python2.7 : non signature

0 votes

Il ne faut pas simple_init en tant que décorateur, retourner la fonction décorée au lieu de l'appeler ?

3 votes

@alexis le décorateur "decorator.decorator" enveloppe automatiquement la fonction

29voto

Comme d'autres l'ont mentionné, la répétition n'est pas mauvaise, mais dans certains cas, un namedtuple peut être une bonne solution 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

Je n'en ai trouvé qu'un usage limité, mais vous pouvez hériter d'un namedtuple comme de tout autre objet (exemple poursuivi) :

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

5 votes

Ces sont tuples, donc votre properties peut s'écrire comme suit return tuple(self) qui est plus facile à maintenir si, à l'avenir, d'autres champs sont ajoutés à la définition du namedtuple.

1 votes

De plus, la chaîne de déclaration de votre namedtuple ne nécessite pas de virgules entre les noms de champs, XYZ = namedtuple("XYZ", "x y z") fonctionne tout aussi bien.

0 votes

Merci @PaulMcGuire. J'essayais de penser à un complément très simple pour montrer l'héritage et j'ai un peu hésité sur ce point. Vous avez raison à 100% et c'est un excellent raccourci avec d'autres objets hérités, aussi ! Je mentionne que les noms de champs peuvent être séparés par des virgules ou des espaces -- je préfère CSV par habitude.

29voto

Joran Beasley Points 28451

L'explicite est mieux que l'implicite ... alors vous pourriez certainement 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 de savoir si vous devez le faire.

... ceci dit, si vous voulez un tuple nommé, je vous recommande d'utiliser un namedtuple (rappelez-vous que les tuples ont certaines conditions attachées à eux) ... peut-être voulez-vous un ordereddict ou même simplement un dict ...

0 votes

Alors l'objet aura besoin d'un ramassage cyclique des ordures puisqu'il a lui-même comme attribut

3 votes

@bernie (ou c'est bemie ?), parfois ke r ning est dur

4 votes

Pour des tests un peu plus efficaces, if k != "self": pourrait être changé en if v is not self: , test d'identité bon marché, plutôt que la comparaison de chaînes de caractères. Je suppose que techniquement __init__ pourrait être appelé une deuxième fois après la construction et passer. self comme argument ultérieur, mais je ne veux vraiment pas penser à quel genre de monstre pourrait faire ça :-)

21voto

gerrit Points 1588

Pour développer gruszczy J'ai utilisé un modèle comme celui-là :

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 parce qu'elle :

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

Avant Python 3.6, cela ne permet pas de contrôler l'ordre dans lequel les attributs sont définis, ce qui peut poser un problème si certains attributs sont des propriétés avec des setters qui accèdent à d'autres attributs.

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

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