Dlaczego warto korzystać z klas abstrakcyjnych w Pythonie?

213

Ponieważ jestem przyzwyczajony do starych sposobów pisania kaczek w Pythonie, nie rozumiem potrzeby ABC (abstrakcyjne klasy podstawowe). Pomoc jest dobra, w jaki sposób z nich korzystać.

Próbowałem odczytać uzasadnienie w PEP , ale przeszło mi to przez głowę. Gdybym szukał kontenera sekwencji zmiennych, sprawdziłbym __setitem__, a raczej spróbuje go użyć ( EAFP ). Nie spotkałem się z rzeczywistym użyciem modułu liczb , który korzysta z ABC, ale to jest najbliższe zrozumienie.

Czy ktoś może mi wyjaśnić uzasadnienie?

Muhammad Alkarouri
źródło

Odpowiedzi:

162

Krótka wersja

ABC oferują wyższy poziom kontraktu semantycznego między klientami a wdrożonymi klasami.

Długa wersja

Istnieje umowa między klasą a jej rozmówcami. Klasa obiecuje robić pewne rzeczy i mieć określone właściwości.

Kontrakt ma różne poziomy.

Na bardzo niskim poziomie umowa może zawierać nazwę metody lub jej liczbę parametrów.

W języku o statycznym typie umowa faktycznie byłaby egzekwowana przez kompilator. W Pythonie możesz użyć EAFP lub wpisać introspekcję, aby potwierdzić, że nieznany obiekt spełnia ten oczekiwany kontrakt.

Ale w kontrakcie są również obietnice semantyczne wyższego poziomu.

Na przykład, jeśli istnieje __str__()metoda, oczekuje się, że zwróci ciąg znaków reprezentujący obiekt. To mogłoby usunąć całą zawartość obiektu, zatwierdzić transakcję i wypluć pustą stronę z drukarki ... ale jest powszechne zrozumienie tego, co należy zrobić, opisano w instrukcji Pythona.

To szczególny przypadek, w którym kontrakt semantyczny jest opisany w instrukcji. Co powinna print()zrobić metoda? Czy powinien zapisać obiekt na drukarce, czy linię na ekranie, czy coś innego? To zależy - musisz przeczytać komentarze, aby zrozumieć pełną umowę tutaj. Fragment kodu klienta, który po prostu sprawdza, czy print()metoda istnieje, potwierdził część umowy - że można wywołać metodę, ale nie zgadza się co do semantyki wyższego poziomu wywołania.

Definiowanie abstrakcyjnej klasy bazowej (ABC) jest sposobem na zawarcie umowy między implementującymi klasę a dzwoniącymi. To nie jest tylko lista nazw metod, ale wspólne zrozumienie, co te metody powinny zrobić. Jeśli odziedziczysz po tym ABC, obiecujesz przestrzegać wszystkich zasad opisanych w komentarzach, w tym semantyki print()metody.

Pisanie kaczek w Pythonie ma wiele zalet w zakresie elastyczności w porównaniu do pisania statycznego, ale nie rozwiązuje wszystkich problemów. ABC oferuje pośrednie rozwiązanie między wolną formą Pythona a niewolą i dyscypliną języka o statycznym pisaniu.

Dziwne
źródło
12
Myślę, że masz rację, ale nie mogę za tobą podążać. Jaka jest różnica pod względem kontraktu między klasą implementującą __contains__a klasą dziedziczącą collections.Container? W twoim przykładzie w Pythonie zawsze było wspólne rozumienie __str__. Implementacja __str__składa się z takich samych obietnic, jak dziedziczenie po ABC, a następnie implementacja __str__. W obu przypadkach możesz zerwać umowę; nie ma żadnej możliwej do udowodnienia semantyki, takiej jak te, które mamy podczas pisania statycznego.
Muhammad Alkarouri
15
collections.Containerjest przypadkiem zdegenerowanym, który obejmuje \_\_contains\_\_tylko i wyłącznie oznaczenie uprzednio określonej konwencji. Zgadzam się, że używanie ABC samo w sobie nie stanowi dużej wartości. Podejrzewam, że został dodany, aby umożliwić (na przykład) Setodziedziczenie po nim. Zanim dotrzesz na miejsce Set, nagle przynależność do ABC ma znaczną semantykę. Przedmiot nie może dwukrotnie należeć do kolekcji. NIE jest to wykrywalne przez istnienie metod.
Dziwne,
3
Tak, myślę, że Setjest to lepszy przykład niż print(). Próbowałem znaleźć nazwę metody, której znaczenie było niejednoznaczne i sama nazwa nie mogła tego uchwycić, więc nie można mieć pewności, że zrobi to dobrze tylko na podstawie nazwy i podręcznika Pythona.
Dziwne,
3
Czy jest jakaś szansa na przepisanie odpowiedzi Setjako przykład zamiast print? Setma sens, @Oddthinking.
Ehtesh Choudhury
1
Myślę, że ten artykuł wyjaśnia to bardzo dobrze: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

@ Odpowiedź Oddthinkinga nie jest zła, ale myślę, że brakuje prawdziwego , praktycznego powodu , dla którego Python ma ABC w świecie pisania kaczych liter.

Metody abstrakcyjne są zgrabne, ale moim zdaniem tak naprawdę nie wypełniają żadnych przypadków użycia, które nie są jeszcze objęte pisaniem kaczek. Rzeczywista moc abstrakcyjnych klas bazowych polega na tym, że pozwalają one dostosować zachowanie isinstanceiissubclass . ( __subclasshook__jest w zasadzie bardziej przyjaznym interfejsem API na Pythonie __instancecheck__i__subclasscheck__ hakach). Dostosowywanie wbudowanych konstrukcji do pracy na niestandardowych typach jest w dużej mierze częścią filozofii Pythona.

Kod źródłowy Pythona jest przykładowy. Oto jak collections.Containerjest zdefiniowane w standardowej bibliotece (w momencie pisania):

class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Ta definicja __subclasshook__mówi, że każda klasa z __contains__atrybutem jest uważana za podklasę kontenera, nawet jeśli nie podklasuje jej bezpośrednio. Więc mogę napisać to:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

Innymi słowy, jeśli zaimplementujesz odpowiedni interfejs, jesteś podklasą! ABC zapewniają formalny sposób definiowania interfejsów w Pythonie, pozostając wiernym duchowi pisania kaczego. Poza tym działa to w sposób zgodny z zasadą otwartego i zamkniętego .

Model obiektowy Pythona wygląda powierzchownie podobnie do bardziej „tradycyjnego” systemu OO (przez co mam na myśli Javę *) - mamy twoje klasy, twoje obiekty, twoje metody - ale kiedy zarysujesz powierzchnię, znajdziesz coś znacznie bogatszego i bardziej elastyczne. Podobnie, pojęcie abstrakcyjnych klas podstawowych w Pythonie może być rozpoznawalne przez programistę Java, ale w praktyce są one przeznaczone do zupełnie innych celów.

Czasami piszę funkcje polimorficzne, które mogą oddziaływać na pojedynczy element lub zbiór elementów, i okazuje się, isinstance(x, collections.Iterable)że jest dużo bardziej czytelny niż hasattr(x, '__iter__')lub równoważny try...exceptblok. (Gdybyś nie znał Pythona, która z tych trzech sprawiłaby, że zamiar kodu byłby najwyraźniejszy?)

To powiedziawszy, uważam, że rzadko muszę pisać własne ABC i zwykle odkrywam potrzebę takiego poprzez refaktoryzację. Jeśli widzę funkcję polimorficzną wykonującą wiele kontroli atrybutów lub wiele funkcji wykonujących te same kontrole atrybutów, ten zapach sugeruje istnienie ABC oczekującego na wyodrębnienie.

* bez wdawania się w debatę na temat tego, czy Java jest „tradycyjnym” systemem OO ...


Dodatek : Mimo że abstrakcyjna klasa podstawowa może zastąpić zachowanie isinstancei issubclass, nadal nie wchodzi w MRO wirtualnej podklasy. Jest to potencjalna pułapka dla klientów: nie każdy obiekt, dla którego isinstance(x, MyABC) == Truezdefiniowano metody MyABC.

class MyABC(metaclass=abc.ABCMeta):
    def abc_method(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        return True

class C(object):
    pass

# typical client code
c = C()
if isinstance(c, MyABC):  # will be true
    c.abc_method()  # raises AttributeError

Niestety, jedna z tych pułapek „po prostu nie rób tego” (których Python ma stosunkowo niewiele!): Unikaj definiowania ABC zarówno za pomocą __subclasshook__metod a, jak i nieabstrakcyjnych. Co więcej, powinieneś __subclasshook__dostosować swoją definicję do zestawu metod abstrakcyjnych zdefiniowanych przez ABC.

Benjamin Hodgson
źródło
21
„jeśli wdrożysz odpowiedni interfejs, jesteś podklasą” Bardzo za to dziękuję. Nie wiem, czy Oddthinking to przegapił, ale na pewno tak. FWIW, isinstance(x, collections.Iterable)jest dla mnie bardziej zrozumiały i znam Pythona.
Muhammad Alkarouri
Doskonały post. Dziękuję Ci. Myślę, że Uzupełnienie „tylko nie rób tego” pułapki, jest trochę jak robi normalne podklasy dziedziczenia ale posiadające Cpodklasa usunąć (lub zepsuć nieodwracalnie) abc_method()odziedziczone MyABC. Zasadnicza różnica polega na tym, że to nadklasa psuje umowę spadkową, a nie podklasę.
Michael Scott Cuthbert,
Czy nie musiałbyś zrobić, Container.register(ContainAllTheThings)aby dany przykład zadziałał?
BoZenKhaa
1
@BoZenKhaa Kod w odpowiedzi działa! Spróbuj! Znaczenie __subclasshook__to „każda klasa, która spełnia ten predykat, jest uważana za podklasę do celów isinstancei issubclasssprawdza, niezależnie od tego, czy została zarejestrowana w ABC i niezależnie od tego, czy jest to bezpośrednia podklasa ”. Jak powiedziałem w odpowiedzi, jeśli wdrożysz odpowiedni interfejs, jesteś podklasą!
Benjamin Hodgson
1
Powinieneś zmienić formatowanie z kursywy na pogrubioną. „jeśli wdrożysz odpowiedni interfejs, jesteś podklasą!” Bardzo zwięzłe wyjaśnienie. Dziękuję Ci!
marti
108

Przydatną funkcją ABC jest to, że jeśli nie zaimplementujesz wszystkich niezbędnych metod (i właściwości), pojawi się błąd przy tworzeniu wystąpienia, a nie AttributeError, potencjalnie znacznie później, kiedy faktycznie spróbujesz użyć brakującej metody.

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
class Base(object, metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`


c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

Przykład z https://dbader.org/blog/abstract-base-classes-in-python

Edycja: w celu włączenia składni Python3, dzięki @PandasRocks

cerberos
źródło
5
Przykład i link były bardzo pomocne. Dzięki!
nalyd88
jeśli Baza jest zdefiniowana w osobnym pliku, musisz odziedziczyć po niej Base.Base lub zmienić linię importu na „from Base Import Base”
Ryan Tennill
Należy zauważyć, że w Python3 składnia jest nieco inna. Zobacz tę odpowiedź: stackoverflow.com/questions/28688784/...
PandasRocks,
7
Pochodzący z tła C # jest to powód do korzystania z klas abstrakcyjnych. Zapewniasz funkcjonalność, ale stwierdzasz, że funkcjonalność wymaga dalszej implementacji. Inne odpowiedzi wydają się pomijać ten punkt.
Josh Noe,
18

Ułatwi to ustalenie, czy obiekt obsługuje dany protokół bez konieczności sprawdzania obecności wszystkich metod w protokole lub bez wywoływania wyjątku głęboko na terytorium „wroga” z powodu braku wsparcia.

Ignacio Vazquez-Abrams
źródło
6

Metoda abstrakcyjna upewnij się, że każda metoda, którą wywołujesz w klasie nadrzędnej, musi pojawiać się w klasie podrzędnej. Poniżej przedstawiamy sposób wzywania i używania abstrakcji w języku noraml. Program napisany w python3

Normalny sposób dzwonienia

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

nazywa się „methodone ()”

c.methodtwo()

NotImplementedError

Metodą abstrakcyjną

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError: Nie można utworzyć instancji klasy abstrakcyjnej Son z metodami abstrakcyjnymi methodtwo.

Ponieważ metodatwo nie jest wywoływana w klasie potomnej, wystąpił błąd. Prawidłowe wdrożenie znajduje się poniżej

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

nazywa się „methodone ()”

sim
źródło
1
Dzięki. Czy to nie to samo, co w powyższej odpowiedzi na Cerberos?
Muhammad Alkarouri,