Warum abstrakte Basisklassen in Python verwenden?

213

Da ich an die alten Methoden der Ententypisierung in Python gewöhnt bin, verstehe ich die Notwendigkeit von ABC (abstrakte Basisklassen) nicht. Die Hilfe ist gut, wie man sie benutzt.

Ich habe versucht, die Gründe im PEP zu lesen , aber es ging mir über den Kopf. Wenn ich nach einem veränderlichen Sequenzcontainer suchen würde, würde ich ihn prüfen __setitem__oder eher versuchen, ihn zu verwenden ( EAFP ). Ich bin nicht gekommen , über einen wirklichen Leben Verwendung für die Zahlen - Modul, das den Einsatz ABCs der Fall ist, aber das ist die nächste , die ich zu verstehen haben.

Kann mir bitte jemand die Gründe erklären?

Muhammad Alkarouri
quelle

Antworten:

162

Kurzfassung

ABCs bieten ein höheres Maß an semantischem Vertrag zwischen Kunden und den implementierten Klassen.

Lange Version

Es besteht ein Vertrag zwischen einer Klasse und ihren Anrufern. Die Klasse verspricht, bestimmte Dinge zu tun und bestimmte Eigenschaften zu haben.

Der Vertrag hat verschiedene Ebenen.

Auf einer sehr niedrigen Ebene kann der Vertrag den Namen einer Methode oder die Anzahl der Parameter enthalten.

In einer statisch typisierten Sprache würde dieser Vertrag tatsächlich vom Compiler durchgesetzt. In Python können Sie EAFP verwenden oder Introspection eingeben, um zu bestätigen, dass das unbekannte Objekt diesen erwarteten Vertrag erfüllt.

Der Vertrag enthält aber auch übergeordnete semantische Versprechen.

Wenn beispielsweise eine __str__()Methode vorhanden ist , wird erwartet, dass eine Zeichenfolgendarstellung des Objekts zurückgegeben wird. Es könnte den gesamten Inhalt des Objekts löschen, die Transaktion festschreiben und eine leere Seite aus dem Drucker ausspucken ... aber es gibt ein allgemeines Verständnis dafür, was es tun sollte, wie im Python-Handbuch beschrieben.

Dies ist ein Sonderfall, in dem der semantische Vertrag im Handbuch beschrieben wird. Was soll die print()Methode tun? Sollte es das Objekt auf einen Drucker oder eine Zeile auf den Bildschirm schreiben oder etwas anderes? Es kommt darauf an - Sie müssen die Kommentare lesen, um den vollständigen Vertrag hier zu verstehen. Ein Teil des Client-Codes, der lediglich überprüft, ob die print()Methode vorhanden ist, hat einen Teil des Vertrags bestätigt - dass ein Methodenaufruf durchgeführt werden kann, nicht jedoch, dass Übereinstimmung über die Semantik der höheren Ebene des Aufrufs besteht.

Das Definieren einer abstrakten Basisklasse (ABC) ist eine Möglichkeit, einen Vertrag zwischen den Klassenimplementierern und den Aufrufern zu erstellen. Es ist nicht nur eine Liste von Methodennamen, sondern ein gemeinsames Verständnis dessen, was diese Methoden tun sollten. Wenn Sie von diesem ABC erben, versprechen Sie, alle in den Kommentaren beschriebenen Regeln zu befolgen, einschließlich der Semantik der print()Methode.

Pythons Ententypisierung bietet viele Vorteile in Bezug auf Flexibilität gegenüber statischer Typisierung, löst jedoch nicht alle Probleme. ABCs bieten eine Zwischenlösung zwischen der freien Form von Python und der Bindung und Disziplin einer statisch typisierten Sprache.

Seltsames Denken
quelle
12
Ich denke, dass Sie dort einen Punkt haben, aber ich kann Ihnen nicht folgen. Was ist also der vertragliche Unterschied zwischen einer Klasse, die implementiert, __contains__und einer Klasse, von der erbt collections.Container? In Ihrem Beispiel gab es in Python immer ein gemeinsames Verständnis von __str__. Das Implementieren __str__macht die gleichen Versprechen wie das Erben von ABC und das anschließende Implementieren __str__. In beiden Fällen können Sie den Vertrag brechen; Es gibt keine nachweisbare Semantik wie die, die wir bei der statischen Typisierung haben.
Muhammad Alkarouri
15
collections.Containerist ein entarteter Fall, der nur \_\_contains\_\_die vordefinierte Konvention einschließt und nur bedeutet. Die Verwendung eines ABC bringt an sich nicht viel Wert, da stimme ich zu. Ich vermute, es wurde hinzugefügt, um (zum Beispiel) Setzu erlauben, davon zu erben. Zu dem Zeitpunkt, an dem Sie ankommen, Sethat die Zugehörigkeit zum ABC plötzlich eine beträchtliche Semantik. Ein Gegenstand kann nicht zweimal zur Sammlung gehören. Das ist NICHT durch die Existenz von Methoden erkennbar.
Oddthinking
3
Ja, ich denke Setist ein besseres Beispiel als print(). Ich habe versucht, einen Methodennamen zu finden, dessen Bedeutung nicht eindeutig ist und der nicht allein durch den Namen erfasst werden kann. Sie konnten also nicht sicher sein, ob er nur anhand seines Namens und des Python-Handbuchs das Richtige tun würde.
Oddthinking
3
Gibt es eine Chance, die Antwort Setals Beispiel anstatt zu schreiben print? Setmacht sehr viel Sinn, @Oddthinking.
Ehtesh Choudhury
1
Ich denke, dieser Artikel erklärt es sehr gut: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

@ Oddthinkings Antwort ist nicht falsch, aber ich denke, sie vermisst den wirklichen , praktischen Grund , warum Python ABCs in einer Welt des Ententypens hat.

Abstrakte Methoden sind ordentlich, aber meiner Meinung nach füllen sie keine Anwendungsfälle aus, die noch nicht durch Entenschreiben abgedeckt sind. Die wahre Kraft abstrakter Basisklassen liegt in der Art und Weise, wie Sie das Verhalten von isinstanceund anpassen könnenissubclass . ( __subclasshook__ist im Grunde eine freundlichere API zusätzlich zu Pythons __instancecheck__und__subclasscheck__ Hooks.) Die Anpassung der integrierten Konstrukte an benutzerdefinierte Typen ist ein wesentlicher Bestandteil der Python-Philosophie.

Der Quellcode von Python ist beispielhaft. So wird collections.Containerin der Standardbibliothek (zum Zeitpunkt des Schreibens) definiert:

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

Diese Definition von __subclasshook__besagt, dass jede Klasse mit einem __contains__Attribut als Unterklasse von Container betrachtet wird, auch wenn sie nicht direkt untergeordnet wird. Also kann ich das schreiben:

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

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

Mit anderen Worten, wenn Sie die richtige Schnittstelle implementieren, sind Sie eine Unterklasse! ABCs bieten eine formale Möglichkeit, Schnittstellen in Python zu definieren, während sie dem Geist der Ententypisierung treu bleiben. Außerdem funktioniert dies auf eine Weise, die das Open-Closed-Prinzip berücksichtigt .

Das Objektmodell von Python ähnelt oberflächlich dem eines "traditionelleren" OO-Systems (womit ich Java * meine) - wir haben Ihre Klassen, Ihre Objekte, Ihre Methoden - aber wenn Sie die Oberfläche kratzen, werden Sie etwas finden, das viel reicher ist und flexibler. Ebenso mag Pythons Vorstellung von abstrakten Basisklassen für einen Java-Entwickler erkennbar sein, aber in der Praxis sind sie für einen ganz anderen Zweck gedacht.

Manchmal schreibe ich polymorphe Funktionen, die auf ein einzelnes Element oder eine Sammlung von Elementen wirken können, und finde isinstance(x, collections.Iterable), dass sie viel besser lesbar sind als hasattr(x, '__iter__')ein gleichwertiger try...exceptBlock. (Wenn Sie Python nicht kennen würden, welcher dieser drei würde die Absicht des Codes am deutlichsten machen?)

Trotzdem finde ich, dass ich selten mein eigenes ABC schreiben muss, und ich entdecke die Notwendigkeit eines ABC normalerweise durch Refactoring. Wenn ich sehe, dass eine polymorphe Funktion viele Attributprüfungen durchführt oder viele Funktionen dieselben Attributprüfungen durchführt, deutet dieser Geruch auf die Existenz eines ABC hin, das darauf wartet, extrahiert zu werden.

* ohne in die Debatte darüber zu geraten, ob Java ein "traditionelles" OO-System ist ...


Nachtrag : Obwohl eine abstrakte Basisklasse das Verhalten von isinstanceund überschreiben kann issubclass, wird sie immer noch nicht in die MRO der virtuellen Unterklasse eingegeben . Dies ist eine potenzielle Gefahr für Kunden: Nicht für jedes Objekt isinstance(x, MyABC) == Truesind die Methoden definiert 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

Leider ist dies eine dieser "Mach das einfach nicht" -Fallen (von denen Python relativ wenige hat!): Vermeiden Sie es, ABCs sowohl mit einer __subclasshook__als auch mit nicht abstrakten Methoden zu definieren. Darüber hinaus sollten Sie Ihre Definition __subclasshook__konsistent mit den von ABC definierten abstrakten Methoden machen.

Benjamin Hodgson
quelle
21
"Wenn Sie die richtige Schnittstelle implementieren, sind Sie eine Unterklasse" Vielen Dank dafür. Ich weiß nicht, ob Oddthinking es verpasst hat, aber ich habe es auf jeden Fall getan. FWIW isinstance(x, collections.Iterable)ist für mich klarer und ich kenne Python.
Muhammad Alkarouri
Ausgezeichnete Post. Danke dir. Ich denke, dass das Addendum "Mach das einfach nicht" ein bisschen wie eine normale Vererbung von Unterklassen ist, aber dann die CUnterklasse die abc_method()geerbte löschen (oder irreparabel vermasseln) lässt MyABC. Der Hauptunterschied besteht darin, dass es die Oberklasse ist, die den Erbvertrag vermasselt, nicht die Unterklasse.
Michael Scott Cuthbert
Müssten Sie nicht tun, Container.register(ContainAllTheThings)damit das gegebene Beispiel funktioniert?
BoZenKhaa
1
@BoZenKhaa Der Code in der Antwort funktioniert! Versuch es! Die Bedeutung von __subclasshook__ist "jede Klasse, die dieses Prädikat erfüllt, wird als Unterklasse für die Zwecke isinstanceund issubclassÜberprüfungen betrachtet, unabhängig davon, ob sie beim ABC registriert wurde und ob es sich um eine direkte Unterklasse handelt ". Wie ich in der Antwort sagte, wenn Sie die richtige Schnittstelle implementieren, sind Sie eine Unterklasse!
Benjamin Hodgson
1
Sie sollten die Formatierung von kursiv in fett ändern. "Wenn Sie die richtige Schnittstelle implementieren, sind Sie eine Unterklasse!" Sehr prägnante Erklärung. Danke dir!
Marti
108

Eine praktische Funktion von ABCs ist, dass Sie, wenn Sie nicht alle erforderlichen Methoden (und Eigenschaften) implementieren, bei der Instanziierung einen Fehler erhalten und nicht AttributeErrorviel später, wenn Sie tatsächlich versuchen, die fehlende Methode zu verwenden.

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"

Beispiel aus https://dbader.org/blog/abstract-base-classes-in-python

Bearbeiten: Um die Python3-Syntax einzuschließen, danke @PandasRocks

cerberos
quelle
5
Das Beispiel und der Link waren beide sehr hilfreich. Vielen Dank!
Nalyd88
Wenn Base in einer separaten Datei definiert ist, müssen Sie als Base.Base davon erben oder die Importzeile in 'from Base import Base'
ändern
Es ist zu beachten, dass die Syntax in Python3 etwas anders ist. Siehe diese Antwort: stackoverflow.com/questions/28688784/…
PandasRocks
7
Aus einem C # -Hintergrund stammend, ist dies der Grund, abstrakte Klassen zu verwenden. Sie stellen Funktionen bereit, geben jedoch an, dass für die Funktionalität weitere Implementierungen erforderlich sind. Die anderen Antworten scheinen diesen Punkt zu verfehlen.
Josh Noe
18

Dadurch wird es viel einfacher zu bestimmen, ob ein Objekt ein bestimmtes Protokoll unterstützt, ohne auf das Vorhandensein aller Methoden im Protokoll prüfen zu müssen oder ohne eine Ausnahme tief im "feindlichen" Gebiet auszulösen, da keine Unterstützung erfolgt.

Ignacio Vazquez-Abrams
quelle
6

Abstrakte Methode Stellen Sie sicher, dass jede Methode, die Sie in der übergeordneten Klasse aufrufen, in der untergeordneten Klasse angezeigt werden muss. Im Folgenden finden Sie eine noraml-Methode zum Aufrufen und Verwenden von Abstract. Das in Python3 geschriebene Programm

Normale Art zu telefonieren

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

'methodone () heißt'

c.methodtwo()

NotImplementedError

Mit abstrakter Methode

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: Abstrakte Klasse Son kann mit abstrakten Methoden methodtwo nicht instanziiert werden.

Da methodtwo in der untergeordneten Klasse nicht aufgerufen wird, ist ein Fehler aufgetreten. Die richtige Implementierung ist unten

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

'methodone () heißt'

sim
quelle
1
Vielen Dank. Ist das nicht der gleiche Punkt, den Cerberos oben gemacht hat?
Muhammad Alkarouri