Automatycznie zainicjować zmienne instancji?


89

Mam klasę Pythona, która wygląda następująco:

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

śledzony przez:

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

Czy istnieje sposób na automatyczne zainicjowanie tych zmiennych instancji, takich jak lista inicjalizacyjna C ++? Pozwoliłoby to zaoszczędzić mnóstwo zbędnego kodu.


1
Zobacz także dyskusję na autoassigntemat przepisu na status aktywacji i alternatywnej autoargsimplementacji pod adresem: Jaki jest najlepszy sposób automatycznego przypisywania atrybutów w Pythonie i czy jest to dobry pomysł? - Przepełnienie stosu
nealmcb

Odpowiedzi:


104

Możesz użyć dekoratora:

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

Użyj go do dekoracji __init__metody:

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

Wynik:

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

5
To działa i odpowiedz na pytanie, więc zagłosowałem. Ale zatrzymałem odpowiedź Ferdidanda Beyera: „Wyraźne jest lepsze niż ukryte”
Lucas Gabriel Sánchez

14
+1 za świetną odpowiedź, która rozwiązała mój problem. Ale czy nie powinno to być podstawową funkcjonalnością języka? Myślisz, że warto napisać PEP?
Adam Matan,

3
To naprawdę dobra odpowiedź - trafiła prosto do mojego zestawu narzędzi.
Michael van der Westhuizen

3
@NadiaAlramli Myślę, że w kodzie jest mały błąd. Zajrzyj tutaj: gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99

2
Bieżący przykład ma błąd i nie zadziała, jeśli podpis nie zawiera domyślnych argumentów. Musisz uwzględnić sprawdzenie, aby upewnić się, że nazwy i wartości domyślne nie są Brak. Np .: jeśli nazwy i wartości domyślne:

36

Jeśli używasz Pythona 2.6 lub nowszego, możesz użyć collections.namedtuple :

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

Jest to odpowiednie zwłaszcza wtedy, gdy twoja klasa to po prostu duży worek wartości.


2
„Jest to właściwe zwłaszcza wtedy, gdy twoja klasa to po prostu duży worek wartości”. W takim scenariuszu możesz również zrobić to: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie

34

W przypadku Pythona 3.7+ możesz użyć klasy danych , która jest bardzo Pythonowym i łatwym w utrzymaniu sposobem robienia tego, co chcesz.

Umożliwia definiowanie pól dla Twojej klasy, które są automatycznie inicjowanymi zmiennymi instancji.

Wyglądałoby to mniej więcej tak:

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

Ta __init__metoda będzie już w Twojej klasie.

Zauważ, że tutaj wymagane jest podpowiedź typu , dlatego użyłem intiw strprzykładzie. Jeśli nie znasz typu swojego pola, możesz użyć Any z typingmodułu .

Klasa Data ma wiele zalet w porównaniu z proponowanymi rozwiązaniami:

  • Jest to jasne : wszystkie pola są widoczne, co szanuje Zen Pythona i sprawia, że ​​jest czytelny i łatwy w utrzymaniu. Porównaj to z użyciem **kwargs.
  • Może mieć metody . Jak każda inna klasa.
  • To pozwala wyjść poza automatyczny __init__za pomocą __post_init__metody.

EDYCJA: powody, dla których należy unikać używania NamedTuples

Niektórzy sugerują użycie w namedtupletym przypadku, ale nazwane krotki mają pewne zachowania różniące się od klas Pythona, które na początku nie są tak naprawdę oczywiste i powinny być dobrze znane:

1. NamedTuples są niezmienne

Niezmienność może być przydatna, ale może nie jest tym, czego chcesz dla swoich instancji. DataClasses mogą być również jakoś niezmienne jeśli użyć argumentu frozen=Truena @dataclassdekoratora.

2. NamedTuples __eq__zachowuje się jak Tuple

W Pythonie SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)jest True! __eq__Funkcją NamedTuple, używane w porównaniach, tylko uważa, wartości i pozycje tych wartości na porównywanych przypadkach, a nie nazwy swojej klasie lub pól.


Powinno być używane tylko wtedy, gdy celem klasy jest przechowywanie danych.
JC Rocamonde

Lub rozwijać wokół przechowywania danych.
JC Rocamonde,

3
Czy mógłbyś wyjaśnić, dlaczego klasa danych powinna być używana tylko dla klas, które przechowują dane, zamiast mieć również inne zachowanie? Mogę stworzyć nowy wpis SO dla tego całkowicie, aby lepiej zrozumieć odpowiednie przypadki użycia. Dzięki.
Vahid Pazirandeh

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

26

Cytując Zen Pythona ,

Jawne jest lepsze niż niejawne.


10
Czy deklaracja listy inicjalizacyjnej nie byłaby wystarczająco wyraźna?
Adam Matan,

Zgaduję. Ale nie widzę powodu, by dodawać coś takiego do języka. Wyraźnie wolę wielokrotne przypisania zadań niż jakiś rodzaj dekoratorskiej magii za kulisami.
Ferdinand Beyer

29
@Ferdinand, zgadzam się, że byłoby głupio mieć w języku coś, co może być doskonale w standardowej bibliotece, ale POWINNO być w standardowej bibliotece, ponieważ „piękne jest lepsze niż brzydkie” ma pierwszeństwo, a wiele powtarzających się przypisań jest brzydkich (jak każda forma powtórzenia).
Alex Martelli,

Cóż, kontratak: DWIM> DWIS
Terrence Brannon

Zgadzam się, że dekorator jest piękniejszy niż lista zadań, ale PyCharm sprawia, że ​​jest brzydszy, nie rozumiejąc go :-(
user110954

23

Kolejna rzecz, którą możesz zrobić:

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`

Ale jedynym rozwiązaniem, które bym polecił, poza tylko przeliterowaniem, jest „utwórz makro w swoim edytorze” ;-p


1
Dobry chwyt w usuwaniu „siebie”.
michael

15

Możesz to łatwo zrobić za pomocą argumentów słów kluczowych, np. W ten sposób:

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

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

podobna implementacja argumentów pozycyjnych byłaby następująca:

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


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

co według mnie nie rozwiązuje twojego problemu.


3
Inną odmianą, że lubię toself.__dict__.update( **kwargs )
S.Lott

Równie dobrze można użyć locals () i wstawić normalne argumenty.
mk12

7

Rozwiązanie Nadii jest lepsze i mocniejsze, ale myślę, że to też jest interesujące:

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

Potrzebowałem czegoś w tym samym celu, ale żadna z istniejących odpowiedzi nie obejmowała wszystkich przypadków, które przetestowałem. Odpowiedź Nadii była najbliższa temu, czego szukałem, więc zacząłem od jej kodu jako bazy.

Poniższy dekorator działa ze wszystkimi poprawnymi kombinacjami argumentów:

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        )

Implementuje również standardową _konwencję -prefix, umożliwiającą używanie __init__-private zmiennych, które nie zostaną przypisane do instancji klas.


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

Uwaga:

Dodałem testy, ale zwinąłem je do ostatniej linii ( 58 ) dla zwięzłości. Możesz rozszerzyć testy, które szczegółowo opisują wszystkie potencjalne przypadki użycia, find/replaceusuwając wszystkie $znaki znakiem nowej linii.


3

Może nie być potrzeby inicjowania zmiennych, ponieważ locals () już zawiera wartości!

class Dummy (obiekt):

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

d = Dummy (2, 3)

d.params

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

d.params [„b”]

3

Oczywiście na zajęciach można używać self.params


Jest to ładne i oryginalne podejście, ale d['b']jest napisane w języku Python lingua franca, podczas gdy d.params['b']spowoduje zamieszanie dla czytelników kodu.
Adam Matan,

3

Gdy tylko getargspeczostanie uznany za przestarzały od Pythona 3.5, oto rozwiązanie wykorzystujące 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

Sprawdź, czy działa:

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

W przypadku Pythona 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

Próbny:

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

Podejście bez dekoratora zarówno dla Pythona 2, jak i 3 przy użyciu ramek:

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)

Próbny:

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

1

nu11ptr stworzył mały moduł PyInstanceVars , który zawiera tę funkcjonalność jako dekorator funkcji. W pliku README modułu znajduje się stwierdzenie, że „ [...] wydajność jest teraz tylko o 30-40% gorsza niż jawna inicjalizacja w CPythonie ”.

Przykład użycia zaczerpnięty prosto z dokumentacji modułu :

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

Może to pytanie zamknięte, ale chciałbym zaproponować swoje rozwiązanie, aby wiedzieć, co o tym myślisz. Użyłem metaklasy, która stosuje dekorator do metody inicjowania

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

na końcu funkcji init :

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

Więcej informacji setattr()można znaleźć tutaj


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.