Initialiser automatiquement les variables d'instance?


89

J'ai une classe python qui ressemble à ceci:

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

suivi par:

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

Existe-t-il un moyen de réinitialiser automatiquement ces variables d'instance, comme la liste d'initialisation de C ++? Cela épargnerait beaucoup de code redondant.


1
Voir aussi la discussion sur la autoassignrecette d'activestate et une autoargsimplémentation alternative sur: Quelle est la meilleure façon de faire l'attribution automatique d'attributs en Python, et est-ce une bonne idée? - Stack Overflow
nealmcb

Réponses:


104

Vous pouvez utiliser un décorateur:

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

Utilisez-le pour décorer la __init__méthode:

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

Production:

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

5
Cela fonctionne et répondez à la question, alors j'ai voté. Mais j'ai gardé la réponse de Ferdidand Beyer: "Explicite vaut mieux qu'implicite"
Lucas Gabriel Sánchez

14
+1 Pour une excellente réponse qui a résolu mon problème. Mais cela ne devrait-il pas être une fonctionnalité de base du langage? Pensez-vous que cela vaut la peine d'écrire un PEP?
Adam Matan

3
C'est une très bonne réponse - cela est entré directement dans ma boîte à outils.
Michael van der Westhuizen

3
@NadiaAlramli Je pense qu'il y a un petit bug dans le code. Regardez ici gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99

2
L'exemple actuel a un bogue et ne fonctionnera pas si la signature n'inclut pas d'arguments par défaut. Vous devez inclure une vérification pour vous assurer que les noms et les valeurs par défaut ne sont pas Aucun. Ex: si noms et valeurs par défaut:

36

Si vous utilisez Python 2.6 ou supérieur, vous pouvez utiliser collections.namedtuple :

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

Ceci est particulièrement approprié lorsque votre classe n'est en réalité qu'un grand sac de valeurs.


2
"Ceci est particulièrement approprié lorsque votre classe n'est en réalité qu'un grand sac de valeurs." Dans un tel scénario, vous pouvez également faire ceci: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie

34

Pour Python 3.7+, vous pouvez utiliser une classe de données , qui est un moyen très pythonique et maintenable de faire ce que vous voulez.

Il vous permet de définir des champs pour votre classe, qui sont vos variables d'instance automatiquement initialisées.

Cela ressemblerait à quelque chose comme ça:

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

La __init__méthode sera déjà dans votre classe.

Notez qu'ici, l' indication de type est requise , c'est pourquoi j'ai utilisé intet strdans l'exemple. Si vous ne connaissez pas le type de votre champ, vous pouvez utiliser Any du typingmodule .

La Data Class présente de nombreux avantages par rapport aux solutions proposées:

  • C'est explicite : tous les champs sont visibles, ce qui respecte le Zen de Python et le rend lisible et maintenable. Comparez-le à l'utilisation de **kwargs.
  • Il peut avoir des méthodes . Comme toute autre classe.
  • Il permet d'aller au-delà de l'automatique en __init__utilisant la __post_init__méthode.

EDIT: Raisons pour éviter d'utiliser NamedTuples

Certains suggèrent l'utilisation de namedtuplepour ce cas, mais les namedtuples ont des comportements qui diffèrent des classes Python, qui ne sont pas vraiment évidents au début et devraient être bien connus:

1. Les NamedTuples sont immuables

L'immuabilité peut être utile, mais ce n'est peut-être pas ce que vous voulez pour vos instances. Les DataClasses peuvent également être en quelque sorte immuables si vous utilisez l'argument frozen=Truesur le @dataclassdécorateur.

2. NamedTuples __eq__se comporte comme Tuple

En Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)c'est True! La __eq__fonction de NamedTuple, utilisée dans les comparaisons, ne considère que les valeurs et les positions de ces valeurs sur les instances comparées, pas leurs noms de classe ou de champs.


Cela ne doit être utilisé que si le but de la classe est de stocker des données.
JC Rocamonde le

Ou se développer autour du stockage de données.
JC Rocamonde le

3
Expliquez-vous pourquoi la classe de données devrait être utilisée uniquement pour les classes qui stockent des données, plutôt que d'avoir un autre comportement? Je pourrais créer un nouveau message SO pour cela afin de mieux comprendre ses cas d'utilisation appropriés. Merci.
Vahid Pazirandeh

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

26

Citant le Zen de Python ,

L'explicite vaut mieux que l'implicite.


10
Une déclaration de liste d'initialisation ne serait-elle pas assez explicite?
Adam Matan

Je suppose. Mais je ne vois pas de raison d'ajouter quelque chose comme ça à la langue. Je préfère clairement les déclarations de tâches multiples à une sorte de magie décorative dans les coulisses.
Ferdinand Beyer

29
@Ferdinand, je suis d'accord que ce serait idiot d'avoir dans la langue quelque chose qui peut parfaitement être dans la stdlib, mais, ça DEVRAIT être dans la stdlib, parce que "beau est mieux que laid" a la priorité, et de nombreuses tâches répétitives sont laides (comme toute forme de répétition).
Alex Martelli

Eh bien, pour contrer: DWIM> DWIS
Terrence Brannon

Je conviens que le décorateur est plus beau qu'une liste de missions mais PyCharm le rend plus laid en ne le comprenant pas :-(
user110954

23

Une autre chose que vous pouvez faire:

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`

Mais la seule solution que je recommanderais, en plus de le préciser, est de "créer une macro dans votre éditeur" ;-p


1
Bonne prise sur la suppression de «soi».
michael

15

Vous pouvez le faire facilement avec les arguments de mot-clé, par exemple comme ceci:

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

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

une implémentation similaire pour les arguments positionnels serait:

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


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

ce qui ne me semble pas résoudre votre problème.


3
Une autre variation que j'aime estself.__dict__.update( **kwargs )
S.Lott

Autant utiliser les locals () et mettre des arguments normaux.
mk12 le

7

La solution de Nadia est meilleure et plus puissante, mais je pense que c'est aussi intéressant:

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

J'avais besoin de quelque chose dans le même but, mais aucune des réponses existantes ne couvrait tous les cas que j'ai testés. La réponse de Nadia était la plus proche de ce que je cherchais, alors j'ai commencé avec son code comme base.

Le décorateur ci-dessous fonctionne avec toutes les combinaisons d'arguments valides:

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        )

Il implémente également la _convention standard -prefix pour autoriser les __init__variables -private qui ne seront pas affectées aux instances de 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"))

Remarque:

J'ai inclus des tests, mais je les ai regroupés dans la dernière ligne ( 58 ) par souci de concision. Vous pouvez étendre les tests, qui détaillent tous les cas d'utilisation potentiels, en find/replacemettant tous les $caractères avec une nouvelle ligne.


3

Il n'est peut-être pas nécessaire d'initialiser les variables, car locals () contient déjà les valeurs!

classe Dummy (objet):

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

d = factice (2, 3)

d.params

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

d.params ['b']

3

Bien sûr, dans une classe, on peut utiliser self.params


C'est une approche agréable et originale, mais elle d['b']est écrite dans la lingua franca de Python, tout d.params['b']en créant de la confusion pour les lecteurs de code.
Adam Matan

3

Dès qu'il getargspecest obsolète depuis Python 3.5, voici la solution utilisant 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

Vérifiez si fonctionne:

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

Pour Python 3.3+:

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

Démo:

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

Une approche non décoratrice pour Python 2 et 3 utilisant des cadres:

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)

Démo:

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

1

nu11ptr a créé un petit module, PyInstanceVars , qui inclut cette fonctionnalité en tant que décorateur de fonctions. Dans le fichier README du module, il est indiqué que « [...] les performances ne sont plus que 30 à 40% pires que l'initialisation explicite sous CPython ».

Exemple d'utilisation, tiré directement de la documentation du module :

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

C'est peut-être une question fermée, mais je voudrais proposer ma solution pour savoir ce que vous en pensez. J'ai utilisé une métaclasse qui applique un décorateur à la méthode 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

à la fin de la fonction init :

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

Pour en savoir plus, setattr()veuillez consulter ici


0

Il existe une fonction d'aide pour ce faire dans la lib 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.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.