Czy dobrą praktyką jest deklarowanie zmiennych instancji jako Brak w klasie w Pythonie?

68

Rozważ następującą klasę:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Moi współpracownicy określają to w następujący sposób:

class Person:
    name = None
    age = None

    def __init__(self, name, age):
        self.name = name
        self.age = age

Głównym tego powodem jest to, że ich wybrany edytor pokazuje właściwości autouzupełniania.

Osobiście nie lubię tego drugiego, ponieważ nie ma sensu, aby klasa miała ustawione właściwości None.

Który byłby lepszą praktyką i z jakich powodów?

Remco Haszing
źródło
62
Nigdy nie pozwól, aby twoje IDE dyktowało kod, który piszesz?
Martijn Pieters
14
Nawiasem mówiąc: użycie odpowiedniego IDE Pythona (np. PyCharm), ustawienie atrybutów w __init__już zapewnia autouzupełnianie itp. Również użycie Nonezapobiega temu, aby IDE wniosło lepszy typ atrybutu, więc lepiej zamiast tego użyć rozsądnej wartości domyślnej (kiedy możliwy).
Bakuriu
Jeśli jest to tylko funkcja autouzupełniania, możesz użyć podpowiedzi typu, a dodatkowe dokumenty będą dodatkowym atutem.
dashy
3
„Nigdy nie pozwól, aby IDE dyktowało kod, który piszesz” jest przedmiotem dyskusji. Począwszy od Pythona 3.6 znajdują się wbudowane adnotacje i typingmoduł, który pozwala ci dostarczać wskazówki do IDE i lintera, jeśli tego rodzaju rzeczy łaskoczą twoje fantazje ...
cz
1
Te przypisania na poziomie klasy nie mają wpływu na resztę kodu. Nie mają one wpływu na self. Nawet jeśli self.nameani self.agenie były przydzielane __init__oni by nie pokazać się na przykład self, tylko pokazać się w klasie Person.
jolvi

Odpowiedzi:

70

Nazywam tę drugą złą praktyką zasadą „nie robi się tak, jak myślisz”.

Pozycję twojego współpracownika można przepisać w następujący sposób: „Zamierzam stworzyć grupę statycznych quasi-globalnych zmiennych statycznych, do których nigdy nie ma dostępu, ale które zajmują miejsce w tabelach przestrzeni nazw różnych klas ( __dict__), tylko po to, aby moje IDE zrobiło coś."

StarWeaver
źródło
4
To trochę jak dokumenty ;-) Oczywiście, Python ma przydatny tryb, aby je rozebrać -OOdla tych, którzy tego potrzebują.
Steve Jessop
15
Zajęta przestrzeń jest zdecydowanie najmniej ważnym powodem, dla którego konwencja śmierdzi. Jest to kilkanaście bajtów na nazwę (na proces). Mam wrażenie, że zmarnowałem czas na przeczytanie części twojej odpowiedzi na ten temat, więc nieważne, ile kosztuje miejsce.
8
@delnan Zgadzam się, że rozmiar pamięci wpisów jest bez znaczenia, bardziej zastanawiałem się nad zajętym obszarem logicznym / mentalnym, wprowadzając introspekcyjne debugowanie i takie wymagają więcej czytania i sortowania, tak sądzę. I ~ LL
StarWeaver
3
Właściwie, gdybyś był paranoikiem kosmosu, zauważyłbyś, że to oszczędza przestrzeń i marnuje czas. Niezainicjowani członkowie używają wartości klasy i dlatego nie ma wielu kopii nazwy zmiennej zmapowanej na Brak (po jednej dla każdej instancji). Zatem koszt w klasie wynosi kilka bajtów, a oszczędności to kilka bajtów na instancję. Ale każde nieudane wyszukiwanie nazwy zmiennej kosztuje trochę czasu.
Jon Jay Obermark
2
Jeśli martwisz się o 8 bajtów, nie będziesz używać Pythona.
cz
26

1. Spraw, aby Twój kod był łatwy do zrozumienia

Kod jest odczytywany znacznie częściej niż pisany. Uprość zadanie opiekuna kodu (w przyszłym roku może to być Ty).

Nie wiem o żadnych twardych regułach, ale wolę, aby jakikolwiek stan przyszłej instancji był jasno określony. Upaść z AttributeErrorjest wystarczająco poważna. Gorzej widać brak jasności cyklu życia atrybutu instancji. Ilość gimnastyki umysłowej wymaganej do przywrócenia możliwych sekwencji wywołań, które prowadzą do przypisania atrybutu, może łatwo stać się trywialna, co prowadzi do błędów.

Zwykle więc nie tylko definiuję wszystko w konstruktorze, ale również staram się ograniczyć liczbę zmiennych do zminimalizowania.

2. Nie mieszaj członków klasy i instancji

Wszystko, co zdefiniujesz bezpośrednio w classdeklaracji, należy do klasy i jest wspólne dla wszystkich instancji klasy. Np. Kiedy zdefiniujesz funkcję wewnątrz klasy, staje się ona metodą, która jest taka sama dla wszystkich instancji. To samo dotyczy członków danych. Jest to całkowicie odmienne od atrybutów instancji, w których zwykle się definiuje __init__.

Elementy danych na poziomie klasy są najbardziej przydatne jako stałe:

class Missile(object):
  MAX_SPEED = 100  # all missiles accelerate up to this speed
  ACCELERATION = 5  # rate of acceleration per game frame

  def move(self):
    self.speed += self.ACCELERATION
    if self.speed > self.MAX_SPEED:
      self.speed = self.MAX_SPEED
    # ...
9000
źródło
2
Tak, ale mieszanie członków na poziomie klasy i instancji jest właściwie tym, co robi def . Tworzy funkcje, które uważamy za atrybuty obiektów, ale tak naprawdę są członkami klasy. To samo dotyczy własności i jej podobnych. Podawanie iluzji, że praca należy do przedmiotów, gdy klasa naprawdę zapośredniczy w niej, jest uświęconym czasem sposobem, aby nie zwariować. Jak może być tak źle, skoro sam Python jest w porządku.
Jon Jay Obermark
Cóż, kolejność rozwiązywania metod w Pythonie (dotyczy także członków danych) nie jest bardzo prosta. Czego nie można znaleźć na poziomie instancji, będą wyszukiwane na poziomie klasy, a następnie między klasami podstawowymi itp. Rzeczywiście można ukryć element członkowski na poziomie klasy (dane lub metodę), przypisując element członkowski o tej samej nazwie. Ale metody na poziomie instancji są powiązane z instancjami selfi nie muszą selfbyć przekazywane, podczas gdy metody na poziomie klasy są niezwiązane i są prostymi funkcjami, jak widać w defczasie, i akceptują instancję jako pierwszy argument. To są różne rzeczy.
9000
Myślę, że an AttributeErrorto dobry sygnał niż błąd. W przeciwnym razie połknąłbyś Brak i uzyskałbyś bezsensowne wyniki. Jest to szczególnie ważne w omawianym przypadku, w którym atrybuty są zdefiniowane w __init__, więc brakujący atrybut (ale istniejący na poziomie klasy) może być spowodowany jedynie błędnym dziedziczeniem.
Davidmh
@Davidmh: Rzeczywiście wykryty błąd jest zawsze lepszy niż błąd niewykryty! Powiedziałbym, że jeśli musisz utworzyć atrybut, Nonea ta wartość nie ma sensu w momencie budowy instancji, masz problem w swojej architekturze i musisz przemyśleć cykl życia wartości atrybutu lub jego wartości początkowej. Zauważ, że definiując swoje atrybuty wcześnie, możesz wykryć taki problem jeszcze przed napisaniem reszty klasy, nie mówiąc już o uruchomieniu kodu.
9000
Zabawa! pociski! w każdym razie jestem całkiem pewien, że można tworzyć zmienne na poziomie klasy i mieszać je ... dopóki poziom klasy zawiera wartości domyślne itp.
Erik Aronesty
18

Osobiście definiuję członków w metodzie __ init __ (). Nigdy nie myślałem o zdefiniowaniu ich w części klasowej. Ale to, co zawsze robię: inicjuję wszystkie elementy w metodzie __ init__, nawet te niepotrzebne w metodzie __ init__.

Przykład:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        self._selected = None

   def setSelected(self, value):
        self._selected = value

Myślę, że ważne jest zdefiniowanie wszystkich członków w jednym miejscu. Dzięki temu kod jest bardziej czytelny. Niezależnie od tego, czy znajduje się w __ init __ (), czy na zewnątrz, nie jest tak ważne. Ważne jest jednak, aby zespół zastosował mniej więcej ten sam styl kodowania.

Och, i możesz zauważyć, że kiedykolwiek dodałem przedrostek „_” do zmiennych składowych.

to. ja
źródło
13
Powinieneś ustawić wszystkie wartości w konstruktorze. Jeśli wartość została ustawiona w samej klasie, byłaby współdzielona między instancjami, co jest w porządku dla None, ale nie dla większości wartości. Więc niczego nie zmieniaj. ;)
Remco Haszing
4
Domyślne wartości parametrów oraz możliwość przyjmowania dowolnych argumentów pozycyjnych i słów kluczowych sprawia, że ​​jest to o wiele mniej bolesne, niż można by się spodziewać. (I w rzeczywistości możliwe jest przekazanie konstrukcji innym metodom lub nawet samodzielnym funkcjom, więc jeśli naprawdę potrzebujesz wielu wysyłek, możesz to zrobić).
Sean Vieira
6
@Benedict Python nie ma kontroli dostępu. Wiodącym podkreśleniem jest zaakceptowana konwencja dotycząca szczegółów implementacji. Zobacz PEP 8 .
Doval
3
@Doval Istnieje wyjątkowo dobry powód, aby poprzedzać nazwy atrybutów znakiem _: Aby wskazać, że jest prywatny! (Dlaczego tak wiele osób w tym wątku myli lub na wpół myli Python z innymi językami?)
1
Ach, więc jesteś jedną z tych właściwości-lub-funkcji-dla wszystkich odsłoniętych interakcji ... Przepraszam, że źle zrozumiałem. Ten styl zawsze wydaje mi się przesadny, ale ma następujące.
Jon Jay Obermark
11

To zła praktyka. Nie potrzebujesz tych wartości, one zaśmiecają kod i mogą powodować błędy.

Rozważać:

>>> class WithNone:
...   x = None
...   y = None
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> class InitOnly:
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> wn = WithNone(1,2)
>>> wn.x
1
>>> WithNone.x #Note that it returns none, no error
>>> io = InitOnly(1,2)
>>> InitOnly.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class InitOnly has no attribute 'x'
Daenyth
źródło
Trudno mi nazwać to „powodowaniem błędu”. Jest niejednoznaczne, co masz na myśli, prosząc o „x” tutaj, ale równie dobrze możesz chcieć najbardziej prawdopodobnej wartości początkowej.
Jon Jay Obermark
Powinienem był opracować, że może to spowodować błąd, jeśli zostanie użyty nieprawidłowo.
Daenyth
Wyższe ryzyko wciąż inicjuje się na obiekcie. Żadne z nich nie jest naprawdę bezpieczne. Jedynym błędem, jaki spowoduje, jest przełknięcie naprawdę kulawych wyjątków.
Jon Jay Obermark
1
Niepowodzenie spowodowania błędów jest problemem, jeśli błędy zapobiegłyby poważniejszym problemom.
Erik Aronesty
0

Pójdę z „trochę jak dokumentacja, a następnie” i stwierdzę, że jestNone to nieszkodliwe, o ile jest to zawsze , lub wąski zakres innych wartości, wszystkie niezmienne.

Cuchnie atawizmem i nadmiernym przywiązaniem do języków pisanych statycznie. I nie ma to nic wspólnego z kodem. Ale ma to niewielki cel, który pozostaje w dokumentacji.

Dokumentuje to, jakie są oczekiwane imiona, więc jeśli połączę kod z kimś, a jedno z nas będzie miało „nazwę użytkownika”, a druga nazwa użytkownika, będzie to dla ludzi wskazówka, że ​​rozeszliśmy się i nie używamy tych samych zmiennych.

Wymuszanie pełnej inicjalizacji jako zasady osiąga to samo w bardziej Pythoński sposób, ale jeśli jest w niej rzeczywisty kod __init__, zapewnia to wyraźniejsze miejsce do dokumentowania używanych zmiennych.

Oczywiście BIG problem polega na tym, że kusi ludzi do inicjowania wartościami innymi niż None, co może być złe:

class X:
    v = {}
x = X()
x.v[1] = 2

pozostawia globalny ślad i nie tworzy instancji dla x.

Ale jest to bardziej dziwactwo w Pythonie jako całości niż w tej praktyce i powinniśmy już być na tym punkcie paranoiczni.

Jon Jay Obermark
źródło