Jak uniknąć „self.x = x; self.y = y; self.z = z ”wzorzec w __init__?

170

Widzę takie wzory

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

dość często, często z dużo większą liczbą parametrów. Czy istnieje dobry sposób na uniknięcie tego typu żmudnej powtarzalności? Czy namedtuplezamiast tego klasa powinna dziedziczyć ?

MaxB
źródło
31
Nie każda wrażliwość jest zła. Należy pamiętać, że model klas Pythona nie zawiera jawnej definicji atrybutów instancji, więc te przypisania są samodokumentującymi się odpowiednikami.
chepner
4
@chepner: Cóż, nie wymaga wyraźnej definicji. Możesz jednak użyć __slots__do tego celu ; jest nieco niepytoniczny (bardziej rozwlekły, aby zaoszczędzić pamięć), ale w dużej mierze podoba mi się to, aby uniknąć ryzyka automatycznego ożywienia zupełnie nowego atrybutu, jeśli wpiszę nazwę.
ShadowRanger
2
Każdy dobry edytor będzie miał szablony. Piszesz ini <shortcut> x, y, z): <shortcut>i gotowe.
Gerenuk
3
Namedtuples są niesamowite, jeśli chcesz mieć niezmienny obiekt wartości. Jeśli chcesz mieć zwykłą, zmienną klasę, nie możesz ich używać.
RemcoGerlich
4
„Nie” jest dobrą opcją, każda dostępna opcja zabije sygnaturę metody (a tym samym potencjalnie cały interfejs). Poza tym, jeśli twoje klasy mają nieznośną liczbę pól do zainicjowania, możesz rozważyć podzielenie ich.
Kroltan

Odpowiedzi:

87

Edycja: jeśli masz Pythona 3.7+, po prostu użyj klas danych

Rozwiązanie dekoracyjne, które zachowuje podpis:

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
źródło
2
dobra odpowiedź, ale nie będzie działać z pythonem2.7: niesignature
MaxB
3
@alexis dekorator „decorator.decorator” automatycznie zawija funkcję
Siphor
4
Jestem dość rozdarty, czy to kochać, czy nienawidzić. Doceniam zachowanie podpisu.
Kyle Strand
14
„... Wyraźne jest lepsze niż niejawne. Proste jest lepsze niż złożone. ...” -Zen z Pythona
Jack Stout
9
-1 Szczerze mówiąc to jest okropne. Nie mam pojęcia, co robi ten kod na pierwszy rzut oka i jest to dosłownie dziesięć razy więcej niż ilość kodu. Bycie sprytnym jest fajne iw ogóle, ale jest to nadużycie twojej oczywistej sprytu.
Ian Newson,
108

Zastrzeżenie: Wygląda na to, że kilka osób jest zaniepokojonych przedstawieniem tego rozwiązania, więc przedstawię bardzo jasne zastrzeżenie. Nie powinieneś używać tego rozwiązania. Podaję je tylko jako informacje, więc wiesz, że język jest do tego zdolny. Reszta odpowiedzi to po prostu pokazanie możliwości językowych, a nie popieranie ich w ten sposób.


Nie ma nic złego w jawnym kopiowaniu parametrów do atrybutów. Jeśli masz zbyt wiele parametrów w ctor, czasami jest to uważane za zapach kodu i być może powinieneś zgrupować te parametry w mniejszą liczbę obiektów. Innym razem jest to konieczne i nie ma w tym nic złego. W każdym razie robienie tego wyraźnie jest drogą do zrobienia.

Ponieważ jednak pytasz JAK można to zrobić (a nie czy należy to zrobić), to jedno rozwiązanie jest takie:

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
źródło
16
dobra odpowiedź +1 ... chociaż self.__dict__.update(kwargs)może być nieco bardziej pytoniczna
Joran Beasley
44
Problem z tym podejściem polega na tym, że nie ma zapisu tego, jakich argumentów A.__init__faktycznie oczekuje i nie ma sprawdzania błędów pod kątem błędnie wpisanych nazw argumentów.
MaxB
7
@JoranBeasley Aktualizowanie słownika instancji na ślepo za pomocą kwargspowoduje otwarcie odpowiednika ataku polegającego na iniekcji SQL. Jeśli twój obiekt ma metodę o nazwie my_methodi przekazujesz argument o nazwie my_methoddo konstruktora, to update()słownik, po prostu nadpisałeś metodę.
Pedro
3
Jak powiedzieli inni, sugestia jest naprawdę kiepskim stylem programowania. Ukrywa kluczowe informacje. Możesz to pokazać, ale powinieneś wyraźnie zniechęcić OP do używania go.
Gerenuk
3
@Pedro Czy jest jakaś semantyczna różnica między składnią gruzczy i JoranBeasley?
gerrit
29

Jak wspominali inni, powtarzalność nie jest zła, ale w niektórych przypadkach nazwana dwukrotność może świetnie pasować do tego typu problemów. Pozwala to uniknąć używania locals () lub kwargs, które zwykle są złym pomysłem.

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

Znalazłem ograniczone zastosowanie, ale możesz dziedziczyć nazwany tuple, jak w przypadku każdego innego obiektu (kontynuacja przykładu):

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()
Mały skrypt powłoki
źródło
5
to krotki, więc twoją propertiesmetodę można zapisać jako just return tuple(self), co jest łatwiejsze w utrzymaniu, jeśli w przyszłości do definicji namedtuple zostanie dodanych więcej pól.
PaulMcG
1
Ponadto, twój ciąg deklaracji namedtuple nie wymaga przecinków między nazwami pól, XYZ = namedtuple("XYZ", "x y z")działa równie dobrze.
PaulMcG
Dzięki @PaulMcGuire. Próbowałem wymyślić naprawdę prosty dodatek, aby pokazać dziedziczenie i rodzaj odstępu. Masz 100% racji i jest to świetny skrót do innych dziedziczonych obiektów! Wspominam, że nazwy pól mogą być oddzielone przecinkami lub spacjami - wolę CSV od zwyczaju
A Small Shell Script
1
Często używam namedtuples do tego właśnie celu, szczególnie w kodzie matematycznym, w którym funkcja może być wysoce sparametryzowana i mieć kilka współczynników, które mają sens tylko razem.
detly
Problem namedtuplepolega na tym, że są one tylko do odczytu. Nie możesz tego zrobić abc.x += 1ani nic takiego.
hamstergene
29

jawne jest lepsze niż niejawne ... więc upewnij się, że możesz uczynić to bardziej zwięzłym:

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

Lepsze pytanie brzmi: czy powinieneś?

... to powiedziawszy, że jeśli chcesz nazwaną krotkę, polecam użycie nazwanej krotki (pamiętaj, że krotki mają określone warunki) ... być może chcesz nakazu lub nawet dykta ...

Joran Beasley
źródło
Wtedy obiekt będzie potrzebował cyklicznego czyszczenia pamięci, ponieważ ma sam siebie jako atrybut
John La Rooy,
3
@bernie (czy to bemie?), czasami ke r ning jest trudny
cat
4
W przypadku nieco bardziej wydajnych testów if k != "self":można go zmienić na if v is not self:tani test tożsamości, a nie porównanie ciągów. Wydaje mi się, że technicznie __init__można by go nazwać drugi raz po konstrukcji i przekazać selfjako kolejny argument, ale naprawdę nie chcę myśleć, jaki potwór by to zrobił. :-)
ShadowRanger
Które mogą być wykonane w funkcji, która przyjmuje wartość zwracaną locals: set_fields_from_locals(locals()). Wtedy to nie dłużej niż bardziej magiczne rozwiązania oparte na dekoratorach.
Lii
20

Aby rozwinąć gruszczyodpowiedź, użyłem wzoru takiego jak:

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

Podoba mi się ta metoda, ponieważ:

  • unika powtórzeń
  • jest odporny na literówki podczas konstruowania obiektu
  • działa dobrze z podklasami (może po prostu super().__init(...))
  • pozwala na dokumentację atrybutów na poziomie klasy (gdzie należą), a nie w X.__init__

W wersjach wcześniejszych niż Python 3.6 nie daje to żadnej kontroli nad kolejnością ustawiania atrybutów, co może być problemem, jeśli niektóre atrybuty są właściwościami z ustawieniami, które mają dostęp do innych atrybutów.

Prawdopodobnie można by to trochę poprawić, ale jestem jedynym użytkownikiem własnego kodu, więc nie martwię się żadną formą oczyszczania danych wejściowych. Być AttributeErrormoże bardziej odpowiedni byłby.

gerrit
źródło
10

Możesz też:

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

Oczywiście musiałbyś zaimportować inspectmoduł.

zondo
źródło
8

To rozwiązanie bez dodatkowego importu.

Funkcja pomocnicza

Mała funkcja pomocnicza sprawia, że ​​jest wygodniejszy i można go ponownie użyć:

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)

Podanie

Musisz zadzwonić z locals():

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

Test

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

Wynik:

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

Bez zmiany locals()

Jeśli nie chcesz zmieniać, locals()użyj tej wersji:

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
źródło
docs.python.org/2/library/functions.html#locals locals() nie powinny być modyfikowane (może to wpłynąć na interpreter, w twoim przypadku, usunięcie selfz zakresu funkcji wywołującej)
MaxB
@MaxB Z cytowanych dokumentów: ... zmiany mogą nie wpływać na wartości zmiennych lokalnych i wolnych używanych przez interpreter. selfjest nadal dostępny w __init__.
Mike Müller,
Prawo, czytelnik oczekuje, że wpływają na zmienne lokalne, ale może lub nie , w zależności od szeregu okoliczności. Chodzi o to, że to UB.
MaxB
Cytat: „Zawartość tego słownika nie powinna być modyfikowana”
MaxB,
@MaxB Dodałem wersję, która nie zmienia locals ().
Mike Müller,
7

Ciekawą biblioteką, która to obsługuje (i unika wielu innych schematów), są attrs . Na przykład Twój przykład można sprowadzić do tego (załóżmy, że klasa nazywa się MyClass):

import attr

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

Nie potrzebujesz __init__już nawet metody, chyba że robi ona również inne rzeczy. Oto miłe wprowadzenie Glypha Lefkowitza .

RafG
źródło
W jakim stopniu funkcjonalność jest attrzbędna dataclasses?
gerrit
1
@gerrit Jest to omówione w dokumentacji pakietu attrs . Tbh, różnice nie wydają się już tak duże.
Ivo Merchiers
5

Moje 0,02 $. Jest to bardzo zbliżone do odpowiedzi Jorana Beasleya, ale bardziej eleganckie:

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)

Dodatkowo odpowiedź Mike'a Müllera (najlepsza w moim guście) może zostać zredukowana za pomocą tej techniki:

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

I właśnie telefon auto_init(locals())od twojego__init__

bgusach
źródło
1
docs.python.org/2/library/functions.html#locals locals() nie powinny być modyfikowane (niezdefiniowane zachowanie)
MaxB, 11-16
4

To naturalny sposób robienia rzeczy w Pythonie. Nie próbuj wymyślać czegoś mądrzejszego, ponieważ doprowadzi to do zbyt sprytnego kodu, którego nikt z Twojego zespołu nie zrozumie. Jeśli chcesz być graczem zespołowym, pisz dalej w ten sposób.

Peter Krumins
źródło
4

Python 3.7 i nowsze

W Pythonie 3.7 możesz (nie) używać dataclassdekoratora, dostępnego w dataclassesmodule. Z dokumentacji:

Moduł ten udostępnia dekorator i funkcje do automatycznego dodawania wygenerowanych metod specjalnych, takich jak __init__()i __repr__()do klas zdefiniowanych przez użytkownika. Pierwotnie został opisany w PEP 557.

Zmienne składowe używane w tych generowanych metodach są definiowane przy użyciu adnotacji typu PEP 526. Na przykład ten kod:

@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

Doda między innymi a, __init__()który wygląda następująco:

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

Zauważ, że ta metoda jest automatycznie dodawana do klasy: nie jest bezpośrednio określona w definicji InventoryItem pokazanej powyżej.

Jeśli klasa jest duża i złożona, to może być niewłaściwe używanie dataclass. Piszę to w dniu wydania Pythona 3.7.0, więc wzorce użycia nie są jeszcze dobrze ugruntowane.

gerrit
źródło