Vererbung: Attribute aus Subklassen in Basisklassen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Hallo,

ich arbeite gerade ein Buch zur OOP durch und bin auf folgenden Code gestoßen:

Code: Alles auswählen

class Contact:
    all_contacts = []

    def __init__(self, name, email):
        self.name = name
        self.email = email
        Contact.all_contacts.append(self)


class MailSender:
    def send_email(self, message):
        print("Sending mail to ", self.email)
        # Add code logic here


class EmailableContact(Contact, MailSender):
    pass
`message` wird nicht weiter verwendet, das ganze soll auch nur als Beispiel für Mixins dienen. Ich bezweifle zwar, dass ich multiple Vererbung jemals brauchen werde, frage mich aber, wie man hier mit `self.email` "besser" umgehen kann. Ist es in Python üblich in Basisklassen Attribute zu verwenden, die im Namespace der Basisklasse eigentlich überhaupt nicht bekannt sind, und erst in der Subklasse verwendet werden? Sollte man o.g. Beispiel nicht besser mit abc als eine abstrakte Klasse ausbilden, damit man zumindest beim Erstellen einer Subklasse erkennt dass man diese und jene Attribute/Methoden implementieren muss?

Ich *weiß* jetzt zwar, dass ich `MailSender` so nicht direkt verwenden kann, mag mir aber vorstellen dass das in komplexeren Projekten schnell unübersichtlich werden kann.
When we say computer, we mean the electronic computer.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da ein Mixin selbst keinen Zustand haben soll, und gleichzeitig ein Attribut ohne Unterstrich davor eine public API darstellt, folgt daraus: ja, so macht man das. Wo da nun eine Abstrakte Klasse irgendetwas verbessern wuerde, erschliesst sich mir nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Randbemerkung: Was an dem Beispiel wirklich schlecht ist, ist `all_contacts`. Das ist globaler Zustand. Und verhindert beispielsweise das man mehr als ein Adressbuch mit `Contact`-Objekt hat, beziehungsweise wenn man das braucht ist diese Liste einfach nur blödsinniger Ballast.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

`all_contacts` ist sogar noch mehr blödsinnig, weil auch jede temporär gebrauchte Instanz darin gespeichert wird und man Mühe hat, die da wieder raus zubekommen.

@sls: Contact an sich ist ja nicht abstrakt. Es kann durchaus Anwendungsfälle geben, wo man einen Kontakt hat, der nicht per Email erreichbar ist (also in einem realen Kontext könnte das Sinn machen). Sollte es sich tatsächlich um eine abstrakte Klasse handeln, würde ich nichts mit abc, etc. machen, sondern einfach die Klasse AbstractXY nennen, dann ist dem Leser viel klarer, dass es sich eine abstrakte Klasse ist.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Hier ist nochmal eine entsprechende Beschreibung des Autors zur Contact-Klasse:
How do we apply inheritance in practice? The simplest and most obvious use of
inheritance is to add functionality to an existing class. Let's start with a simple
contact manager that tracks the name and e-mail address of several people. The
contact class is responsible for maintaining a list of all contacts in a class variable,
and for initializing the name and address for an individual contact:

Code: Alles auswählen

class Contact:
	all_contacts = []
	def __init__(self, name, email):
		self.name = name
		self.email = email
		Contact.all_contacts.append(self)
This example introduces us to class variables. The all_contacts list, because it is
part of the class definition, is shared by all instances of this class. This means that
there is only one Contact.all_contacts list, which we can access as Contact.all_
contacts . Less obviously, we can also access it as self.all_contacts on any object
instantiated from Contact . If the field can't be found on the object, then it
will be found on the class and thus refer to the same single list.
Be careful with this syntax, for if you ever set the variable using
self.all_contacts, you will actually be creating a new instance
variable associated only with that object. The class variable will still
be unchanged and accessible as Contact.all_contacts.

This is a simple class that allows us to track a couple pieces of data about each
contact. But what if some of our contacts are also suppliers that we need to order
supplies from? We could add an order method to the Contact class, but that would
allow people to accidentally order things from contacts who are customers or family
friends. Instead, let's create a new Supplier class that acts like our Contact class,
but has an additional order method:

Code: Alles auswählen

class Supplier(Contact):
	def order(self, order):
		print("If this were a real system we would send "
			"'{}' order to '{}'".format(order, self.name))
In einem späteren Beispiel wird dann das list-built-in erweitert:

Code: Alles auswählen

class ContactList(list):
	def search(self, name):
		'''Return all contacts that contain the search value
		in their name.'''
		matching_contacts = []
		for contact in self:
			if name in contact.name:
				matching_contacts.append(contact)
		return matching_contacts
		
class Contact:
	all_contacts = ContactList()
	def __init__(self, name, email):
	....
When we say computer, we mean the electronic computer.
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

`list` würde ich in der Regel auch nicht beerben, nur wenn die vorhandenen Methoden auch sinnvolle Ergebnisse liefern und gebraucht werden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich schwanke hier ein bisschen. Ich stimme mit __blackjack__ ueberein, dass das ableiten von einem builtin ungewwoehnlich ist, und eigentlich nicht passieren sollte. Nicht zuletzt weil man sich bei sowas fast immer irgendwelche "slicing"-Probleme einfaengt (so zumindest heisst das in C++, wenn man einen Wert der Basisklasse mit der abgeleiteten belegt, und dabei alles, was die kann, verliert).

Da wuerde ich immer auf Komposition setzen.

Das man Klassenattribute, auch veraenderliche, mal vorfuehrt, finde ich prinzipiell aber auch ok. Es gab durchaus Faelle, wo ich das auch gemacht habe aus guten Gruenden. ZB via Metaklassen alle Subklassen registrieren etc. Nur sollte davor natuerlich gewarnt werden.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@sls: ja, das ist eine typische Fehlinterpretation von Klassenattributen, dass sie dafür da sind, Instanzen zu sammeln. Wenn man soetwas tatsächlich möchte, liefert der Garbage-Collector bessere Methoden (was aber wirklich nur zu Debugging-Zwecken genutzt werden sollte). Klassenattribute verwende ich eigentlich fast ausschließlich als Klassen-Konstanten.

Um __blackjack__s Punkt nochmal deutlich zu machen, warum eine schlechte Idee ist, von `list` zu erben:

Code: Alles auswählen

class ContactList(list):
    pass

a = ContactList([1,2,3])
b = ContactList([7,6,8])
print(a + b)
# [1, 2, 3, 7, 6, 8]
print(type(a), type(b), type(a + b))
# <class '__main__.ContactList'>, <class '__main__.ContactList'>, <type 'list'>
Bei einfachen Operationen ändert sich der Typ der Klasse.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Sirius3 hat geschrieben: Mittwoch 14. November 2018, 08:57 Sollte es sich tatsächlich um eine abstrakte Klasse handeln, würde ich nichts mit abc, etc. machen, sondern einfach die Klasse AbstractXY nennen, dann ist dem Leser viel klarer, dass es sich eine abstrakte Klasse ist.
Bei abstrakten Klassen *müssen* alle Methoden und Attribute in der erbenden Klasse implementiert sein, oder sehe ich das falsch? Ich werde gerade nicht schlau aus der Beschreibung von python-course.eu:
Abstract classes may not be instantiated, and require subclasses to provide implementations for the abstract methods. Subclasses of an abstract class in Python are not required to implement abstract methods of the parent class.
EDIT: so, ich meine ich hab's verstanden. Wenn eine erbende Klasse nicht alle Methoden / Attribute einer abstrakten Klasse implementiert, würde sie "theoretisch" selbst auch nur wieder eine irgendwie-so-halb-abstrakte Klasse darstellen.

Wenn du das abc-Modul nicht verwendest, wie machst du es dann? Einen `NotImplementedError` schmeißen, oder verzichtest du darauf den Benutzer zu zwingen den ganzen Krempel der abstrakten Klasse in der erbenden Klasse zu implementieren?
When we say computer, we mean the electronic computer.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@sls: Python ist dynamisch Typisiert. Da haben abstrakte Klassen rein dekorative Funktion, also eigentlich gar keine.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Alles klar, vielen Dank an alle. Eine letzte Frage habe ich noch. Ich verwende öfter das Tornado-Webframework und dort gibt es diese Klasse:

Code: Alles auswählen

class RequestHandler(object):
    """Base class for HTTP request handlers.

    Subclasses must define at least one of the methods defined in the
    "Entry points" section below.
    """
    SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PATCH", "PUT",
                         "OPTIONS")

    _template_loaders = {}  # type: typing.Dict[str, template.BaseLoader]
    _template_loader_lock = threading.Lock()
    _remove_control_chars_regex = re.compile(r"[\x00-\x08\x0e-\x1f]")

    def __init__(self, application, request, **kwargs):
        super(RequestHandler, self).__init__()

        self.application = application
        self.request = request
        ...
Wozu ist eigentlich super(RequestHandler, self).__init__() gut? In RequestHandler wird doch ein eigener initializer definiert, wozu dann den aus object dazu holen, oder was verstehe ich hier nicht?
When we say computer, we mean the electronic computer.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ist nötig, um bei Mehrfachvererbung sicherzustellen, dass alle __init__-Methoden aufgerufen werden.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Sorry, ich hab's noch nicht ganz begriffen. Ich hab' dazu folgenden Test probiert:

Code: Alles auswählen

class A:
    def __init__(self):
        print("A has been called.")
        super().__init__()


class B(A):
    def __init__(self):
        print("B has been called.")
        super().__init__()


class C(B):
    def __init__(self):
        print("C has been called.")
        super().__init__()


c = C()
Selbst wenn super().__init__() in `A` fehlt, wird A.__init__() über B.__init__() aufgerufen. Aktuell verstehe ich das so, dass super() Methoden und Eigenschaften der Elternklasse übernimmt, von der man direkt erbt.
When we say computer, we mean the electronic computer.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dann lade mal B NICHT von A ab, aber dann C von A & B. Ohne super in A oder B wird eines der beiden eben nicht aufgerufen.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Ok, ich hab weiter herumprobiert:

Code: Alles auswählen

class A:
    def __init__(self):
        print("A has been called.")


class B:
    def __init__(self):
        print("B has been called.")


class C(A, B):
    def __init__(self):
        print("C has been called.")
        super().__init__()


c = C()
Ergebnis:

C has been called.
A has been called.

Tausche ich die Reihenfolge zu C(B, A):

C has been called.
B has been called.

füge ich nun super().__init__() in `B` hinzu:

C has been called.
B has been called.
A has been called.

Für mich ergibt sich daraus, dass die MRO mich erstmal ziemlich mürbe gemacht hat. Ich merke mir einfach, bei Mehrfachvererbung immer super().__init__() in den Basisklassen zu implementieren. Das macht wohl alles sinn, wenn C(A, B) ist, *muss* super().__init__() in A vorkommen, damit auch B.__init__() aufgerufen wird. Ist die Reihenfolge C(B,A) muss super().__init__() in B vorkommen, damit auch A.__init__() aufgerufen wird.

Danke für die Hilfe!
When we say computer, we mean the electronic computer.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na du musst halt immer super aufrufen. Eigentlich in jeder Klasse. Weißt ja nicht, ob die mal abgelitten wird. Mache ich zugegeben aber auch nicht. Man muss es halt wissen.
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

Verstehe, ich glaube da muss ich erst noch ein Gefühl für bekommen. Um's mir heute nochmal richtig zu geben habe ich noch ein weiteres Beispiel bearbeitet, ich glaube jetzt habe ich mal ein Grundverständnis für den ganzen Zauber:

Code: Alles auswählen

class BaseClass:
    num_base_calls = 0

    def call_me(self):
        print(4)
        print("Calling method on Base Class")
        self.num_base_calls += 1


class LeftSubClass(BaseClass):
    num_left_sub_calls = 0

    def call_me(self):
        print(3)
        super().call_me()
        print("Calling method on Left Sub Class")
        self.num_left_sub_calls += 1


class RightSubClass(BaseClass):
    num_right_sub_calls = 0

    def call_me(self):
        print(2)
        super().call_me()
        print("Calling method on Right sub Class")
        self.num_right_sub_calls += 1


class SubClass(LeftSubClass, RightSubClass):
    num_sub_calls = 0

    def call_me(self):
        print(1)
        super().call_me()
        print("Calling method on Sub Class")
        self.num_sub_calls += 1


s = SubClass()
s.call_me()

print("base: ", s.num_base_calls)
print("left sub: ", s.num_left_sub_calls)
print("right sub: ", s.num_right_sub_calls)
print("sub: ", s.num_sub_calls)
Ausgabe:

1
3
2
4
Calling method on Base Class
Calling method on Right sub Class
Calling method on Left Sub Class
Calling method on Sub Class
base: 1
left sub: 1
right sub: 1
sub: 1

Jetzt weiß ich auch, warum mein ursprünglicher Ansatz via A.__init__(), B.__init__() in der __init__()-Methode von C potentiell in die Hose gehen könnte, bei obigen Beispiel fiel mir auf, dass dabei BaseClass doppelt aufgerufen wird. Jetzt werde ich von super() träumen %)
When we say computer, we mean the electronic computer.
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Also ich finde super alles andere als super und in meinem Code habe ich einfach grundsätzlich keine Mehrfachvererbung ausser in seltenen Fällen Mixins wo das nicht nötig ist. Das macht mir nur Kopfschmerzen für etwas was ich noch nie gebraucht habe und auch nicht brauchen werde. Ich empfehle zu dem Thema diese Lektüre: Python's Super is nifty, but you can't use it.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
sls
User
Beiträge: 480
Registriert: Mittwoch 13. Mai 2015, 23:52
Wohnort: Country country = new Zealand();

@__blackjack__: ich muss gestehen dass mich super() ziemlich verunsichert. Laut dem von dir verlinkten Artikel soll man super() in Subklassen immer implementieren, wenn es denn die Basisklasse auch tut, ich hatte ja ein wenig die Hoffnung dass ich super() einfach meiden könnte, für das folgende Problem habe ich allerdings keine alternative, wenn `A` oder `B` super() implementiert:

Code: Alles auswählen

class A:
    def __init__(self, a1, a2, *args, **kwargs):
        print("A has been called.")
        super().__init__(*args, **kwargs)
        self.a1 = a1
        self.a2 = a2

class B:
    def __init__(self, b1, b2, *args, **kwargs):
        print("B has been called.")
        super().__init__(*args, **kwargs)
        self.b1 = b1
        self.b2 = b2


class C(A, B):
    def __init__(self, a1, a2, b1, b2, c1, c2, *args, **kwargs):
        super().__init__(a1, a2, b1, b2, *args, **kwargs)
        self.c1 = c1
        self.c2 = c2


c = C(1, 2, 3, 4, 5, 6)
print(c.a1, c.a2, c.b1, c.b2, c.c1, c.c2)
Gerade die ganzen Webframeworks mit denen ich öfter herummache nutzen munter super, teilweise ist die Beschreibung nicht ganz so toll, so dass ich bei Mehrfachvererbung teilweise Schwierigkeiten habe überhaupt zu erkennen *woher* welche Argumente kommen sollen.
When we say computer, we mean the electronic computer.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Es ist in der Realität ein ziemliches nicht-Problem. Es zu verstehen hilft wenn man es mal braucht. Doch das ist selten. Wir sind ja nicht Java.
Antworten