#!/bin/env python3
#
# einfaches OOP-Beispiel

# eine eigene Zähler-Klasse definieren
class counter(object):
    'Klasse zur Realisierung eines Zählers'

    # generell sollten nur new-style classes genutzt werden, also jene, die von
    # object abgeleitet sind; bei Python 2 muss man die Basisklasse object
    # explizit angeben, bei Python 3 kann diese Angabe entfallen, da es dort
    # keine old-style classes mehr gibt:
    # 
    #   class counter(): ...

    # Klassenvariable mit Vorzugs-Startwert 0 anlegen; sie kann von außen
    # überschrieben werden
    init_val = 0

    # der erste Parameter jeder Instanz-Methode erhält die Referenz auf das
    # jeweilige Objekt und sollte immer 'self' genannt werden

    def __init__(self, *args):
        # Konstruktor/Initialisierer (constructor/initializer):
        # Initialisierung eines neuen Zähler-Objekts; der Startwert wird im
        # Instanz-Dictionary gespeichert; Vorzugs-Startwert ist das Klassenattribut
        # init_val
        self.val = args[0] if args else self.init_val

    def count(self, n=1):
        # Zähler um n inkrementieren
        self.val += n

    def get(self):
        # Zählerstand auslesen
        return self.val

# Ableitung einer Klasse von einem eingebauten Datentyp
class plist(list):
    'plist ist eine Liste, die alle append-Operationen protokolliert'

    # Methode append() von Klasse list wird überschrieben
    def append(self, x):
        print('appending', x)
        # Methode append() der Basis- bzw. Superklasse wird gerufen
        super().append(x)
        # bei Python 3 kann super() parameterlos gerufen werden, bei Python
        # benötigte super() 2 Argumente, z.B.:
        #   super(plist, self).append(x)
        # ohne das empfohlene super() ginge es auch so:
        #   list.append(self, x)

# 3 Counter-Objekte erzeugen
c1 = counter(5)       # Startwert 5
c2 = counter()        # Startwert 0
counter.init_val = 9  # Vorzugs-Startwert in der Klasse auf 9 setzen
c3 = counter()        # Startwert 9

for c in c1, c2, c3:  # Zählerstände über get() auslesen
    print(c.get())

c1.count()            # Zähler um 1 erhöhen
print(c1.get())

c1.count(8)           # Zähler um 8 erhöhen
print(c1.get())

# die Instanz-Attribute sind öffentlich, man kann sie direkt auslesen oder
# manipulieren
print(c1.val, c2.val, c3.val)
c1.val += 10
c2.val -= 1
c3.val *= 5
print(c1.val, c2.val, c3.val)

c1.val, c2.val, c3.val = 5, 10, 15
c1.val *= 8
print(c1.val, c2.val, c3.val)

# Anzeige des Instanz-Dictionarys objekt.__dict__
for o in c1, c2, c3:
    print(vars(o), o.__dict__)

# mit dir() sieht man die Namen der Instanz-Attribute, wobei im Gegensatz zu
# vars() auch die Basisklassen betrachtet werden
print(dir(c1))

# Hinweis: die innerhalb einer Klassendefinition verwendeten Namen von
# Attributen und Methoden, die mit 2 Unterstrichen beginnen und nicht mit
# mindestens 2 Unterstrichen enden, unterliegen einem sog. name mangling; dabei
# wird der Name mit einem Präfix versehen, das aus einem Unterstrich gefolgt
# vom Klassennamen besteht; durch dieses "Verstecken" lassen sich
# Namenskollisionen vermeiden; die Namen bleiben aber weiter öffentlich 

class C(object):
    __x = 10  # name mangling: aus _x  wird _C__x

print(C()._C__x)

print()

# eine Instanz von plist anlegen
pl = plist()
print(pl)

# Element protokolliert anfügen
pl.append(88)
print(pl)

print()

# Test auf Klassenzugehörigkeit
print(isinstance(pl, counter)) # False
print(isinstance(pl, plist))   # True
print(isinstance(pl, list))    # True

# Test auf Subklasse
print(issubclass(plist, list))     # True
print(issubclass(plist, object))   # True
print(issubclass(plist, counter))  # False
print(issubclass(counter, object)) # True

print()

# Ermittlung der Klasse und des Tupels aller Basisklassen sowie der MRO (method
# resolution order)
for o in c1, pl:
    print(o)
    print('__class__ ==>', o.__class__)
    print('__bases   ==>', type(o).__bases__)
    print('__mro__   ==>', type(o).__mro__)
    print()
