Klassen und Funktionale Programmierung

Bei der Arbeit mit Funktionen höherer Ordnung, insbesondere im Kontext von Decorateuren, stoßen wir oft auf die Notwendigkeit, innere Zustände oder Objekte unserer Funktion von außen sichtbar oder zugänglich zu machen. In unseren Dekorator-Beispielen haben wir bereits festgestellt, dass durch die Verwendung von Closures innere Objekte erstellt wurden, auf die wir von außen nicht zugreifen konnten. Dies war in vielen Fällen beabsichtigt oder der gewünschte Effekt. Was aber, wenn wir diese inneren Zustände nach außen hin sichtbar oder zugänglich machen möchten? In diesem Kapitel möchten wir verschiedene Möglichkeiten diskutieren, dies zu erreichen. Im Wesentlichen können wir dieses "Fenster nach außen" durch Attributierung oder durch komplexe Rückgabeobjekte wie Tupel oder Dictionaries erreichen. Wir haben beide Techniken zuvor verwendet, aber hier wollen wir diesen Ansatz verfeinern.

Zebra-Löwe-hybrid

Beispiel

Der folgende call_counter-Dekorator verfolgt die Anzahl der Aufrufe einer Funktion. Dies erleichtert die Leistungsanalyse, Optimierung oder Fehlerbehebung, indem er einen einfachen Mechanismus zum Überwachen und Protokollieren von Funktionsaufrufen bereitstellt. Dadurch können Entwickler Nutzungsmuster verstehen und potenzielle Verbesserungsbereiche identifizieren.

Für den Zweck dieses Kapitels unseres Tutorials ist die innere Funktion get_calls von besonderem Interesse. Die Funktion get_calls bietet eine bequeme Möglichkeit, die Anzahl der Funktionsaufrufe extern abzurufen und darauf zuzugreifen. Die Funktion get_calls innerhalb des call_counter-Decorators verhält sich ähnlich wie eine Getter-Methode in einer Klasse. Sie kapselt die Logik zum Zugriff auf den Wert der Variable calls und bietet eine Möglichkeit, diesen Wert von außerhalb des Decorators abzurufen. Dieses Entwurfsmuster entspricht den Prinzipien der Kapselung und Abstraktion, die in der objektorientierten Programmierung häufig verwendet werden, wobei Getter-Methoden verwendet werden, um den internen Zustand eines Objekts abzurufen, während Datenintegrität gewahrt und Implementierungsdetails verborgen bleiben. In diesem Fall fungiert get_calls als Getter-Funktion für die Variable calls, und bietet eine saubere und kontrollierte Möglichkeit, ihren Wert extern abzurufen.

In [1]:
def call_counter(func):
    
    calls = 0
    def helper(*args, **kwargs):
        nonlocal calls
        calls += 1
        return func(*args, **kwargs)
    
    def get_calls():   
        return calls
    
    helper.get_calls = get_calls

    return helper

from random import randint

@call_counter
def f1(x):
    return 3*x

@call_counter
def f2(x, y):
    return x + 3*y

for i in range(randint(100, 3999)):
    f1(i)
    
for i in range(randint(100, 3999)):
    f2(i, 32)
    
print(f"{f1.get_calls()=}, {f2.get_calls()=}")
f1.get_calls()=1153, f2.get_calls()=1259

Auf diese Art haben wir nur einen Lesezugriff auf den Zähler von außen durch die Funktion get_calls. Wenn aus irgendeinem Grund der externe Änderung des Zählers erlaubt werden soll - auch wenn dies möglicherweise nicht sehr sinnvoll erscheint - kann dies einfach mit einer zusätzlichen Funktion namens set_calls erreicht werden.

Im nächsten Abschnitt gehen wir einen Schritt weiter und zeigen, wie Funktionale Programmierungstechniken tatsächlich verwendet werden können, um Aspekte der Klassenentwurf nachzubilden. In Python können Funktionen und Closures genutzt werden, um Zustand und Verhalten ähnlich wie Klassen zu kapseln.

Object Oriented Programming (OOP) und Funktionale Programmierung

Objektorientierte Programmierung (OOP) und Funktionale Programmierung (FP) gehören zu den am weitesten verbreiteten Programmierparadigmen. OOP konzentriert sich darauf, Objekte zur Darstellung von Daten und Verhalten zu verwenden, während FP darauf abzielt, Programme durch die Kombination von Funktionen für die Datenverarbeitung zu entwerfen. Höherwertige Funktionen sind ein leistungsstolles Konzept in der funktionalen Programmierung. Sie ermöglichen die Abstraktion und Komposition von Code, indem Funktionen als "Citizens erster Klasse" behandelt werden, was Manipulationen wie bei jedem anderen Datentyp ermöglicht. Rein syntaktisch könnte man den Eindruck gewinnen, dass beide Programmierparadigmen völlig unterschiedlich sind.

Aber wenn Sie sich die folgende Implementierung ansehen, könnten Sie den Eindruck bekommen, dass jemand eine Klasse schreiben wollte und nur einen Fehler gemacht hat, indem er def vor Person anstelle von class geschrieben hat:

In [2]:
def Person(name, age):

    def self():
        return None
    
    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age
        
    def set_age(new_age):
        nonlocal age
        age = new_age

    self.get_name = get_name
    self.set_name = set_name
    self.get_age = get_age
    self.set_age = set_age

    return self

# Create a person object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())

person.set_name('Jane')
print(person.get_name(), person.get_age())
Russel 25
Jane 25

Die obige Implementierung definiert eine Funktion "Person", die in gewisser Weise wie eine Klassendefinition funktioniert. "Person"-Objekte können mithilfe von Person instanziiert werden. Innerhalb von "Person" gibt es eine Funktion namens "person", die als Container für andere innere Funktionen dient. In einer ordnungsgemäßen Klassendefinition würden diese inneren Funktionen als Methoden bezeichnet. Ähnlich wie bei einer Klasse haben wir Getter (get_name und get_age) und Setter (set_name und set_age) definiert. Die Funktion "Person" erstellt eine Closure für die lokalen Variablen "name" und "age", um ihren Zustand zu erhalten. Mithilfe von "nonlocal" können innere Funktionen auf sie zugreifen. Um auf innere Funktionen extern zuzugreifen, hängen wir sie als Attribute an self an, die von "Person" zurückgegeben werden.

Im folgenden Programm definieren wir eine entsprechende "richtige" Klassendefinition:

In [3]:
class Person2():

    def __init__(self, name, age):
        self.set_name(name)
        self.set_age(age)
    
    def get_name(self):
        return self.__name

    def set_name(self, new_name):
        self.__name = new_name

    def get_age(self):
        return self.__age
        
    def set_age(self, new_age):
        self.__age = new_age

# Create a Person2 object
person = Person2("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Jane')
print(person.get_name(), person.get_age())
Russel 25
Jane 25

ir erweitern unsere "funktionale Klasse" Funktion, indem wir eine repr- und eine equal-Funktion hinzufügen (in der Klassenterminologie als Methode bezeichnet):

In [3]:
def Person(name, age):
    
    def self():
        return None

    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age

    def set_age(new_age):
        nonlocal age
        age = new_age

    def repr():
        return f"Person(name={name}, age={age})"

    def equal(other):
        nonlocal name, age
        return name == other.get_name() and age == other.get_age()

    self.get_name = get_name
    self.set_name = set_name
    self.get_age = get_age
    self.set_age = set_age
    self.repr = repr
    self.equal= equal

    return self
In [4]:
# Create a Person2 object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Eve')
print(person.get_name(), person.get_age())
Russel 25
Eve 25
In [5]:
person2 = Person("Jane", 25)
person3 = Person("Eve", 25)
print(person.equal(person2))
False
In [6]:
print(person.equal(person3))
True
In [7]:
person.repr()
Out[7]:
'Person(name=Eve, age=25)'
In [8]:
type(person)
Out[8]:
function

Nachahmung von Vererbung

Es ist wirklich faszinierend! Mit diesem Ansatz bietet sich die Flexibilität, Vererbung oder sogar Mehrfachvererbung zu simulieren. Indem wir das Potential Funktionen höherer Ordnung nutzen, können wir das Verhalten von Vererbung nachahmen, ohne Klassen explizit zu definieren. Dieser rein funktionale Ansatz eröffnet neue Möglichkeiten zur Strukturierung und Organisation unseres Codes und bietet eine einzigartige Perspektive auf objektorientierte Konzepte.

Lassen Sie uns zunächst unsere klassenähnliche Funktion erneut definieren.

In [9]:
def Person(name, age):
    
    def self():
        return None

    def get_name():
        return name

    def set_name(new_name):
        nonlocal name
        name = new_name

    def get_age():
        return age

    def set_age(new_age):
        nonlocal age
        age = new_age

    def repr():
        return f"Person(name={name}, age={age})"

    def equal(other):
        nonlocal name, age
        return name == other.get_name() and age == other.get_age()


    methods = ['get_name', 'set_name', 'get_age', 'set_age', 'repr', 'equal']
    # creating attributes of the nested function names to self
    for method in methods:
        self.__dict__[method] = eval(method)

    return self
In [10]:
# Create a Person2 object
person = Person("Russel", 25)

print(person.get_name(), person.get_age())
person.set_name('Eve')
print(person.get_name(), person.get_age())
Russel 25
Eve 25

Employee verhält sich wie eine Kind-Klasse von Person:

In [12]:
from functools import wraps

def Employee(name, age, staff_id):

    self = Person(name, age)
    # all attributes of Person are attached to self:
    self = wraps(self)(self)

    def get_staff_id():
        return staff_id

    def set_staff_id(new_staff_id):
        nonlocal staff_id
        staff_id = new_staff_id

    # adding 'methods' of child class
    methods = ['get_staff_id', 'set_staff_id']
    for method in methods:
        self.__dict__[method] = eval(method)


    return self
    
In [13]:
x = Employee('Homer', 42, '007')
x.get_age()
Out[13]:
42
In [14]:
x.set_staff_id('434')
x.get_staff_id()
Out[14]:
'434'

Übungen

Übung 1

Erstellen Sie eine Python-Klasse namens Librarycatalogue, die einen Katalog für eine Bibliothek repräsentiert. Die Klasse sollte die folgenden Attribute und Methoden haben:

Attribute:

  • books: Ein Wörterbuch, in dem die Schlüssel Buchtitel (Strings) sind und die Werte die entsprechenden Autoren (ebenfalls Strings) sind.

Methoden:

  • __init__(self): Die Konstruktormethode, die das Wörterbuch books initialisiert.
  • add_book(self, title, author): Eine Methode, die die Parameter title und author entgegennimmt und das Buch zum Katalog hinzufügt.
  • remove_book(self, title): Eine Methode, die den Parameter title entgegennimmt und das Buch aus dem Katalog entfernt, falls es existiert. Wenn das Buch nicht gefunden wird, geben Sie eine Meldung aus, die angibt, dass das Buch nicht im Katalog ist.
  • find_books_by_author(self, author): Eine Methode, die den Parameter author entgegennimmt und eine Liste von Buchtiteln zurückgibt, die von diesem Autor geschrieben wurden.
  • find_author_by_book(self, title): Eine Methode, die den Parameter title entgegennimmt und den Autor des angegebenen Buches zurückgibt.
  • display_catalogue(self): Eine Methode, die den gesamten Katalog ausgibt und alle Bücher und ihre Autoren auflistet.

Ihre Aufgabe besteht darin, die Klasse Librarycatalogue mit den angegebenen Attributen und Methoden zu implementieren. Erstellen Sie dann Instanzen der Klasse und testen Sie ihre Funktionalität, indem Sie Bücher hinzufügen, Bücher entfernen, Bücher nach Autor finden, Autoren nach Buch finden und den Katalog anzeigen.

Übung 2

Schreiben Sie die vorherige Klasse als Funktion um.

Lösungen

Lösung zu Übung 1

In [11]:
class Librarycatalogue:
    def __init__(self):
        self.books = {}

    def add_book(self, title, author):
        self.books[title] = author

    def remove_book(self, title):
        if title in self.books:
            del self.books[title]
            print(f"Book '{title}' removed from the catalogue.")
        else:
            print(f"Book '{title}' is not in the catalogue.")

    def find_books_by_author(self, author):
        found_books = [title for title, auth in self.books.items() if auth == author]
        return found_books

    def find_author_by_book(self, title):
        if title in self.books:
            return self.books[title]
        else:
            return f"Author of '{title}' is not found in the catalogue."

    def display_catalogue(self):
        print("catalogue:")
        for title, author in self.books.items():
            print(f"- {title} by {author}")


# Test the Librarycatalogue class
library = Librarycatalogue()

# Add books to the catalogue
library.add_book("1984", "George Orwell")
library.add_book("To Kill a Mockingbird", "Harper Lee")
library.add_book("Ulysses", "James Joyce")

# Display the catalogue
library.display_catalogue()

# Find books by author
print("\nBooks by Harper Lee:", library.find_books_by_author("Harper Lee"))

# Find author by book
print("\nAuthor of '1984':", library.find_author_by_book("1984"))
print("Author of 'Ulysses':", library.find_author_by_book("Ulysses"))

# Remove a book
library.remove_book("To Kill a Mockingbird")

# Display the catalogue again
library.display_catalogue()
catalogue:
- 1984 by George Orwell
- To Kill a Mockingbird by Harper Lee
- Ulysses by James Joyce

Books by Harper Lee: ['To Kill a Mockingbird']

Author of '1984': George Orwell
Author of 'Ulysses': James Joyce
Book 'To Kill a Mockingbird' removed from the catalogue.
catalogue:
- 1984 by George Orwell
- Ulysses by James Joyce

Solution to Exercise 2

In [12]:
def Librarycatalogue():

    books = {}
    
    def self():
        return None

    # names of the nested functions to be exported
    methods = ['add_book', 'remove_book', 'find_books_by_author', 
                'find_author_by_book', 'display_catalogue']

    def add_book(title, author):
        books[title] = author

    def remove_book(title):
        if title in books:
            del books[title]
            print(f"Book '{title}' removed from the catalogue.")
        else:
            print(f"Book '{title}' is not in the catalogue.")

    def find_books_by_author(author):
        found_books = [title for title, auth in books.items() if auth == author]
        return found_books

    def find_author_by_book(title):
        if title in books:
            return books[title]
        else:
            return f"Author of '{title}' is not found in the catalogue."

    def display_catalogue():
        print("catalogue:")
        for title, author in books.items():
            print(f"- {title} by {author}")

    # creating attributes of the nested function names to self
    for method in methods:
        self.__dict__[method] = eval(method)

    return self

    
In [13]:
# Test the Librarycatalogue class
library = Librarycatalogue()

# Add books to the catalogue
library.add_book("1984", "George Orwell")
library.add_book("Hotel New Hampshire", "John Irving")
library.add_book("Ulysses", "James Joyce")

# Display the catalogue
library.display_catalogue()

# Find books by author
print("\nBooks by Harper Lee:", library.find_books_by_author("Harper Lee"))

# Find author by book
print("\nAuthor of '1984':", library.find_author_by_book("1984"))
print("Author of 'Ulysses':", library.find_author_by_book("Ulysses"))

# Remove a book
library.remove_book("To Kill a Mockingbird")

# Display the catalogue again
library.display_catalogue()
catalogue:
- 1984 by George Orwell
- Hotel New Hampshire by John Irving
- Ulysses by James Joyce

Books by Harper Lee: []

Author of '1984': George Orwell
Author of 'Ulysses': James Joyce
Book 'To Kill a Mockingbird' is not in the catalogue.
catalogue:
- 1984 by George Orwell
- Hotel New Hampshire by John Irving
- Ulysses by James Joyce