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.
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.
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()=}")
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:
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())
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:
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())
ir erweitern unsere "funktionale Klasse" Funktion, indem wir eine repr
- und eine equal
-Funktion hinzufügen (in der Klassenterminologie als Methode bezeichnet):
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
# 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())
person2 = Person("Jane", 25)
person3 = Person("Eve", 25)
print(person.equal(person2))
print(person.equal(person3))
person.repr()
type(person)
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.
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
# 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())
Employee verhält sich wie eine Kind-Klasse von Person:
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
x = Employee('Homer', 42, '007')
x.get_age()
x.set_staff_id('434')
x.get_staff_id()
Ü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örterbuchbooks
initialisiert.add_book(self, title, author)
: Eine Methode, die die Parametertitle
undauthor
entgegennimmt und das Buch zum Katalog hinzufügt.remove_book(self, title)
: Eine Methode, die den Parametertitle
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 Parameterauthor
entgegennimmt und eine Liste von Buchtiteln zurückgibt, die von diesem Autor geschrieben wurden.find_author_by_book(self, title)
: Eine Methode, die den Parametertitle
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.
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()
Solution to Exercise 2¶
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
# 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()