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

170

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 à la namedtupleplace?

MaxB
la source
31
Toute réceptivité n'est pas 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.
chepner
4
@chepner: Eh bien, ne nécessite pas de définition explicite. Vous pouvez cependant utiliser __slots__à cette fin ; c'est légèrement impythonique (plus verbeux pour économiser de la mémoire), mais je l'aime largement pour éviter le risque d'auto-vivifier un tout nouvel attribut si je tape sur le nom.
ShadowRanger
2
Tout bon éditeur aura des modèles. Vous tapez ini <shortcut> x, y, z): <shortcut>et vous avez terminé.
Gerenuk le
3
Namedtuples est génial, si vous voulez un objet de valeur immuable. Si vous voulez une classe régulière mutable, vous ne pouvez pas les utiliser.
RemcoGerlich le
4
"Ne pas" est une bonne option, toute option disponible tuera la signature de la méthode (et donc potentiellement toute l'interface). De plus, si vos classes ont une quantité insupportable de champs à initialiser, vous pouvez envisager de les fractionner.
Kroltan

Réponses:

87

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

Une solution décoratrice qui garde 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__))
Siphor
la source
2
belle réponse, mais ne fonctionnera pas avec python2.7: nonsignature
MaxB
3
@alexis le décorateur "decorator.decorator" encapsule automatiquement la fonction
Siphor
4
Je suis assez déchiré de savoir si je dois aimer ça ou le détester. J'apprécie la préservation de la signature.
Kyle Strand
14
"... Explicite vaut mieux qu'implicite. Simple est mieux que complexe. ..." -Zen of Python
Jack Stout
9
-1 Franchement, c'est horrible. Je n'ai aucune idée de ce que fait ce code en un coup d'œil, et c'est littéralement dix fois la quantité de code. Être intelligent est cool et tout, mais c'est une mauvaise utilisation de votre intelligence évidente.
Ian Newson
108

Clause de non-responsabilité: Il semble que plusieurs personnes soient préoccupées par la présentation de cette solution, je vais donc fournir une clause de non-responsabilité très claire. Vous ne devez pas utiliser cette solution. Je ne le donne qu'à titre d'information, vous savez donc que la langue en est capable. Le reste de la réponse montre simplement les capacités linguistiques, et non leur utilisation de cette manière.


Il n'y a vraiment 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 moins 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 doit ê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
gruszczy
la source
16
bonne réponse +1 ... mais self.__dict__.update(kwargs)pourrait être légèrement plus pythonique
Joran Beasley
44
Le problème avec cette approche est qu'il n'y a aucun enregistrement de ce à quoi les arguments A.__init__s'attendent réellement, et aucune vérification d'erreur pour les noms d'argument mal saisis.
MaxB
7
@JoranBeasley La mise à jour aveugle du dictionnaire d'instances avec kwargsvous laisse ouvert à l'équivalent d'une attaque par injection SQL. Si votre objet a une méthode nommée my_methodet que vous passez un argument nommé my_methodau constructeur, alors update()le dictionnaire, vous venez de remplacer la méthode.
Pedro
3
Comme d'autres l'ont dit, la suggestion est un style de programmation vraiment médiocre. Il cache des informations cruciales. Vous pouvez le montrer, mais vous devez explicitement décourager l'OP de l'utiliser.
Gerenuk le
3
@Pedro Y a-t-il une différence sémantique entre la syntaxe de gruzczy et de JoranBeasley?
gerrit
29

Comme d'autres l'ont mentionné, la répétition n'est pas mauvaise, mais dans certains cas, un multiplet nommé peut convenir parfaitement à ce type de problème. Cela évite d'utiliser des locaux () ou des 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'en ai trouvé une utilisation limitée, mais vous pouvez hériter d'un nommé comme avec tout autre objet (suite de l'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()
Un petit script Shell
la source
5
Ce sont des tuples, donc votre propertiesméthode peut être écrite comme juste return tuple(self), ce qui est plus facile à gérer si, à l'avenir, plus de champs sont ajoutés à la définition de namedtuple.
PaulMcG
1
En outre, votre chaîne de déclaration de nom de tableau ne nécessite pas de virgule entre les noms de champ, XYZ = namedtuple("XYZ", "x y z")fonctionne tout aussi bien.
PaulMcG
Merci @PaulMcGuire. J'essayais de penser à un add-on vraiment simple pour montrer l'héritage et une sorte d'espacement à ce sujet. Vous avez raison à 100% et c'est aussi un excellent raccourci avec d'autres objets hérités! Je mentionne que les noms de champ peuvent être séparés par des virgules ou des espaces - Je préfère CSV par habitude
Un petit script Shell
1
J'utilise souvent namedtuples dans ce but précis, en particulier dans le code mathématique où une fonction peut être hautement paramétrée et avoir un tas de coefficients qui n'ont de sens qu'ensemble.
detly le
Le problème avec namedtupleest qu'ils sont en lecture seule. Vous ne pouvez pas faire abc.x += 1ou quelque chose comme ça.
hamstergene
29

explicite vaut mieux qu'implicite ... alors vous pouvez 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: devriez-vous?

... cela dit, si vous voulez un tuple nommé, je recommanderais d'utiliser un nommé tuple (rappelez-vous que certaines conditions sont attachées aux tuples) ... peut-être voulez-vous un ordonné ou même juste un dict ...

Joran Beasley
la source
Ensuite, l'objet aura besoin d'un ramasse-miettes cyclique puisqu'il a lui-même comme attribut
John La Rooy
3
@bernie (ou est-ce bemie?), parfois la clé est difficile
chat
4
Pour des tests légèrement plus efficaces, if k != "self":pourrait être remplacé par if v is not self:un test d'identité bon marché, plutôt que par une comparaison de chaînes. Je suppose que techniquement, cela __init__pourrait être appelé une seconde fois après la construction et être passé selfcomme argument ultérieur, mais je ne veux vraiment pas penser à quel genre de monstre ferait cela. :-)
ShadowRanger
Cela pourrait être transformé en une fonction qui prend la valeur de retour locals: set_fields_from_locals(locals()). Alors ce n'est plus que les solutions plus magiques basées sur les décorateurs.
Lii
20

Pour développer la gruszczyréponse, j'ai utilisé un modèle comme:

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 la répétition
  • résiste aux fautes de frappe lors de la construction d'un objet
  • fonctionne bien avec le sous-classement (peut juste super().__init(...))
  • permet la documentation des 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 pouvait 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, donc je ne suis pas inquiet de toute forme d'assainissement des entrées. Un AttributeErrorserait peut-être plus approprié.

gerrit
la source
10

Vous pouvez également faire:

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

Bien sûr, vous devrez importer le inspectmodule.

Zondo
la source
8

C'est une solution sans importation supplémentaire.

Fonction d'assistance

Une petite fonction d'assistance 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__)

Production:

{'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)
Mike Müller
la source
docs.python.org/2/library/functions.html#locals locals() ne doit pas être modifié (cela peut affecter l'interpréteur, dans votre cas, la suppression selfde la portée de la fonction appelante)
MaxB
@MaxB D'après les documents que vous citez: ... les modifications peuvent ne pas affecter les valeurs des variables locales et libres utilisées par l'interpréteur. selfest toujours disponible en __init__.
Mike Müller
Exactement, le lecteur s'attend à ce que cela affecte les variables locales, mais cela peut ou non , selon un certain nombre de circonstances. Le fait est que c'est UB.
MaxB
Citation: "Le contenu de ce dictionnaire ne doit pas être modifié"
MaxB
@MaxB J'ai ajouté une version qui ne change pas les locaux ().
Mike Müller
7

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

import attr

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

Vous n'avez même plus besoin d'une __init__méthode, à moins qu'elle ne fasse également d'autres choses. Voici une belle introduction de Glyph Lefkowitz .

RafG
la source
Dans quelle mesure la fonction de attrest-elle redondante dataclasses?
gerrit le
1
@gerrit Ceci est discuté dans la documentation du paquet attrs . Tbh, les différences ne semblent plus si grandes.
Ivo Merchiers
5

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

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 le juste appel auto_init(locals())de votre__init__

bgusach
la source
1
docs.python.org/2/library/functions.html#locals locals() ne doit pas être modifié (comportement non défini)
MaxB
4

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

Peter Krumins
la source
4

Python 3.7 et versions ultérieures

En Python 3.7, vous pouvez (ab) utiliser le dataclassdécorateur, disponible depuis le dataclassesmodule. De la documentation:

Ce module fournit un décorateur et des fonctions pour ajouter automatiquement des méthodes spéciales générées telles que __init__()et __repr__()aux classes définies par l'utilisateur. Il a été initialement décrit 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, un __init__()qui ressemble à:

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 directement spécifiée dans la définition InventoryItem ci-dessus.

Si votre classe est nombreuse et complexe, il peut être inapproprié d'utiliser un fichier 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.

gerrit
la source