Inizializzare automaticamente le variabili di istanza?


90

Ho una classe Python che assomiglia a questa:

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):

seguito da:

        self.PID=PID
        self.PPID=PPID
        self.cmd=cmd
        ...

C'è un modo per inizializzare automaticamente queste variabili di istanza, come l'elenco di inizializzazione di C ++? Risparmierebbe un sacco di codice ridondante.


1
Vedere anche la discussione sulla autoassignricetta activestate e autoargsun'implementazione alternativa su: Qual è il modo migliore per eseguire l'assegnazione automatica degli attributi in Python, ed è una buona idea? - Stack Overflow
nealmcb

Risposte:


104

Puoi usare un decoratore:

from functools import wraps
import inspect

def initializer(func):
    """
    Automatically assigns the parameters.

    >>> class process:
    ...     @initializer
    ...     def __init__(self, cmd, reachable=False, user='root'):
    ...         pass
    >>> p = process('halt', True)
    >>> p.cmd, p.reachable, p.user
    ('halt', True, 'root')
    """
    names, varargs, keywords, defaults = inspect.getargspec(func)

    @wraps(func)
    def wrapper(self, *args, **kargs):
        for name, arg in list(zip(names[1:], args)) + list(kargs.items()):
            setattr(self, name, arg)

        for name, default in zip(reversed(names), reversed(defaults)):
            if not hasattr(self, name):
                setattr(self, name, default)

        func(self, *args, **kargs)

    return wrapper

Usalo per decorare il __init__metodo:

class process:
    @initializer
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        pass

Produzione:

>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
['FDs', 'PID', 'PPID', '__doc__', '__init__', '__module__', 'cmd', 'reachable', 'user'

5
Funziona e rispondi alla domanda, quindi ho votato. Ma ho mantenuto la risposta di Ferdidand Beyer: "Esplicito è meglio che implicito"
Lucas Gabriel Sánchez

14
+1 per un'ottima risposta che ha risolto il mio problema. Ma non dovrebbe essere una funzionalità fondamentale del linguaggio? Pensi che valga la pena scrivere un PEP?
Adam Matan

3
Questa è davvero una buona risposta: è finita direttamente nella mia cassetta degli attrezzi.
Michael van der Westhuizen

3
@NadiaAlramli Penso che ci sia un piccolo bug nel codice. Guarda qui gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99

2
L'esempio corrente ha un bug e non funzionerà se la firma non include argomenti predefiniti. È necessario includere un segno di spunta per assicurarsi che i nomi e i valori predefiniti non siano Nessuno. Es: se nomi e valori predefiniti:

36

Se stai usando Python 2.6 o versioni successive, puoi usare collections.namedtuple :

>>> from collections import namedtuple
>>> Process = namedtuple('Process', 'PID PPID cmd')
>>> proc = Process(1, 2, 3)
>>> proc.PID
1
>>> proc.PPID
2

Questo è appropriato soprattutto quando la tua classe è davvero solo un grande bagaglio di valori.


2
"Questo è appropriato soprattutto quando la tua classe è davvero solo una grande borsa di valori." In uno scenario del genere, potresti anche farlo: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie

35

Per Python 3.7+ puoi usare un file Data Class , che è un modo molto pitonico e gestibile per fare quello che vuoi.

Ti permette di definire campi per la tua classe, che sono le variabili di istanza inizializzate automaticamente.

Sarebbe qualcosa del genere:

@dataclass
class Process:
    PID: int
    PPID: int
    cmd: str
    ...

Il __init__ metodo sarà già nella tua classe.

Nota che qui è richiesto il suggerimento sul tipo , ecco perché ho usato inte strnell'esempio. Se non conosci il tipo di campo, puoi utilizzare Any dal filetyping modulo .

La Data Class presenta molti vantaggi rispetto alle soluzioni proposte:

  • È esplicito : tutti i campi sono visibili, il che rispetta lo Zen di Python e lo rende leggibile e manutenibile. Confrontalo con l'uso di **kwargs.
  • Può avere metodi . Proprio come qualsiasi altra classe.
  • Ti permette di andare oltre l'automatico __init__utilizzando il __post_init__metodo.

EDIT: motivi per evitare di utilizzare NamedTuples

Alcuni suggeriscono l'uso di namedtupleper questo caso, ma le namedtuples hanno alcuni comportamenti che differiscono dalle classi Python, che all'inizio non sono molto evidenti e dovrebbero essere ben noti:

1. NamedTuples non sono modificabili

L'immutabilità può essere utile, ma forse non è ciò che desideri per le tue istanze. DataClass può anche essere in qualche modo immutabile se usi l'argomento frozen=Truesul @dataclassdecoratore.

2. NamedTuples __eq__si comporta come Tuple

In Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)è True! La __eq__funzione di NamedTuple, utilizzata nei confronti, considera solo i valori e le posizioni di tali valori nelle istanze confrontate, non i nomi delle classi o dei campi.


Questo dovrebbe essere usato solo se lo scopo della classe è memorizzare i dati.
JC Rocamonde

O per sviluppare la memorizzazione dei dati.
JC Rocamonde

3
Spiegheresti perché dataclass dovrebbe essere usato solo per classi che memorizzano dati, piuttosto che avere anche altri comportamenti? Potrei creare un nuovo post SO per questo interamente per comprendere meglio i suoi casi d'uso appropriati. Grazie.
Vahid Pazirandeh

Data Classes can be thought of as "mutable namedtuples with defaults". - PEP557
aafulei

26

Citando lo Zen di Python ,

Esplicito è meglio che implicito.


10
Una dichiarazione di lista di inizializzazione non sarebbe abbastanza esplicita?
Adam Matan,

Suppongo. Ma non vedo un motivo per aggiungere qualcosa del genere alla lingua. Preferisco chiaramente più dichiarazioni di incarico a una sorta di magia da decoratore dietro le quinte.
Ferdinand Beyer,

30
@Ferdinand, sono d'accordo che sarebbe sciocco avere nella lingua qualcosa che può essere perfettamente nello stdlib, ma DOVREBBE essere nello stdlib, perché "bello è meglio di brutto" ha la precedenza e molti compiti ripetitivi sono brutti (come qualsiasi forma di ripetizione).
Alex Martelli

Bene, per contro: DWIM> DWIS
Terrence Brannon

Sono d'accordo che il decoratore è più bello di un elenco di compiti, ma PyCharm lo rende più brutto non
capendolo

24

Un'altra cosa che puoi fare:

class X(object):
    def __init__(self, a,b,c,d):
        vars = locals() # dict of local names
        self.__dict__.update(vars) # __dict__ holds and object's attributes
        del self.__dict__["self"] # don't need `self`

Ma l'unica soluzione che consiglierei, oltre a spiegarla semplicemente, è "crea una macro nel tuo editor" ;-p


1
Buona presa sull'eliminazione di "sé".
michael

15

Puoi farlo facilmente con gli argomenti delle parole chiave, ad esempio in questo modo:

>>> class D:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

>>> D(test='d').test
'd'

un'implementazione simile per gli argomenti posizionali sarebbe:

>> class C:
    def __init__(self, *args):
        self.t, self.d = args


>>> C('abc', 'def').t
'abc'
>>> C('abc', 'def').d
'def'

che a me non sembra risolvere il tuo problema.


3
Un'altra variazione che mi piace èself.__dict__.update( **kwargs )
S.Lott

Tanto vale usare locals () e inserire argomenti normali.
mk12

7

La soluzione di Nadia è migliore e più potente, ma penso che anche questa sia interessante:

def constructor(*arg_names):
  def __init__(self, *args):
    for name, val in zip(arg_names, args):
      self.__setattr__(name, val)
  return __init__


class MyClass(object):
  __init__ = constructor("var1", "var2", "var3")


>>> c = MyClass("fish", "cheese", "beans")
>>> c.var2
"cheese"

5

Avevo bisogno di qualcosa per lo stesso scopo, ma nessuna delle risposte esistenti copriva tutti i casi che ho testato. La risposta di Nadia era la più vicina a quello che stavo cercando, quindi ho iniziato con il suo codice come base.

Il decoratore di seguito funziona con tutte le combinazioni valide di argomenti:

Positional                                          __init__(self, a, b                )
Keyword                                             __init__(self, a=None, b=None      )
Positional + Keyword                                __init__(self, a, b, c=None, d=None)
Variable Positional                                 __init__(self, *a                  )
Variable Positional + Keyword                       __init__(self, *a, b=None          )
Variable Positional + Variable Keyword              __init__(self, *a, **kwargs        )
Positional + Variable Positional + Keyword          __init__(self, a, *b, c=None       )
Positional + Variable Positional + Variable Keyword __init__(self, a, *b, **kwargs     )
Keyword Only                                        __init__(self, *, a=None           )
Positional + Keyword Only                           __init__(self, a, *, b=None        )

Implementa anche la _convenzione standard -prefix per consentire __init__variabili -private che non verranno assegnate alle istanze di classe.


###  StdLib  ###
from functools import wraps
import inspect


###########################################################################################################################
#//////|   Decorator   |//////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def auto_assign_arguments(function):

  @wraps(function)
  def wrapped(self, *args, **kwargs):
    _assign_args(self, list(args), kwargs, function)
    function(self, *args, **kwargs)

  return wrapped


###########################################################################################################################
#//////|   Utils   |//////////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def _assign_args(instance, args, kwargs, function):

  def set_attribute(instance, parameter, default_arg):
    if not(parameter.startswith("_")):
      setattr(instance, parameter, default_arg)

  def assign_keyword_defaults(parameters, defaults):
    for parameter, default_arg in zip(reversed(parameters), reversed(defaults)):
      set_attribute(instance, parameter, default_arg)

  def assign_positional_args(parameters, args):
    for parameter, arg in zip(parameters, args.copy()):
      set_attribute(instance, parameter, arg)
      args.remove(arg)

  def assign_keyword_args(kwargs):
    for parameter, arg in kwargs.items():
      set_attribute(instance, parameter, arg)
  def assign_keyword_only_defaults(defaults):
    return assign_keyword_args(defaults)

  def assign_variable_args(parameter, args):
    set_attribute(instance, parameter, args)

  POSITIONAL_PARAMS, VARIABLE_PARAM, _, KEYWORD_DEFAULTS, _, KEYWORD_ONLY_DEFAULTS, _ = inspect.getfullargspec(function)
  POSITIONAL_PARAMS = POSITIONAL_PARAMS[1:] # remove 'self'

  if(KEYWORD_DEFAULTS     ): assign_keyword_defaults     (parameters=POSITIONAL_PARAMS,  defaults=KEYWORD_DEFAULTS)
  if(KEYWORD_ONLY_DEFAULTS): assign_keyword_only_defaults(defaults=KEYWORD_ONLY_DEFAULTS                          )
  if(args                 ): assign_positional_args      (parameters=POSITIONAL_PARAMS,  args=args                )
  if(kwargs               ): assign_keyword_args         (kwargs=kwargs                                           )
  if(VARIABLE_PARAM       ): assign_variable_args        (parameter=VARIABLE_PARAM,      args=args                )


###########################################################################################################################$#//////|   Tests   |//////////////////////////////////////////////////////////////////////////////////////////////////////#$###########################################################################################################################$$if __name__ == "__main__":$$#######|   Positional   |##################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2)$$#######|   Keyword   |#####################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a="KW_DEFAULT_1", b="KW_DEFAULT_2"):$      pass$$  t = T(a="kw_arg_1", b="kw_arg_2")$  assert (t.a == "kw_arg_1") and (t.b == "kw_arg_2")$$#######|   Positional + Keyword   |########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, c="KW_DEFAULT_1", d="KW_DEFAULT_2"):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, c="kw_arg_1")$  assert (t.a == 1) and (t.b == 2) and (t.c == "kw_arg_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, d="kw_arg_2")$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "kw_arg_2")$$#######|   Variable Positional   |#########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3])$$#######|   Variable Positional + Keyword   |###############################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, b="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3]) and (t.b == "KW_DEFAULT_1")$$  t = T(1, 2, 3, b="kw_arg_1")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1")$$#######|   Variable Positional + Variable Keyword   |######################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, **kwargs):$      pass$$  t = T(1, 2, 3, b="kw_arg_1", c="kw_arg_2")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1") and (t.c == "kw_arg_2")$$#######|   Positional + Variable Positional + Keyword   |##################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, c="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3, c="kw_arg_1")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1")$$#######|   Positional + Variable Positional + Variable Keyword   |#########################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, **kwargs):$      pass$$  t = T(1, 2, 3, c="kw_arg_1", d="kw_arg_2")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1") and (t.d == "kw_arg_2")$$#######|   Keyword Only   |################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *, a="KW_DEFAULT_1"):$      pass$$  t = T(a="kw_arg_1")$  assert (t.a == "kw_arg_1")$$#######|   Positional + Keyword Only   |###################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *, b="KW_DEFAULT_1"):$      pass$$  t = T(1)$  assert (t.a == 1) and (t.b == "KW_DEFAULT_1")$$  t = T(1, b="kw_arg_1")$  assert (t.a == 1) and (t.b == "kw_arg_1")$$#######|   Private __init__ Variables (underscored)   |####################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, _c):$      pass$$  t = T(1, 2, 3)$  assert hasattr(t, "a") and hasattr(t, "b") and not(hasattr(t, "_c"))

Nota:

Ho incluso dei test, ma li ho raggruppati nell'ultima riga ( 58 ) per brevità. Puoi espandere i test, che dettagliano tutti i potenziali casi d'uso, find/replaceinserendo tutti i $caratteri con una nuova riga.


3

Potrebbe non essere necessario inizializzare le variabili, poiché locals () contiene già i valori!

class Dummy (oggetto):

def __init__(self, a, b, default='Fred'):
    self.params = {k:v for k,v in locals().items() if k != 'self'}

d = fittizio (2, 3)

d.params

{'a': 2, 'b': 3, 'default': 'Fred'}

d.params ['b']

3

Naturalmente, all'interno di una classe si potrebbe usare self.params


È un approccio carino e originale, ma d['b']è scritto nella lingua franca di Python mentre d.params['b']causerà confusione ai lettori di codice.
Adam Matan

3

Non appena getargspecè deprecato da Python 3.5, ecco la soluzione che utilizza inspect.signature:

from inspect import signature, Parameter
import functools


def auto_assign(func):
    # Signature:
    sig = signature(func)
    for name, param in sig.parameters.items():
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            raise RuntimeError('Unable to auto assign if *args or **kwargs in signature.')
    # Wrapper:
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        for i, (name, param) in enumerate(sig.parameters.items()):
            # Skip 'self' param:
            if i == 0: continue
            # Search value in args, kwargs or defaults:
            if i - 1 < len(args):
                val = args[i - 1]
            elif name in kwargs:
                val = kwargs[name]
            else:
                val = param.default
            setattr(self, name, val)
        func(self, *args, **kwargs)
    return wrapper

Controlla se funziona:

class Foo(object):
    @auto_assign
    def __init__(self, a, b, c=None, d=None, e=3):
        pass

f = Foo(1, 2, d="a")
assert f.a == 1
assert f.b == 2
assert f.c is None
assert f.d == "a"
assert f.e == 3

print("Ok")

2

Per Python 3.3 e versioni successive:

from functools import wraps
from inspect import Parameter, signature


def instance_variables(f):
    sig = signature(f)
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        values = sig.bind(self, *args, **kwargs)
        for k, p in sig.parameters.items():
            if k != 'self':
                if k in values.arguments:
                    val = values.arguments[k]
                    if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
                        setattr(self, k, val)
                    elif p.kind == Parameter.VAR_KEYWORD:
                        for k, v in values.arguments[k].items():
                            setattr(self, k, v) 
                else:
                    setattr(self, k, p.default) 
    return wrapper

class Point(object):
    @instance_variables 
    def __init__(self, x, y, z=1, *, m='meh', **kwargs):
        pass

Demo:

>>> p = Point('foo', 'bar', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

Un approccio non decoratore sia per Python 2 che per 3 utilizzando i frame:

import inspect


def populate_self(self):
    frame = inspect.getouterframes(inspect.currentframe())[1][0]
    for k, v in frame.f_locals.items():
        if k != 'self':
            setattr(self, k, v)


class Point(object):
    def __init__(self, x, y):
        populate_self(self)

Demo:

>>> p = Point('foo', 'bar')
>>> p.x
'foo'
>>> p.y
'bar'

1

nu11ptr ha creato un piccolo modulo, PyInstanceVars , che include questa funzionalità come decoratore di funzioni. Nel README del modulo si afferma che "le [...] prestazioni sono ora solo del 30-40% peggiori dell'inizializzazione esplicita in CPython ".

Esempio di utilizzo, tratto direttamente dalla documentazione del modulo :

>>> from instancevars import *
>>> class TestMe(object):
...     @instancevars(omit=['arg2_'])
...     def __init__(self, _arg1, arg2_, arg3='test'):
...             self.arg2 = arg2_ + 1
...
>>> testme = TestMe(1, 2)
>>> testme._arg1
1
>>> testme.arg2_
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'TestMe' object has no attribute 'arg2_'
>>> testme.arg2
3
>>> testme.arg3
'test'

0

Forse questa è una domanda chiusa, ma vorrei proporre la mia soluzione per sapere cosa ne pensate. Ho usato una metaclasse che applica un decoratore al metodo init

import inspect

class AutoInit(type):
    def __new__(meta, classname, supers, classdict):
        classdict['__init__'] = wrapper(classdict['__init__'])
        return type.__new__(meta, classname, supers, classdict)

def wrapper(old_init):
    def autoinit(*args):
        formals = inspect.getfullargspec(old_init).args
        for name, value in zip(formals[1:], args[1:]):
            setattr(args[0], name, value)
    return autoinit


0

alla fine della funzione init :

for var in list(locals().keys()):
    setattr(self,var,locals()[var])

Per ulteriori setattr()informazioni, fare riferimento qui


0

Esiste una funzione di supporto per farlo nella libreria fastcore https://fastcore.fast.ai/utils.html#store_attr .

from fastcore.utils import store_attr

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        store_attr() # this will do the same as self.PID = PID etc.
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.