2017-09-15 61 views
7

Próbuję zrozumieć oop w pytonie Przyszedłem do tej sytuacji, która mnie zastanawia i nie byłem w stanie znaleźć satysfakcjonującego wyjaśnienia ... Budowałem klasę Countable, która ma licznik atrybut, który zlicza liczbę instancji klasy, które zostały zainicjalizowane. Chcę, aby ten licznik był zwiększany również wtedy, gdy inicjowana jest podklasa (lub subklasę) danej klasy. Oto moja realizacja:Dziedziczenie zmiennych klasowych w pytonie

class Countable(object): 
    counter = 0 
    def __new__(cls, *args, **kwargs): 
     cls.increment_counter() 
     count(cls) 
     return object.__new__(cls, *args, **kwargs) 

    @classmethod 
    def increment_counter(cls): 
     cls.counter += 1 
     if cls.__base__ is not object: 
      cls.__base__.increment_counter() 

gdzie count(cls) jest tam dla celów debugowania, a później napisać go w dół.

Teraz zróbmy kilka podklas to:

class A(Countable): 
    def __init__(self, a='a'): 
     self.a = a 

class B(Countable): 
    def __init__(self, b='b'): 
     self.b = b 

class B2(B): 
    def __init__(self, b2='b2'): 
     self.b2 = b2 

def count(cls): 
    print('@{:<5} Countables: {} As: {} Bs: {} B2s: {}' 
      ''.format(cls.__name__, Countable.counter, A.counter, B.counter, B2.counter)) 

kiedy uruchomić kod jak poniżej:

a = A() 
a = A() 
a = A() 
b = B() 
b = B() 
a = A() 
b2 = B2() 
b2 = B2() 

I uzyskać następujący wynik, który wygląda dziwnie do mnie:

@A  Countables: 1 As: 1 Bs: 1 B2s: 1 
@A  Countables: 2 As: 2 Bs: 2 B2s: 2 
@A  Countables: 3 As: 3 Bs: 3 B2s: 3 
@B  Countables: 4 As: 3 Bs: 4 B2s: 4 
@B  Countables: 5 As: 3 Bs: 5 B2s: 5 
@A  Countables: 6 As: 4 Bs: 5 B2s: 5 
@B2  Countables: 7 As: 4 Bs: 6 B2s: 6 
@B2  Countables: 8 As: 4 Bs: 7 B2s: 7 

Dlaczego na początku licznik A i B zwiększa się, pomimo tego, że dzwonię pod numer A()? I dlaczego po raz pierwszy dzwonię pod numer B(), który zachowuje się jak oczekiwano?

Już się dowiedziałem, że zachowanie takie, jak chcę, wystarczy dodać counter = 0 dla każdej podklasy, ale nie byłem w stanie znaleźć wyjaśnienia, dlaczego zachowuje się w ten sposób .... Dziękuję!


Dodałem kilka odbitek do debugowania, a dla uproszczonego, ograniczonego tworzenia klas do dwóch. Jest to dość dziwne:

>>> a = A() 
<class '__main__.A'> incrementing 
increment parent of <class '__main__.A'> as well 
<class '__main__.Countable'> incrementing 
@A  Counters: 1 As: 1 Bs: 1 B2s: 1 
>>> B.counter 
1 
>>> B.counter is A.counter 
True 
>>> b = B() 
<class '__main__.B'> incrementing 
increment parent of <class '__main__.B'> as well 
<class '__main__.Countable'> incrementing 
@B  Counters: 2 As: 1 Bs: 2 B2s: 2 
>>> B.counter is A.counter 
False 

Jak to się stało, gdy B() nie jest jeszcze zainicjowany, to wskazuje na tej samej zmiennej jako A.counter ale po utworzeniu pojedynczy obiekt jest inna?

+0

I nie można odtworzyć swoje wyjście. Moje wyjście dla 'B2s' jest zawsze takie samo jak' Bs'. –

+0

Edytowałem Twoje pytanie z uproszczonym przykładem problemu. To interesujące pytanie, mam nadzieję, że ktoś może rzucić nieco światła na proces. – Vinny

+0

@Przypomnienie, że masz rację, wkleiłem dane wyjściowe innego przykładu ... teraz to naprawiam! –

Odpowiedz

7

Problem z kodem polega na tym, że podklasy Countable nie mają własnego atrybutu counter. Są one jedynie dziedziczące po Countable, więc po zmianie Countablecounter, zmienia się również klasa counter klasy potomnej.

Minimal przykład:

class Countable: 
    counter = 0 

class A(Countable): 
    pass # A does not have its own counter, it shares Countable's counter 

print(Countable.counter) # 0 
print(A.counter) # 0 

Countable.counter += 1 

print(Countable.counter) # 1 
print(A.counter) # 1 

Jeśli A miał własny atrybut counter, wszystko będzie działać zgodnie z oczekiwaniami:

class Countable: 
    counter = 0 

class A(Countable): 
    counter = 0 # A has its own counter now 

print(Countable.counter) # 0 
print(A.counter) # 0 

Countable.counter += 1 

print(Countable.counter) # 1 
print(A.counter) # 0 

Ale jeśli wszystkie z tych klas podziela tą counter, dlaczego widzimy różne liczby na wyjściu? To dlatego, że faktycznie dodać atrybut counter do klasy dziecięcej później, z tym kodem:

cls.counter += 1 

Jest to odpowiednik cls.counter = cls.counter + 1. Jednak ważne jest, aby zrozumieć, co oznacza cls.counter. W wersji cls.counter + 1, cls nie ma jeszcze własnego atrybutu counter, więc w rzeczywistości daje to klasę nadrzędną: counter.Następnie ta wartość jest zwiększana, a cls.counter = ... dodaje atrybut counter do klasy podrzędnej, która nie istniała do tej pory. Jest to zasadniczo równoznaczne z pisaniem cls.counter = cls.__base__.counter + 1. Można to zobaczyć w akcji tutaj:

class Countable: 
    counter = 0 

class A(Countable): 
    pass 

# Does A have its own counter attribute? 
print('counter' in A.__dict__) # False 

A.counter += 1 

# Does A have its own counter attribute now? 
print('counter' in A.__dict__) # True 

Więc jakie jest rozwiązanie tego problemu? Potrzebujesz metaclass. To daje możliwość, aby dać każdemu Countable podklasa swój atrybut counter gdy zostanie on utworzony:

class CountableMeta(type): 
    def __new__(cls, name, bases, attrs): 
     new_class = super(CountableMeta, cls).__new__(cls, name, bases, attrs) 
     new_class.counter = 0 # each class gets its own counter 
     return new_class 

class Countable: 
    __metaclass__ = CountableMeta 

# in python 3 Countable would be defined like this: 
# 
# class Countable(metaclass=CountableMeta): 
# pass 

class A(Countable): 
    pass 

print(Countable.counter) # 0 
print(A.counter) # 0 

Countable.counter += 1 

print(Countable.counter) # 1 
print(A.counter) # 0 
+1

Chciałbym tylko dodać, że w Python3.6 +, można również użyć ['__init_subclass __() hook'] (https://docs.python.org/3/ reference/datamodel.html # customizing-class-creation) w tym samym celu (dodając atrybut "counter" do każdej podklasy). – plamut

+0

Lub (w Pythonie 2.7.x + i 3.x) użyj dekoratora klasy. –

+0

Jednak po zakończeniu tworzenia pierwszego obiektu ('a = A()') otrzymuję 'id (Countable.counter) == id (A.counter)'. Dlaczego tak się dzieje, jeśli zadanie tworzy nową klasę dla klasy A ?? –