Objekt-Modell von Python 3

Ein Teil der folgenden Beispiele stammt aus dem Buch „Python Essential Reference“ von David M. Beazley und werden mit freundlicher Erlaubnis des Autors in teilweise angepasster Form genutzt.

Der Attribut-Zugriff erfolgt nach einem speziellen Algorithmus.

Klassendefinition

# -*- coding: utf8 -*-

import gc

# Klassendefinition

class Account(object):
    # Ableitung von object kann bei Python 3 entfallen:
    #   class Account:

    # Klassenvariable
    num_accounts = 0

    # Initialisierer; de facto kein Konstruktor, da das Objekt hier schon
    # existiert
    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        Account.num_accounts += 1

    # "Finalisierer" (de facto kein Destruktor); wird erst beim Zerstören des
    # Objekts gerufen, also wenn der Referenzzähler auf 0 sinkt; bei zyklischen
    # Referenzen werden Objekte mit __del__() vor Python 3.4 nicht zerstört; es
    # ist nicht garantiert, dass __del__() für Objekte gerufen wird, die beim
    # Beenden des Interpreters noch existieren
    #
    # siehe auch
    #   https://docs.python.org/3/reference/datamodel.html#object.__del__
    #
    #   https://docs.python.org/3/whatsnew/3.4.html#pep-442-safe-object-finalization
    #
    #     PEP 442 removes the current limitations and quirks of object
    #     finalization in CPython. With it, objects with __del__() methods, as
    #     well as generators with finally clauses, can be finalized when they
    #     are part of a reference cycle.
    def __del__(self):
        Account.num_accounts -= 1
        print('__del__', self.name)

    def deposit(self, amt):
        self.balance = self.balance + amt

    def withdraw(self, amt):
        self.balance = self.balance - amt

    def inquiry(self):
        return self.balance

if __name__ == '__main__':
    # Instanzen

    # einige Accounts anlegen
    a = Account('Guido', 1000.00)  # ruft Account.__init__(a, 'Guido', 1000.00)
    b = Account('Bill', 10.00)
    c = Account('Susan', 4000.00)
    d = Account('Mary', 500.00)

    # Attributzugriff
    print(Account.num_accounts)
    print(Account.__init__)
    print(Account.__del__)
    print(Account.deposit)
    print(Account.withdraw)
    print(Account.inquiry)

    a.deposit(100.00)   # ruft Account.deposit(a, 100.00)
    b.withdraw(50.00)   # ruft Account.withdraw(b, 50.00)
    name = a.name       # den Account-Namen ermitteln

    print(name, a.balance)
    print(b.name, b.inquiry())

    # Objekte zerstören
    del a
    print('a deleted')  # __del__() wird gerufen

    b2 = b              # Referenzzähler wird erhöht
    del b
    print('b deleted')  # __del__() wird nicht gerufen, da b2 noch auf das Objekt verweist

    # zirkuläre Referenz erstellen
    c.x = d
    d.x = c
    del c, d
    gc.collect()        # den Garbage Collector gezielt rufen

    print('***')

Sichtbarkeitsregeln im Klassen-Körper

# spezielle Sichtbarkeitsregeln im Klassen-Körper

a = 'global a'

class X():
    # außerhalb von Methoden erfolgt der Zugriff auf Attribute der Klasse über
    # einfache Namen; in Methoden sind dagegen für Klassen- und Instanz-Attribute
    # immer voll qualifizierte Namen zu verwenden

    a = 'a in class'
    b = a.replace('a ', 'b ')
    # X.a statt a ist nicht zulässig, da X nicht sichtbar ist
    #
    # NameError: name 'X' is not defined

    def hello(self):
        print('hello ', self)

    def print_vars(self, my = a): # hier ist X.a als a sichtbar
        print(my)                 # a in class
        try:
            hello(1)              # hello() ist nicht sichtbar
        except Exception as e:
            print(repr(e))        # NameError("name 'hello' is not defined",)
        X.hello(2)                # X.hello() ist sichtbar
        print(self.a)             # a in class
        print(X.a)                # a in class
        print(a)                  # global a
        print(self.b)             # b in class
        print(b)                  # NameError: name 'b' is not defined

    # im Klassenkörper ist hello() sichtbar
    hello(3)

x = X()
x.print_vars()

Vererbung (inheritance)

# Vererbung (inheritance)

import random
from classdef import Account

class SimpleEvilAccount(Account):
    fake = False
    def inquiry(self):
        # das Fake-Flag könnte man zufällig setzen, z.B. so:
        #   self.fake = random.randint(0, 4) == 1
        # wir wollen das Fake-Flag immer negieren; beim ersten Ruf der Methode
        # inquiry() wird durch den Zugriff auf self.fake die Klassenvariable
        # SimpleEvilAccount.fake ausgelesen und deren Inhalt in die neu
        # erstellte Instanz-Variable self.fake übernommen; später wird stets
        # die Instanz-Variable negiert
        self.fake = not self.fake
        return self.balance * 1.10 if self.fake else self.balance

class EvilAccount(SimpleEvilAccount):
    def __init__(self, name, balance, evilfactor):
        super().__init__(name, balance)    # den Account initialisieren
        self.evilfactor = evilfactor

    def inquiry(self):
        self.fake = not self.fake
        return self.balance * self.evilfactor if self.fake else self.balance

class MoreEvilAccount(EvilAccount):
    def deposit(self, amount):
        self.withdraw(5.00)                 # Gebühr abziehen
        super().deposit(amount)             # Einzahlung ausführen
        # EvilAccount.deposit(self, amount) # Realisierung ohne das empfohlene super()

if __name__ == '__main__':
    print(MoreEvilAccount.__mro__)

    for c in (SimpleEvilAccount('George', 1000.00),
              EvilAccount('George Evil', 1000.00, 1.2),
              MoreEvilAccount('George More Evil', 1000.00, 1.3)):
        c.deposit(10.0)                     # ruft am Ende Account.deposit(c, 10.0)
        for _ in range(3): print(c.inquiry())
        print(isinstance(c, Account))
        print('---')

Mehrfachvererbung (multiple inheritance)

# Mehrfachvererbung (multiple inheritance)

from inheritance import EvilAccount

class number():
    def __init__(self, number):
        self.number = number

    def add(self, amt):
        print('number.add', amt)
        self.number += amt

# Mixin-Klasse
class addsub():
    def add(self, amt):
        # überschreibe number.add()
        print('addsub.add', amt)
        # rufe das überschriebene number.add()
        super().add(amt)

    def sub(self, amt):
        print('addsub.sub', amt)
        self.add(-amt)

class counter(addsub, number): pass
print(counter.__mro__)

n = number(10)
n.add(5)           # ruft number.add(n, 5)
print(n.number)
print('---')

c = counter(100)
c.add(10)          # ruft addsub.add(c, 10)
c.sub(5)
print(c.number)
print('---')
    
# Mixin-Klasse
class DepositCharge(object):
    fee = 5.00
    def deposit_fee(self, amt, fee = fee):
         # fee = fee übernimmt den Wert von DepositCharge.fee
        print('DepositCharge deposit_fee', amt, fee, self.fee)
        self.deposit(amt, fee)

# Mixin-Klasse
class WithdrawCharge(object):
    fee = 2.50
    def withdraw_fee(self, amt, fee = fee):
         # fee = fee übernimmt den Wert von WithdrawCharge.fee
        print('WithdrawCharge withdraw_fee', amt, fee, self.fee)
        self.withdraw(amt, fee)

# Klasse, die Mehrfachvererbung nutzt (normalerweise nur für die Einbindung von
# Mixins empfehlenswert)
class MostEvilAccount(EvilAccount, DepositCharge, WithdrawCharge):
    def deposit(self, amt, fee = 0):
        print('MostEvilAccount deposit', amt, fee, self.fee)
        super().deposit(amt - fee)

    def withdraw(self, amt, fee = 0):
        print('MostEvilAccount withdraw', amt, fee, self.fee)
        super().withdraw(amt + fee)

d = MostEvilAccount('Dave', 500.00, 1.10)
d.deposit_fee(20)    # ruft DepositCharge.deposit_fee()
d.withdraw_fee(10)   # ruft WithdrawCharge.withdraw_fee()
                     # self.fee hat wegen der MRO in beiden Fällen den Wert 5.00 (!);
                     # durch "fee = fee" bei deposit_fee() und withdraw_fee()
                     # wird der Wert der jeweiligen Klassenvariablen verwendet
for _ in range(2): print(d.inquiry())

print()
for cls in MostEvilAccount.__mro__: print(cls)
# <class '__main__.MostEvilAccount'>
# <class 'evilaccount.EvilAccount'>
# <class 'evilaccount.SimpleEvilAccount'>
# <class 'account.Account'>
# <class '__main__.DepositCharge'>
# <class '__main__.WithdrawCharge'>
# <class 'object'>

# ggf. ist keine MRO bestimmbar
class X(): pass
class Y(X): pass
class Z(X, Y): pass
    # TypeError: Cannot create a consistent method resolution
    # order (MRO) for bases X, Y

Mixin-Beispiel - ForkingMixIn

XML-RPC-Server

#!/usr/bin/env python3
# -*- coding: utf8 -*-

"""
Demo für die Mixin-Nutzung:
  - SocketServer stellt ForkingMixIn und ThreadingMixIn bereit
  - wir nutzen hier das ForkingMixIn beim SimpleXMLRPCServer
"""

from __future__ import print_function
import sys

try:
    from xmlrpc.server import SimpleXMLRPCServer      # Python 3
    from socketserver import ForkingMixIn
    py3 = True
except ImportError:                                   # Python 2
    from SimpleXMLRPCServer import SimpleXMLRPCServer
    from SocketServer import ForkingMixIn
    py3 = False

class MyXMLRPCServer(ForkingMixIn, SimpleXMLRPCServer):
    def verify_request(self, request, client_address):
        host, port = client_address
        if host != '127.0.0.1':
            print('invalid client', host, file = sys.stderr)
            return False

        if py3:
            return super().verify_request(request, client_address)

        # MyXMLRPCServer ist wegen seiner Basisklassen bei Python 2 eine old-style
        # class; daher funktioniert super() nicht und wir referenzieren die
        # Basisklasse über deren Namen und müssen den Parameter self übergeben
        return SimpleXMLRPCServer.verify_request(self, request, client_address)

# vom XML-RPC-Server bereitgestellte Additions-Funktion
def add(x, y):
    return x + y

server = MyXMLRPCServer(('', 45000))
server.register_function(add)
server.serve_forever()

XML-RPC-Klient

#!/bin/env python3

import xmlrpc.client, sys
# bei Python 2 ist xmlrpclib zu importieren

host = sys.argv[1] if len(sys.argv) > 1 else 'localhost'
s = xmlrpc.client.ServerProxy('http://%s:45000' % host)
print(s.add(3, 4))

Polymorphie, Dynamische Bindung und Duck Typing

# Polymorphie, Dynamische Bindung und Duck Typing

# https://en.wikipedia.org/wiki/Duck_typing#Example

class Parrot:
    def fly(self): print('Parrot flying')

class Airplane:
    def fly(self): print('Airplane flying')

class Whale:
    def swim(self): print('Whale swimming')

def lift_off(entity): entity.fly()

parrot = Parrot()
airplane = Airplane()
whale = Whale()

lift_off(parrot)   # Parrot flying
lift_off(airplane) # Airplane flying
lift_off(whale)    # Ausnahme: 'Whale' object has no attribute 'fly'

Statische und Klassenmethoden

# Statische und Klassenmethoden

import time

class Foo:
     @staticmethod
     def add(x, y):
         return x + y

print(Foo.add(3, 4))       # 7
print('---')

class Times:
    factor = 1
    @classmethod
    def mul(cls, x):
        return cls.factor * x

class TwoTimes(Times):
    factor = 2

print(TwoTimes.mul(4))      # ruft logisch Times.mul(TwoTimes, 4) -> 8

# reale alternative Rufmöglichkeiten

# TwoTimes.mul ==> <bound method Times.mul of <class '__main__.TwoTimes'>>
v = Times.__dict__['mul']              # <classmethod object at 0x...>
f = type(v).__get__(v, None, TwoTimes) # <bound method Times.mul of <class '__main__.TwoTimes'>>
print(f(4))

# Times.__dict__['mul'].__get__(TwoTimes()) ==>
# <bound method Times.mul of <class '__main__.TwoTimes'>>
print(Times.__dict__['mul'].__get__(TwoTimes())(4))

# Times.__dict__['mul'].__func__ ==>
# <function Times.mul at 0x7fcf531479d8>
print(Times.__dict__['mul'].__func__(TwoTimes, 4))

print('---')

class Date(object):
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __str__(self):
        # String-Darstellung einer Date-Instanz
        return '%04d-%02d-%02d' % (self.year, self.month, self.day)

    @staticmethod
    def now():
         t = time.localtime()
         return Date(t.tm_year, t.tm_mon, t.tm_mday)

    @staticmethod
    def tomorrow():
         t = time.localtime(time.time() + 86400)     # 86400 Sekunden (1 Tag) addieren
         return Date(t.tm_year, t.tm_mon, t.tm_mday)

    @classmethod
    def cnow(cls):
        t = time.localtime()
        # kreiert ein Objekt vom passenden Typ (cls)
        return cls(t.tm_year, t.tm_mon, t.tm_mday)

class EuroDate(Date):
    # überschreibt Date.__str__(), liefert ein europäisches Datumsformat
    def __str__(self):
        return '%02d/%02d/%4d' % (self.day, self.month, self.year)

# einige Datumsobjekte erstellen
a = Date(1967, 4, 9)
e = EuroDate(1967, 4, 9)
n = Date.now()       # ruft die statische Methode now()
t = Date.tomorrow()  # ruft die statische Methode tomorrow()
D = e.now()          # ruft Date.now() und liefert ein Date-Objekt
A = a.cnow()         # ruft logisch Date.cnow(Date) und liefert ein Date-Objekt
E = e.cnow()         # ruft logisch Date.cnow(EuroDate) und liefert ein EuroDate-Objekt

# real nutzbarer Ruf der classmethod cnow
print(Date.__dict__['cnow'].__func__(Date))     
print(Date.__dict__['cnow'].__func__(EuroDate))
print('---')

for x in a, e, n, t, D, A, E:
    print(x, vars(x), type(x))
print('---')

Properties

# Properties
#
# dabei handelt es sich um spezielle Deskriptoren

import math

class Circle(object):
     def __init__(self, radius):
         self.radius = radius

     # einige zusätzliche Eigenschaften von Circle-Objekten
     @property
     def area(self):
         # Kreisfläche: π * r²
         return math.pi * self.radius ** 2

     @property
     def perimeter(self):
         # Kreisumfang: 2 * π * r
         return 2 * math.pi * self.radius

c = Circle(4.0)
print(c.radius)    # 4.0
print(c.area)      # 50.26548245743669
print(c.perimeter) # 25.132741228718345

# c.area = 2
# Traceback (most recent call last):
#   File '<stdin>', line 1, in <module>
# AttributeError: can't set attribute

print(Circle.area)            # <property object at 0x...>
print(type(Circle.area))      # <class 'property'>

print(type(Circle.__init__))  # <class 'function'>
print(Circle.__init__)        # <function Circle.__init__ at 0x...>
print(type(c.__init__))       # <class 'method'>
print(c.__init__)             # <bound method Circle.__init__ of <__main__.Circle object at 0x...>>

# Hinweis:
# der Zugriff auf das Methoden-Objekt __init__ über die Instanz (c.__init__)
# liefert kein Funktions-Objekt, sondern eine gebundene Methode (bound method),
# da alle Funktionen eine Methode __get__() haben und so als Deskriptor wirken;
# eine Methode könnte man hier auch als eine Art Property verstehen
#
# siehe auch https://docs.python.org/3.6/howto/descriptor.html#functions-and-methods

class Foo(object):
    def __init__(self, name):
        self.__name = name

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
             raise TypeError('Must be a string!')
        self.__name = value

    @name.deleter
    def name(self):
        raise TypeError("Can't delete name")

f = Foo('Guido')
f.name = 'Monty'   # ruft setter name(f, 'Monty')
print(f.name)      # ruft getter name()
members = frozenset(dir(f))
print('members', members)
try:
    f.name = 45    # ruft setter name(f, 45) -> TypeError
    del f.name     # ruft deleter name(f) -> TypeError
except Exception as e:
    print(e)
print('---')

# älterer Code mit property() statt @property
#
# bei der Dekorator-Variante (@property) sind die get/set/delete-Funktionen
# nicht als Methoden der Klasse sichtbar
class Foo(object):
    def __init__(self, name):
        self.__name = name

    def getname(self):
        return self.__name

    def setname(self, value):
        if not isinstance(value, str):
             raise TypeError('Must be a string!')
        self.__name = value

    def delname(self):
        raise TypeError("Can't delete name")

    name = property(getname, setname, delname)

members2 = frozenset(dir(Foo('Max')))
print(members - members2) # frozenset()
print(members2 - members) # frozenset({'setname', 'getname', 'delname'})

Descriptoren (descriptors)

# Descriptoren (descriptors)
#
# https://docs.python.org/3.6/howto/descriptor.html

from weakref import WeakKeyDictionary

# ein Deskriptor ist ein Objekt, das den Wert eines Attributs repräsentiert; es
# implementiert eine oder mehrere der Methoden __get__(), __set__() und
# __delete__(); Deskriptoren sind nur auf Klassen-Ebene instanziierbar, nicht
# auf der Instanz-Ebene

class TypedProperty(object):
    def __init__(self, name, typ, default = None):
        self.name = '_' + name
        self.type = typ
        self.default = typ() if default is None else default

    def __get__(self, instance, cls):
        # wird __get__() des Deskriptor-Objekts für die Instanz None gerufen,
        # dann wird eine Referenz auf das Deskriptor-Objekts selbst (self)
        # zurückgegeben; siehe auch
        #   https://docs.python.org/3.6/howto/descriptor.html#properties
        # Properties liefern bei __get__() für Objekt None ebenfalls self
        return getattr(instance, self.name, self.default) if instance else self

    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError('Must be a %s' % self.type)
        setattr(instance, self.name, value)

    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute")

class Foo(object):
    name = TypedProperty('name', str)
    num  = TypedProperty('num', int, 42)

# https://www.smallsurething.com/python-descriptors-made-simple/
class Price(object):
    def __init__(self):
        self.default = 0
        self.values = WeakKeyDictionary()

    def __get__(self, instance, owner):
        return self.values.get(instance, self.default)

    def __set__(self, instance, value):
        if value < 0 or value > 100:
            raise ValueError('Price must be between 0 and 100.')
        self.values[instance] = value

    def __delete__(self, instance):
        del self.values[instance]

class Book(object):
    # den Preis eines Buches speichern wir zentral im Deskriptor-Objekt
    price = Price()

    def __init__(self, author, title, price):
        self.author = author
        self.title = title
        self.price = price

    def __str__(self):
        return '{0} - {1}'.format(self.author, self.title)

if __name__ == '__main__':
    f = Foo()
    f.name = 'Guido'     # ruft Foo.name.__set__(f, 'Guido')
    print(f.name)        # ruft implizit Foo.name.__get__(f, Foo)
    print(Foo.name.__get__(f, Foo))
                         # <bound method TypedProperty.__get__ of <__main__.TypedProperty object at 0x...>>
    print(f.num)         # 42 (default)
    f.num = 88
    print(f.num)         # 88
    try:
        del f.name       # ruft Foo.name.__delete__(f)
    except Exception as e:
        print(e)         # Can't delete attribute

    b1 = Book('William Faulkner', 'The Sound and the Fury', 12)
    b2 = Book('John Dos Passos', 'Manhattan Transfer', 13)
    b3 = Book('George Orwell', '1984', 14)
    print(b1.price, b2.price, b3.price)

    # direkter Zugriff auf das Deskriptor-Objekt vom Typ Price
    books = b1.__class__.__dict__['price'].values

    # Book b2 löschen
    del b2

    # Kontrollausgabe des Inhalts des WeakKeyDictionarys (b2 ist bereits
    # entfernt)
    print(books.data)

    # das WeakKeyDictionary durchlaufen und die Books ausgeben
    for book in books:
        print('%s: %s ; %s' % (book.author, book.title, books[book]))

Datenkapselung und private Attribute

# Datenkapselung und private Attribute

class A(object):
    def __init__(self):
        self.__X = 3        # wird zu to self._A__X
    def __spam(self):       # wird zu _A__spam()
        print('A.__spam', vars(self))
    def spam(self):
        print('A.spam', vars(self))
    def bar(self):
        self.__spam()       # ruft nur A.__spam()
        self.spam()         # ruft A.spam() oder B.spam()

class B(A):
    def __init__(self):
        A.__init__(self)
        self.__X = 37       # wird zu self._B__X
    def __spam(self):       # wird zu _B__spam()
        print('B.__spam', vars(self))
    def spam(self):
        print('B.spam', vars(self))

# über diese privaten Namen (name mangling) kann eine Superklasse verhindern,
# dass eine abgeleitete Klasse die Implementation der Methode ändert; A.bar()
# ruft generell A.__spam(), unabhängig vom Typ von self oder der Existenz der
# Methode __spam() in der abgeleiteten Klasse

A().bar()
B().bar()

Objekt-Erzeugung

# Objekt-Erzeugung

class Circle(object):
    def __init__(self, radius):
         self.radius = radius

# einige Circle-Instanzen schaffen
c = Circle(4.0)
d = Circle(5.0)

# dabei wird normalerweise von der Methode __call__() der Metaklasse zuerst
# __new__() und dann __init__() gerufen, sofern von __new__() ein Objekt vom
# erwarteten Typ (hier Circle) geliefert wurde
#
# siehe auch:
#
#   https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/#putting-it-all-together
#
e = Circle.__new__(Circle, 6.0)
if isinstance(c, Circle):
    Circle.__init__(e, 6.0) # dieser Wert ist hier relevant für self.radius

for x in c, d, e:
    print(vars(x))

# bei der Ableitung von einem unveränderlichen Typ kann man in __new__()
# Einfluss auf den Wert nehmen; außerdem spielt __new__() typischerweisen in
# Metaklassen eine Rolle
class Upperstr(str):
    def __new__(cls,value=''):
        return str.__new__(cls, value.upper())

u = Upperstr('hello')     # u = 'HELLO'

Referenz-Zähler und Garbage Collection

# Referenz-Zähler und Garbage Collection

import sys, gc, weakref

# Realisierung des Observer Patterns: Implementierung ohne weakref
class Account(object):
    def __init__(self, name, balance):
         self.name = name
         self.balance = balance
         self.observers = set()
    def __del__(self):
         print('Account: del called for', self)
         for ob in self.observers:
             ob.close()
         del self.observers
    def register(self,observer):
        self.observers.add(observer)
    def unregister(self,observer):
        self.observers.remove(observer)
    def notify(self):
        for ob in self.observers:
            ob.update()
    def withdraw(self,amt):
        self.balance -= amt
        self.notify()

class AccountObserver(object):
     def __init__(self, theaccount):
         self.theaccount = theaccount
         theaccount.register(self)
     def __del__(self):
         print('AccountObserver: del called for', self)
         self.theaccount.unregister(self)
         del self.theaccount
     def update(self):
         print('Balance is %0.2f' % self.theaccount.balance)
     def close(self):
         print('Account no longer in use')

# Observer Pattern: Implementierung mit weakref (besser)
class AccountObserver2(object):
     def __init__(self, theaccount):
         self.accountref = weakref.ref(theaccount)  # eine weakref erzeugen
         theaccount.register(self)
     def __del__(self):
         acc = self.accountref()    # den Account über die weakref holen
         if acc:                    # den Observer austragen, wenn der Account noch existiert
               acc.unregister(self)
     def update(self):
         print('Balance is %0.2f' % self.accountref().balance)
     def close(self):
         print('Account no longer in use')

a = Account('Dave', 1000.00)
a_ob = AccountObserver(a)
a2 = Account('Dave', 1500.00)
a_ob2 = AccountObserver2(a2)

del a, a_ob
gc.collect()
print('\nnach a, a_ob und gc.collect\n')

del a2, a_ob2
gc.collect()
print('nach a2, a_ob2 und gc.collect\n')

# Ausgabe:
#
# Account: del called for <__main__.Account object at 0x...>
# Account no longer in use
# AccountObserver: del called for <__main__.AccountObserver object at 0x...>
# Exception ignored in: <bound method AccountObserver.__del__ of <__main__.AccountObserver object at 0x...>>
# Traceback (most recent call last):
#   File 'Account_Observer.py', line 31, in __del__
#     self.theaccount.unregister(self)
#   File 'Account_Observer.py', line 17, in unregister
#     self.observers.remove(observer)
# AttributeError: 'Account' object has no attribute 'observers'
# 
# nach a, a_ob und gc.collect
# 
# Account: del called for <__main__.Account object at 0x...>
# Account no longer in use
# 
# nach a2, a_ob2 und gc.collect

Objekt-Repräsentation und Attribut-Bindung

# Objekt-Repräsentation und Attribut-Bindung

from garbage_collection import Account

a = Account('Guido', 1100.0)
print(a.__dict__)              # Dictionary der Instanz-Attribute
# {'name': 'Guido', 'balance': 1100.0, 'observers': set()}

a.number = 123456              # Attribut 'number' wird zu a.__dict__ hinzugefügt

print(a.__class__)             # die Klasse des Objekts
# <class '__main__.Account'>

print(Account.__dict__.keys()) # im Dictionary der Klasse stehen deren Methoden

# dict_keys(['__module__', '__init__', '__del__', 'register', 'unregister',
# 'notify', 'withdraw', '__dict__', '__weakref__', '__doc__'])

print(Account.__bases__)       # die Basis-Klassen der Klasse
# (<class 'object'>,)

print(Account.__mro__)         # MRO - method resolution order (gilt für alle
                               # Attribute, nicht nur Methoden)
# (<class '__main__.Account'>, <class 'object'>)

# der Attribut-Zugriff beim Lesen, Schreiben und Löschen erfolgt nach einem
# etwas komplexeren Algorithmus, der Deskriptoren beachtet:
#
#   https://www-user.tu-chemnitz.de/~hot/PYTHON/#attr_access

# hier werden die speziellen Methoden __getattr__() und __setattr__()
# implementiert
class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    def __getattr__(self, name):
        if name == 'area':
             return math.pi * self.radius ** 2
        elif name == 'perimeter':
             return 2 * math.pi * self.radius
        else:
             return super().__getattr__(name)

    def __setattr__(self, name, value):
        if name in ('area', 'perimeter'):
             raise TypeError('%s is readonly' % name)
        object.__setattr__(self, name, value)

__slots__

# __slots__

# - Festlegung der Attribute der Klasse
# - kein Sicherheits-Feature, sondern Verbesserung der Effizienz
# - Attribute werden über Deskriptoren realisiert
# - standardmäßig wird auf __dict__ verzichtet
# - man kann aber den Slot __dict__ benennen, dann hat man wieder ein __dict__

class Account(object):
    __slots__ = 'name', 'balance'

a = Account()
a.name = 'Dave'
try:
    a.age = 40
except Exception as e:
    print(repr(e))
# AttributeError("'Account' object has no attribute 'age'",)

print(type(Account.balance))
# <class 'member_descriptor'>

class Account(object):
    __slots__ = 'name', 'balance', '__dict__'

a = Account()
a.age = 40
print(vars(a))

Operator-Überladung

# Operator-Überladung

# Hinweis: Python hat den eingebauten Datentyp Complex, die folgende
# Implementierung ist daher in der Praxis unnötig
#
#   type(2+3j)
#   <class 'complex'>
class Complex(object):
    def __init__(self, real, imag = 0):
        self.real = float(real)
        self.imag = float(imag)
    def __repr__(self):
        return 'Complex(%s,%s)' % (self.real, self.imag)
    def __str__(self):
        return '(%g+%gj)' % (self.real, self.imag)

    # self + other
    def __add__(self, other):
        print('_add__', self, other)
        return Complex(self.real + other.real, self.imag + other.imag)
    def __radd__(self,other):
        print('_radd__', self, other)
        return Complex(other.real + self.real, other.imag + self.imag)

    # self - other
    def __sub__(self, other):
        print('_sub__', self, other)
        return Complex(self.real - other.real, self.imag - other.imag)
    def __rsub__(self, other):
        print('_rsub', self, other)
        return Complex(other.real - self.real, other.imag - self.img)

c = Complex(2,3)
print(c + 4.0)
print(4.0 + c)
print(repr(c + 4.0))
print(repr(4.0 + c))

# ('_add__', Complex(2.0,3.0), 4.0)
# (6+3j)
# ('_radd__', Complex(2.0,3.0), 4.0)
# (6+3j)
# ('_add__', Complex(2.0,3.0), 4.0)
# Complex(6.0,3.0)
# ('_radd__', Complex(2.0,3.0), 4.0)
# Complex(6.0,3.0)

# Complex.__add__() funktioniert, da die eingebauten Zahlen die Attribute real
# und imag haben
#
# mit __radd__() und __rsub__() funktionieren auch Ausdrücke wie
#   4.0 + c  ==> c.__radd__(4.0)
#   4.0 - c  ==> c.__rsub__(4.0)
#
# wenn 4.0 + c scheitert, wird __radd__() versucht, bevor Python einen
# TypeError wirft

# ohne __radd__() sähe es so aus:
4.0 + c
# Traceback (most recent call last):
#   File '<stdin>', line 2, in <module>
# TypeError: unsupported operand type(s) for +: 'float' and 'Complex'

Typen und Tests auf Klassenzugehörigkeit

# Typen und Tests auf Klassenzugehörigkeit

class A(object): pass
class B(A): pass
class C(object): pass

a = A()          # Instanz von A
b = B()          # Instanz von B
c = C()          # Instanz von C

type(a)          # <class '__main__.A'>
isinstance(a, A) # True
isinstance(b, A) # True (B ist von A abgeleitet)
isinstance(b, C) # False (C ist nicht von A abgeleitet)

issubclass(B, A) # True
issubclass(C, A) # False

class Foo(object):
    def spam(self, a, b):
        pass

# FooProxy und Foo sind sind funktionell identisch, aber nicht voneinander
# abgeleitet
class FooProxy(object):
    def __init__(self, f):
        self.f = f
    def spam(self, a, b):
        return self.f.spam(a, b)

f = Foo()
g = FooProxy(f)
isinstance(g, Foo)  # False

# Gruppierung funktionell identischer Klassen (Klassen mit demselben Interface)
#
# allg. besser ist die Gruppierung über Abstrakte Basisklassen
class IClass(object):
    def __init__(self):
         self.implementors = set()
    def register(self,C):
         self.implementors.add(C)
    def __instancecheck__(self, x):
         # von isinstance gerufen
         return self.__subclasscheck__(type(x))
    def __subclasscheck__(self, sub):
         # von issubclass gerufen
         return any(c in self.implementors for c in sub.__mro__)

IFoo = IClass()
IFoo.register(Foo)
IFoo.register(FooProxy)

f = Foo()
g = FooProxy(f)
isinstance(f, IFoo)        # True
isinstance(g, IFoo)        # True
issubclass(FooProxy, IFoo) # True

Abstrakte Basisklassen (ABC, Abstract Base Classes)

# Abstrakte Basisklassen (ABC, Abstract Base Classes)

from abc import ABCMeta, abstractmethod, abstractproperty

class Foo(metaclass = ABCMeta):
    @abstractmethod
    def spam(self, a, b):
        print('spam')
    @abstractproperty
    def name(self):
        pass

# ABCs kann man nicht instanziieren:
#
# f = Foo()
#
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: Can't instantiate abstract class Foo with abstract methods name, spam

# zulässig ist der Ruf abstrakter Methoden aus einer Subklasse
class Bar(Foo):
    def spam(self, a, b):
        super().spam(a, b)
    def name(self):
        pass

b = Bar()
b.spam(1, 2)

# Klassen können sich bei einer ABC registrieren
class Grok:
    def spam(self, a, b):
        print('Grok.spam')

Foo.register(Grok)

print(isinstance(Grok(), Foo))  # True
print(issubclass(Grok, Foo))    # True

Metaklassen

# Metaklassen

import collections

# Kreierung der Klasse Foo:
#
#   class Foo():
#     ...
class_name = 'Foo'                 # Name der Klasse
class_parents = (object,)          # Tupel der Basisklassen
class_body = '''                   # Klassenkörper (class body)
def __init__(self, x):
    self.x = x
def blah(self):
    print('Hello World')
'''
class_dict = {}                    # Klassen-Dictionary

# den Körper im lokalen Dictionary ausführen
exec(class_body, globals(), class_dict)

# das Klassen-Objekt Foo über die Metaklasse "type" schaffen
Foo = type(class_name, class_parents, class_dict)

# die Metaklasse könnte man auch explizit angeben
class Foo(metaclass = type):
    pass

# https://docs.python.org/3/reference/datamodel.html?highlight=hook#metaclass-example

class OrderedClass(type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        print('\nOrderedClass __prepare__():\n%s\n' % repr((metacls, name, bases, kwds)))
        return collections.OrderedDict()

    def __new__(cls, name, bases, namespace, **kwds):
        print('OrderedClass __new__():')
        print('  %r' % cls)
        print('  %r' % name)
        print('  %s' % repr(bases))
        print('  %r' % namespace)
        print('  %r' % kwds)
        print('  type(namespace) = %s\n' % type(namespace))
        # type() erwartet für den Namespace ein dict() und kein OrderedDict()
        #result = type.__new__(cls, name, bases, dict(namespace))
        result = super().__new__(cls, name, bases, dict(namespace))
        # im Attribut "members" merken wir uns als Tupel die Keys des
        # OrderedDicts "namespace" in der richtigen Reihenfolge
        result.members = tuple(namespace)
        return result

class A(metaclass = OrderedClass):
    def one(self): pass
    def two(self): pass
    def three(self): pass
    def four(self): pass

print(A.members)
# ('__module__', 'one', 'two', 'three', 'four')

class D(A):
    def two(): pass
    def five(): pass

print(D.members)
# ('__module__', '__qualname__', 'two', 'five')

Klassen-Dekoratoren

# Klassen-Dekoratoren

# anders als Metaklassen wirken sie nur auf die dekorierte Klasse und nicht auf
# abgeleitete Klassen

registry = {}
def register(cls):
    registry[cls.__clsid__] = cls
    return cls

@register
class Foo(object):
    __clsid__ = '123-456'
    def bar(self):
        pass

print(registry)

# ohne Dekorator:
class Foo(object):
    __clsid__ = '123-456'
    def bar(self):
        pass
register(Foo)       # die Klasse registrieren

print(registry)     # die Registry ausgeben