Come evitare "self.x = x; self.y = y; self.z = z "pattern in __init__?


170

Vedo schemi simili

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

abbastanza frequentemente, spesso con molti più parametri. C'è un buon modo per evitare questo tipo di noiosa ripetitività? La classe dovrebbe ereditare namedtupleinvece?


31
Non tutta la ricettività è cattiva. Tieni presente che il modello di classe di Python non include la definizione esplicita degli attributi di istanza, quindi queste assegnazioni sono gli equivalenti autocompensanti.
Chepner

4
@chepner: beh, non richiede una definizione esplicita. Puoi usare __slots__allo scopo però ; è leggermente non ritmico (più prolisso per ottenere risparmi di memoria), ma mi piace in gran parte evitare il rischio di auto-vivificazione di un attributo completamente nuovo se scrivo il nome.
ShadowRanger

2
Ogni buon editor avrà modelli. Digiti ini <shortcut> x, y, z): <shortcut>e hai finito.
Gerenuk,

3
Le Namedtuples sono fantastiche, se vuoi un oggetto di valore immutabile. Se vuoi una classe normale e mutevole, non puoi usarli.
RemcoGerlich,

4
"Non" è una buona opzione, qualsiasi opzione disponibile ucciderà la firma del metodo (e quindi potenzialmente l'intera interfaccia). Inoltre, se le tue classi hanno un numero insopportabile di campi da inizializzare, potresti prendere in considerazione l'idea di dividerle.
Kroltan,

Risposte:


87

Modifica: se si dispone di Python 3.7+, utilizzare solo le dataclass

Una soluzione di decorazione che mantiene la firma:

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
bella risposta, ma non funzionerà con python2.7: nosignature
MaxB

3
@alex è il decoratore "decorator.decorator" che avvolge automaticamente la funzione
Siphor

4
Sono abbastanza lacerato se amare questo o odiarlo. Apprezzo molto la conservazione della firma.
Kyle Strand,

14
"... Esplicito è meglio che implicito. Semplice è meglio che complesso. ..." -Zen of Python
Jack Stout

9
-1 Francamente questo è orribile. Non ho idea di cosa stia facendo questo codice a colpo d'occhio, ed è letteralmente dieci volte la quantità di codice. Essere intelligenti è fantastico e tutto, ma questo è un uso improprio della tua evidente intelligenza.
Ian Newson,

108

Disclaimer: sembra che diverse persone siano preoccupate per la presentazione di questa soluzione, quindi fornirò un disclaimer molto chiaro. Non dovresti usare questa soluzione. Lo fornisco solo come informazione, quindi sai che la lingua è in grado di farlo. Il resto della risposta sta solo mostrando le capacità linguistiche, non approvando il loro utilizzo in questo modo.


Non c'è davvero nulla di sbagliato nel copiare esplicitamente i parametri negli attributi. Se hai troppi parametri nel ctor, a volte viene considerato un odore di codice e forse dovresti raggruppare questi parametri in meno oggetti. Altre volte, è necessario e non c'è nulla di sbagliato in questo. Ad ogni modo, farlo esplicitamente è la strada da percorrere.

Tuttavia, poiché stai chiedendo COME può essere fatto (e non se dovrebbe essere fatto), una soluzione è questa:

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
buona risposta +1 ... anche se self.__dict__.update(kwargs)potrebbe essere leggermente più pitonica
Joran Beasley

44
Il problema con questo approccio è che non esiste alcuna registrazione di ciò che gli argomenti si A.__init__aspettano effettivamente e nessun errore durante la verifica dei nomi degli argomenti errati.
MaxB

7
@JoranBeasley L'aggiornamento cieco del dizionario delle istanze kwargsti lascia aperto all'equivalente di un attacco di iniezione SQL. Se il tuo oggetto ha un metodo chiamato my_methode passi un argomento chiamato my_methodal costruttore, quindi update()al dizionario, hai appena sovrascritto il metodo.
Pedro

3
Come altri hanno detto, il suggerimento è uno stile di programmazione davvero scadente. Nasconde informazioni cruciali. Puoi mostrarlo, ma dovresti scoraggiare esplicitamente l'OP dal usarlo.
Gerenuk,

3
@Pedro C'è qualche differenza semantica tra la sintassi di gruzczy e quella di JoranBeasley?
Gerrit,

29

Come altri hanno già detto, la ripetizione non è male, ma in alcuni casi una coppia con nome può adattarsi perfettamente a questo tipo di problema. Questo evita di usare locals () o kwargs, che di solito sono una cattiva idea.

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

Ho trovato un uso limitato per questo, ma puoi ereditare una namedtuple come con qualsiasi altro oggetto (esempio continua):

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
Queste sono tuple, quindi il tuo propertiesmetodo può essere scritto come giusto return tuple(self), il che è più gestibile se in futuro verranno aggiunti più campi alla definizione di namedtuple.
PaulMcG

1
Inoltre, la stringa di dichiarazione namedtuple non richiede virgole tra i nomi dei campi, XYZ = namedtuple("XYZ", "x y z")funziona altrettanto bene.
PaulMcG

Grazie @PaulMcGuire. Stavo cercando di pensare a un componente aggiuntivo molto semplice per mostrare l'eredità e il tipo di spaziatura su quello. Hai ragione al 100% ed è anche un'ottima scorciatoia con altri oggetti ereditati! Cito i nomi dei campi che possono essere separati da virgola o spazio - Preferisco CSV dall'abitudine
A Small Shell Script

1
Uso spesso namedtuples per questo preciso scopo, specialmente nel codice matematico in cui una funzione potrebbe essere altamente parametrizzata e avere un sacco di coefficienti che hanno senso solo insieme.
detenere il

Il problema namedtupleè che sono di sola lettura. Non puoi fare abc.x += 1niente del genere.
hamstergene,

29

esplicito è meglio di implicito ... quindi sicuro che potresti renderlo più conciso:

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

La domanda migliore è: dovresti?

... detto questo se vuoi una tupla nominata ti consiglierei di usare una tupla nominata (ricorda che le tuple hanno determinate condizioni ad esse associate) ... forse vuoi un ordine nuziale o anche solo un dict ...


Quindi l'oggetto avrà bisogno della raccolta dei rifiuti ciclica poiché ha se stesso come attributo
John La Rooy,

3
@bernie (o è bemie?), a volte ke n ning è difficile
cat

4
Per test leggermente più efficienti, if k != "self":potrebbe essere modificato in if v is not self:, test di identità economico, piuttosto che confronto delle stringhe. Suppongo che tecnicamente __init__potrebbe essere chiamato una seconda volta dopo la costruzione e passato selfcome argomento successivo, ma davvero non voglio pensare a che tipo di mostro lo farebbe. :-)
ShadowRanger

Questo potrebbe essere fatto in una funzione che assume il valore di ritorno locals: set_fields_from_locals(locals()). Quindi non è più lungo delle soluzioni più magiche basate sul decoratore.
Lii,

20

Per espandere gruszczyla risposta, ho usato un modello come:

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

Mi piace questo metodo perché:

  • evita la ripetizione
  • è resistente agli errori di battitura durante la costruzione di un oggetto
  • funziona bene con la sottoclasse (può solo super().__init(...))
  • consente la documentazione degli attributi a livello di classe (a cui appartengono) anziché in X.__init__

Prima di Python 3.6, questo non dà alcun controllo sull'ordine in cui sono impostati gli attributi, il che potrebbe essere un problema se alcuni attributi sono proprietà con setter che accedono ad altri attributi.

Probabilmente potrebbe essere migliorato un po ', ma sono l'unico utente del mio codice, quindi non sono preoccupato per qualsiasi forma di risanamento dell'input. Forse un AttributeErrorsarebbe più appropriato.


10

Puoi anche fare:

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

Ovviamente, dovresti importare il inspectmodulo.


8

Questa è una soluzione senza ulteriori importazioni.

Funzione di aiuto

Una piccola funzione di supporto lo rende più comodo e riutilizzabile:

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)

Applicazione

Devi chiamarlo con locals():

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

Test

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

Produzione:

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

Senza cambiare locals()

Se non ti piace cambiare locals()usa questa versione:

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)

docs.python.org/2/library/functions.html#locals locals() non deve essere modificato (può influire sull'interprete, nel tuo caso, rimuovendolo selfdall'ambito della funzione chiamante)
MaxB

@MaxB Dai documenti citati: ... le modifiche potrebbero non influire sui valori delle variabili locali e libere utilizzate dall'interprete. selfè ancora disponibile in __init__.
Mike Müller,

Bene, il lettore si aspetta che influisca sulle variabili locali, ma può o meno , a seconda di una serie di circostanze. Il punto è che è UB.
MaxB

Citazione: "Il contenuto di questo dizionario non deve essere modificato"
MaxB

@MaxB Ho aggiunto una versione che non cambia i locali ().
Mike Müller,

7

Una libreria interessante che gestisce questo (ed evita molte altre piastre di caldaia) è attr . Il tuo esempio, ad esempio, potrebbe essere ridotto a questo (supponiamo che la classe sia chiamata MyClass):

import attr

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

Non hai nemmeno più bisogno di un __init__metodo, a meno che non faccia anche altre cose. Ecco una bella introduzione di Glyph Lefkowitz .


In che misura la funzionalità del attrreso è ridondante dataclasses?
Gerrit,

1
@gerrit Questo è discusso nella documentazione del pacchetto attrs . Tbh, le differenze non sembrano più così grandi.
Ivo Merchiers,

5

I miei 0,02 $. È molto vicino alla risposta di Joran Beasley, ma più elegante:

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)

Inoltre, la risposta di Mike Müller (la migliore per i miei gusti) può essere ridotta con questa tecnica:

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

E la giusta chiamata auto_init(locals())dal tuo__init__


1
docs.python.org/2/library/functions.html#locals locals() non deve essere modificato (comportamento indefinito)
MaxB

4

È un modo naturale di fare le cose in Python. Non tentare di inventare qualcosa di più intelligente, porterà a un codice troppo intelligente che nessuno nella tua squadra capirà. Se vuoi diventare un giocatore di squadra e continuare a scriverlo in questo modo.


4

Python 3.7 in poi

In Python 3.7, puoi (ab) usare il dataclassdecoratore, disponibile dal dataclassesmodulo. Dalla documentazione:

Questo modulo fornisce un decoratore e funzioni per l'aggiunta automatica di metodi speciali generati come __init__()e __repr__()alle classi definite dall'utente. È stato originariamente descritto in PEP 557.

Le variabili membro da utilizzare in questi metodi generati sono definite usando le annotazioni di tipo PEP 526. Ad esempio questo codice:

@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

Aggiungerà, tra le altre cose, un __init__()aspetto simile a:

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

Si noti che questo metodo viene automaticamente aggiunto alla classe: non viene specificato direttamente nella definizione InventoryItem mostrata sopra.

Se la tua classe è ampia e complessa, potrebbe essere inappropriato usare a dataclass. Sto scrivendo questo il giorno del rilascio di Python 3.7.0, quindi i modelli di utilizzo non sono ancora ben definiti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.