Heb je je ooit afgevraagd hoe Python objecten laat werken met operatoren zoals +
of -
? Of hoe het weet hoe objecten moeten worden weergegeven wanneer je ze afdrukt? Het antwoord ligt in de magische methoden van Python, ook wel bekend als dunder (double under) methoden.
Magische methoden zijn speciale methoden waarmee je kunt definiëren hoe je objecten zich gedragen in reactie op verschillende operaties en ingebouwde functies. Ze zijn wat Python’s objectgeoriënteerd programmeren zo krachtig en intuïtief maakt.
In deze gids leer je hoe je magische methoden kunt gebruiken om meer elegante en krachtige code te maken. Je ziet praktische voorbeelden die laten zien hoe deze methoden werken in real-world scenario’s.
Vereisten
-
Basisbegrip van de Python-syntaxis en concepten van objectgeoriënteerd programmeren.
-
Bekendheid met klassen, objecten en overerving.
-
Kennis van ingebouwde Python-datatypes (lijsten, dictionaries, enzovoort).
-
Een werkende Python 3-installatie wordt aanbevolen om actief deel te nemen aan de voorbeelden hier.
Inhoudsopgave
Wat zijn magische methoden?
Magische methoden in Python zijn speciale methoden die beginnen en eindigen met dubbele underscores (__
). Wanneer je bepaalde bewerkingen of functies op je objecten gebruikt, roept Python automatisch deze methoden aan.
Bijvoorbeeld, wanneer je de +
operator op twee objecten gebruikt, zoekt Python naar de __add__
methode in de linker operand. Als het deze vindt, roept het die methode aan met de rechter operand als argument.
Hier is een eenvoudig voorbeeld dat laat zien hoe dit werkt:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # Dit roept p1.__add__(p2) aan
print(p3.x, p3.y) # Uitvoer: 4 6
Laten we eens kijken wat hier gebeurt:
-
We maken een
Point
klasse die een punt in 2D-ruimte voorstelt -
De
__init__
methode initialiseert de x- en y-coördinaten -
De
__add__
methode definieert wat er gebeurt wanneer we twee punten optellen -
Als we
p1 + p2
schrijven, roept Python automatischp1.__add__(p2)
aan -
Het resultaat is een nieuwe
Point
met coördinaten (4, 6)
Dit is nog maar het begin. Python heeft veel magische methoden waarmee je kunt aanpassen hoe je objecten zich gedragen in verschillende situaties. Laten we enkele van de meest nuttige verkennen.
Objectrepresentatie
Wanneer je met objecten werkt in Python, moet je ze vaak converteren naar strings. Dit gebeurt wanneer je een object afdrukt of probeert weer te geven in de interactieve console. Python biedt twee magische methoden voor dit doel: __str__
en __repr__
.
str versus repr
De methoden __str__
en __repr__
dienen verschillende doeleinden:
-
__str__
: Aangeroepen door destr()
-functie en door deprint()
-functie. Het moet een string retourneren die leesbaar is voor eindgebruikers. -
__repr__
: Aangeroepen door derepr()
-functie en gebruikt in de interactieve console. Het moet een string retourneren die, idealiter, gebruikt kan worden om het object opnieuw te maken.
Hier is een voorbeeld dat het verschil laat zien:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __str__(self):
return f"{self.celsius}°C"
def __repr__(self):
return f"Temperature({self.celsius})"
temp = Temperature(25)
print(str(temp)) # Output: 25°C
print(repr(temp)) # Output: Temperatuur(25)
In dit voorbeeld:
-
__str__
retourneert een gebruikersvriendelijke string die de temperatuur met een gradensymbool weergeeft -
__repr__
retourneert een string die laat zien hoe het object gecreëerd kan worden, wat handig is voor debugging
Het verschil wordt duidelijk wanneer je deze objecten in verschillende contexten gebruikt:
-
Als je de temperatuur afdrukt, zie je de gebruikersvriendelijke versie:
25°C
-
Als je het object inspecteert in de Python-console, zie je de gedetailleerde versie:
Temperatuur(25)
Praktijkvoorbeeld: Aangepaste Foutklasse
Laten we een aangepaste foutklasse maken die betere debuginformatie biedt. Dit voorbeeld laat zien hoe je __str__
en __repr__
kunt gebruiken om je foutmeldingen behulpzamer te maken:
class ValidationError(Exception):
def __init__(self, field, message, value=None):
self.field = field
self.message = message
self.value = value
super().__init__(self.message)
def __str__(self):
if self.value is not None:
return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
return f"Error in field '{self.field}': {self.message}"
def __repr__(self):
if self.value is not None:
return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
return f"ValidationError(field='{self.field}', message='{self.message}')"
# Gebruik
try:
age = -5
if age < 0:
raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
print(e) # Uitvoer: Fout in veld 'leeftijd': Leeftijd moet positief zijn (ontvangen: -5)
Deze aangepaste foutklasse biedt verschillende voordelen:
-
Het bevat de naam van het veld waar de fout is opgetreden
-
Het toont de daadwerkelijke waarde die de fout heeft veroorzaakt
-
Het biedt zowel gebruiksvriendelijke als gedetailleerde foutmeldingen
-
Het vergemakkelijkt debuggen door alle relevante informatie op te nemen
Operator Overloading
Operator overloading is een van de krachtigste functies van de magische methoden van Python. Het stelt je in staat om te definiëren hoe je objecten zich gedragen wanneer ze worden gebruikt met operatoren zoals +
, -
, *
en ==
. Dit maakt je code intuïtiever en leesbaarder.
Rekenkundige Operatoren
Python biedt magische methoden voor alle basisrekenkundige bewerkingen. Hier is een tabel die aangeeft welke methode overeenkomt met welke operator:
Operator | Magische Methode | Beschrijving |
+ |
__add__ |
Optelling |
- |
__sub__ |
Aftrekken |
* |
__mul__ |
Vermenigvuldiging |
/ |
__truediv__ |
Delen |
// |
__floordiv__ |
Gehele deling |
% |
__mod__ |
Modulo |
** |
__pow__ |
Machtsverheffing |
Vergelijkingsoperatoren
Op dezelfde manier kunt u definiëren hoe uw objecten worden vergeleken met behulp van deze magische methoden:
Operator | Magische methode | Omschrijving |
== |
__eq__ |
Gelijk aan |
!= |
__ne__ |
Niet gelijk aan |
< |
__lt__ |
Kleiner dan |
> |
__gt__ |
Groter dan |
<= |
__le__ |
Kleiner dan of gelijk aan |
>= |
__ge__ |
Groter dan of gelijk aan |
Praktijkvoorbeeld: Geldklasse
Laten we een Money
klasse maken die valuta-operaties correct afhandelt. Dit voorbeeld toont hoe je meerdere operatoren implementeert en randgevallen afhandelt:
from functools import total_ordering
from decimal import Decimal
@total_ordering # Implementeert alle vergelijkingsmethoden op basis van __eq__ en __lt__
class Money:
def __init__(self, amount, currency="USD"):
self.amount = Decimal(str(amount))
self.currency = currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, other):
if isinstance(other, (int, float, Decimal)):
return Money(self.amount * Decimal(str(other)), self.currency)
return NotImplemented
def __truediv__(self, other):
if isinstance(other, (int, float, Decimal)):
return Money(self.amount / Decimal(str(other)), self.currency)
return NotImplemented
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.currency == other.currency and self.amount == other.amount
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
return self.amount < other.amount
def __str__(self):
return f"{self.currency} {self.amount:.2f}"
def __repr__(self):
return f"Money({repr(float(self.amount))}, {repr(self.currency)})"
Laten we de belangrijkste kenmerken van deze Money
klasse uiteenzetten:
-
Nauwkeurigheidshandling: We gebruiken
Decimal
in plaats vanfloat
om precisieproblemen met zwevendekommaberekeningen te vermijden. -
Valutaveiligheid: De klasse voorkomt operaties tussen verschillende valuta’s om fouten te vermijden.
-
Typecontrole: Elke methode controleert of de andere operand van het juiste type is met behulp van
isinstance()
. -
Niet geïmplementeerd: Als een operatie geen zin heeft, geven we
NotImplemented
terug zodat Python de omgekeerde operatie kan proberen. -
@total_ordering: Deze decorator implementeert automatisch alle vergelijkingsmethoden op basis van
__eq__
en__lt__
.
Hier is hoe je de Money
klasse gebruikt:
# Basis rekenkunde
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining) # Uitvoer: USD 80.00
# Werken met verschillende valuta's
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total) # Uitvoer: USD 6000.00
# Deling door scalar
weekly_pay = salary / 4
print(weekly_pay) # Uitvoer: USD 1250.00
# Vergelijkingen
print(Money(100, "USD") > Money(50, "USD")) # Uitvoer: Waar
print(Money(100, "USD") == Money(100, "USD")) # Uitvoer: Waar
# Foutafhandeling
try:
Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
print(e) # Uitvoer: Kan verschillende valuta's niet optellen: USD en EUR
Deze Money
klasse demonstreert verschillende belangrijke concepten:
-
Hoe om te gaan met verschillende soorten operanden
-
Hoe goede foutafhandeling te implementeren
-
Hoe de
@total_ordering
decorator te gebruiken -
Hoe precisie te behouden bij financiële berekeningen
-
Hoe zowel string- als representatiemethoden te bieden
Containermethoden
Containermethoden stellen u in staat om uw objecten te laten gedragen als ingebouwde containers zoals lijsten, dictionaries of sets. Dit is bijzonder handig wanneer u aangepast gedrag nodig heeft voor het opslaan en ophalen van gegevens.
Sequentieprotocol
Om uw object zich te laten gedragen als een sequentie (zoals een lijst of tuple), moet u deze methoden implementeren:
Methode | Beschrijving | Voorbeeldgebruik |
__len__ |
Retourneert de lengte van de container | len(obj) |
__getitem__ |
Maakt indexering mogelijk met obj[key] |
obj[0] |
__setitem__ |
Staat toewijzing toe met obj[key] = waarde |
obj[0] = 42 |
__delitem__ |
Maakt verwijdering mogelijk met del obj[key] |
del obj[0] |
__iter__ |
Retourneert een iterator voor de container | for item in obj: |
__contains__ |
Implementeert de in -operator |
42 in obj |
Mapping Protocol
Voor dictionary-achtig gedrag, wil je deze methoden implementeren:
Methode | Omschrijving | Voorbeeldgebruik |
__getitem__ |
Waarde ophalen op basis van sleutel | obj["sleutel"] |
__setitem__ |
Waarde instellen op basis van sleutel | obj["sleutel"] = waarde |
__delitem__ |
Sleutel-waardepaar verwijderen | del obj["sleutel"] |
__len__ |
Aantal sleutel-waardeparen ophalen | len(obj) |
__iter__ |
Itereren over sleutels | for sleutel in obj: |
__contains__ |
Controleren of sleutel bestaat | "sleutel" in obj |
Praktijkvoorbeeld: Aangepaste Cache
Laten we een op tijd gebaseerde cache implementeren die automatisch oude items verloopt. Dit voorbeeld toont hoe je een aangepaste container kunt maken die zich gedraagt als een dictionary maar met extra functionaliteit:
import time
from collections import OrderedDict
class ExpiringCache:
def __init__(self, max_age_seconds=60):
self.max_age = max_age_seconds
self._cache = OrderedDict() # {sleutel: (waarde, tijdstempel)}
def __getitem__(self, key):
if key not in self._cache:
raise KeyError(key)
value, timestamp = self._cache[key]
if time.time() - timestamp > self.max_age:
del self._cache[key]
raise KeyError(f"Key '{key}' has expired")
return value
def __setitem__(self, key, value):
self._cache[key] = (value, time.time())
self._cache.move_to_end(key) # Verplaats naar het einde om de invoervolgorde te behouden
def __delitem__(self, key):
del self._cache[key]
def __len__(self):
self._clean_expired() # Maak verlopen items schoon voordat de lengte wordt gerapporteerd
return len(self._cache)
def __iter__(self):
self._clean_expired() # Maak verlopen items schoon voordat de iteratie
for key in self._cache:
yield key
def __contains__(self, key):
if key not in self._cache:
return False
_, timestamp = self._cache[key]
if time.time() - timestamp > self.max_age:
del self._cache[key]
return False
return True
def _clean_expired(self):
"""Remove all expired entries from the cache."""
now = time.time()
expired_keys = [
key for key, (_, timestamp) in self._cache.items()
if now - timestamp > self.max_age
]
for key in expired_keys:
del self._cache[key]
Laten we bekijken hoe deze cache werkt:
-
Opslag: De cache gebruikt een
OrderedDict
om sleutel-waarde paren samen met tijdstempels op te slaan. -
Verloop: Elke waarde wordt opgeslagen als een tuple van
(waarde, tijdstempel)
. Bij toegang tot een waarde controleren we of deze is verlopen. -
Container methoden: De klasse implementeert alle noodzakelijke methoden om zich als een woordenboek te gedragen:
-
__getitem__
: Haalt waarden op en controleert op verloop -
__setitem__
: Slaat waarden op met de huidige tijdstempel -
__delitem__
: Verwijdert items -
__len__
: Geeft het aantal niet-verlopen items terug -
__iter__
: Itereert over niet-verlopen sleutels -
__contains__
: Controleert of een sleutel bestaat
-
Hier is hoe de cache te gebruiken:
# Maak een cache met een verloop van 2 seconden
cache = ExpiringCache(max_age_seconds=2)
# Sla enkele waarden op
cache["name"] = "Vivek"
cache["age"] = 30
# Toegang tot waarden
print("name" in cache) # Uitvoer: True
print(cache["name"]) # Uitvoer: Vivek
print(len(cache)) # Uitvoer: 2
# Wacht op verloop
print("Waiting for expiration...")
time.sleep(3)
# Controleer verlopen waarden
print("name" in cache) # Uitvoer: False
try:
print(cache["name"])
except KeyError as e:
print(f"KeyError: {e}") # Uitvoer: KeyError: 'name'
print(len(cache)) # Uitvoer: 0
Deze cache-implementatie biedt verschillende voordelen:
-
Automatisch verlopen van oude vermeldingen
-
Interface vergelijkbaar met een woordenboek voor eenvoudig gebruik
-
Geheugenefficiëntie door het verwijderen van verlopen vermeldingen
-
Thread-safe bewerkingen (onder voorbehoud van single-threaded toegang)
-
Behoudt de volgorde van invoer
Attribuuttoegang
Attribuuttoegangsmethoden stellen u in staat om te bepalen hoe uw objecten omgaan met het ophalen, instellen en verwijderen van attributen. Dit is met name handig voor het implementeren van eigenschappen, validatie en logging.
getattr en getattribute
Python biedt twee methoden voor het beheersen van attribuuttoegang:
-
__getattr__
: Alleen aangeroepen wanneer een attribuutopzoeking mislukt (dat wil zeggen wanneer het attribuut niet bestaat) -
__getattribute__
: Aangeroepen bij elke attribuuttoegang, zelfs voor attributen die bestaan
Het belangrijkste verschil is dat __getattribute__
wordt aangeroepen voor elke attribuuttoegang, terwijl __getattr__
alleen wordt aangeroepen wanneer het attribuut niet op normale wijze wordt gevonden.
Hier is een eenvoudig voorbeeld dat het verschil laat zien:
class AttributeDemo:
def __init__(self):
self.name = "Vivek"
def __getattr__(self, name):
print(f"__getattr__ called for {name}")
return f"Default value for {name}"
def __getattribute__(self, name):
print(f"__getattribute__ called for {name}")
return super().__getattribute__(name)
demo = AttributeDemo()
print(demo.name) # Output: __getattribute__ aangeroepen voor naam
# Vivek
print(demo.age) # Output: __getattribute__ aangeroepen voor leeftijd
# __getattr__ aangeroepen voor leeftijd
# Standaardwaarde voor leeftijd
setattr en delattr
Op dezelfde manier kunt u bepalen hoe attributen worden ingesteld en verwijderd:
-
__setattr__
: Wordt aangeroepen wanneer een attribuut wordt ingesteld -
__delattr__
: Wordt aangeroepen wanneer een attribuut wordt verwijderd
Deze methoden stellen u in staat validatie, logging of aangepast gedrag te implementeren wanneer attributen worden gewijzigd.
Praktisch Voorbeeld: Automatisch Loggen van Eigenschappen
Laten we een klasse maken die automatisch alle eigenschapswijzigingen logt. Dit is nuttig voor debugging, auditing of het bijhouden van objectstatuswijzigingen:
import logging
# Instellen van logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class LoggedObject:
def __init__(self, **kwargs):
self._data = {}
# Attributen initialiseren zonder __setattr__ te activeren
for key, value in kwargs.items():
self._data[key] = value
def __getattr__(self, name):
if name in self._data:
logging.debug(f"Accessing attribute {name}: {self._data[name]}")
return self._data[name]
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name == "_data":
# Toestaan direct de _data attribuut in te stellen
super().__setattr__(name, value)
else:
old_value = self._data.get(name, "<undefined>")
self._data[name] = value
logging.info(f"Changed {name}: {old_value} -> {value}")
def __delattr__(self, name):
if name in self._data:
old_value = self._data[name]
del self._data[name]
logging.info(f"Deleted {name} (was: {old_value})")
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
Laten we uitleggen hoe deze klasse werkt:
-
Opslag: De klasse gebruikt een privé
_data
dictionary om attribuutwaarden op te slaan. -
Attribuut toegang:
-
__getattr__
: Geeft waarden terug uit_data
en logt debugberichten -
__setattr__
: Slaat waarden op in_data
en logt wijzigingen -
__delattr__
: Verwijdert waarden uit_data
en logt verwijderingen
-
-
Speciale behandeling: Het
_data
attribuut zelf wordt anders behandeld om oneindige recursie te voorkomen.
Hier is hoe de klasse te gebruiken:
# Maak een gelogd object met initiële waarden
user = LoggedObject(name="Vivek", email="[email protected]")
# Wijzig attributen
user.name = "Vivek" # Logs: Naam gewijzigd: Vivek -> Vivek
user.age = 30 # Logs: Leeftijd gewijzigd: <undefined> -> 30
# Toegang tot attributen
print(user.name) # Uitvoer: Vivek
# Verwijder attributen
del user.email # Logs: Email verwijderd (was: [email protected])
# Probeer toegang te krijgen tot verwijderd attribuut
try:
print(user.email)
except AttributeError as e:
print(f"AttributeError: {e}") # Uitvoer: AttributeError: 'LoggedObject' object heeft geen attribuut 'email'
Deze implementatie biedt verschillende voordelen:
-
Automatisch loggen van alle attribuutwijzigingen
-
Debug-niveau loggen voor attribuuttoegang
-
Duidelijke foutmeldingen voor ontbrekende attributen
-
Makkelijk bijhouden van objectstaatveranderingen
-
Nuttig voor debuggen en auditdoeleinden
Contextbeheerders
Contextmanagers zijn een krachtige functionaliteit in Python die je helpen om resources op de juiste manier te beheren. Ze zorgen ervoor dat resources op de juiste manier worden verkregen en vrijgegeven, zelfs als er een fout optreedt. De with
-verklaring is de meestvoorkomende manier om contextmanagers te gebruiken.
invoeren en verlaten
Om een contextmanager te maken, moet je twee speciale methoden implementeren:
-
__enter__
: Wordt opgeroepen bij het betreden van hetwith
-blok. Het zou de resource moeten retourneren die beheerd moet worden. -
__exit__
: Wordt opgeroepen bij het verlaten van hetwith
-blok, zelfs als er een uitzondering optreedt. Het zou de opschoning moeten afhandelen.
De methode __exit__
ontvangt drie argumenten:
-
exc_type
: Het type van de uitzondering (indien aanwezig) -
exc_val
: Het uitzonderingsinstantie (indien aanwezig) -
exc_tb
: De traceback (indien aanwezig)
Praktijkvoorbeeld: Database Connection Manager
Laten we een contextmanager maken voor databaseverbindingen. Dit voorbeeld toont hoe je op een juiste manier databasebronnen kunt beheren en transacties kunt afhandelen:
import sqlite3
import logging
# Loggen instellen
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class DatabaseConnection:
def __init__(self, db_path):
self.db_path = db_path
self.connection = None
self.cursor = None
def __enter__(self):
logging.info(f"Connecting to database: {self.db_path}")
self.connection = sqlite3.connect(self.db_path)
self.cursor = self.connection.cursor()
return self.cursor
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
logging.error(f"An error occurred: {exc_val}")
self.connection.rollback()
logging.info("Transaction rolled back")
else:
self.connection.commit()
logging.info("Transaction committed")
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
logging.info("Database connection closed")
# Geef False terug om uitzonderingen door te geven, en True om ze te onderdrukken
return False
Laten we bekijken hoe deze contextmanager werkt:
-
Initialisatie:
-
De klasse neemt een databasepad
-
Het initialiseert de verbinding en cursor als None
-
-
Enter methode:
-
Maakt een databaseverbinding
-
Maakt een cursor
-
Geeft de cursor terug voor gebruik in het
with
-blok
-
-
Uitvoermethode:
-
Beheert transactiebeheer (commit/rollback)
-
Sluit cursor en verbinding
-
Registreert alle bewerkingen
-
Geeft False terug om uitzonderingen door te geven
-
Hier is hoe u de contextmanager gebruikt:
# Maak een testdatabase in het geheugen
try:
# Succesvolle transactie
with DatabaseConnection(":memory:") as cursor:
# Maak een tabel
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
""")
# Voeg gegevens toe
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Vivek", "[email protected]")
)
# Vraag gegevens op
cursor.execute("SELECT * FROM users")
print(cursor.fetchall()) # Output: [(1, 'Vivek', '[email protected]')]
# Demonstreer het terugdraaien van de transactie bij een fout
with DatabaseConnection(":memory:") as cursor:
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
""")
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Wewake", "[email protected]")
)
# Dit veroorzaakt een fout - de tabel 'nonexistent' bestaat niet
cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
print(f"Caught exception: {e}")
Deze contextmanager biedt verschillende voordelen:
-
Resources worden automatisch beheerd (bijv. verbindingen worden altijd gesloten).
-
Met transactieveiligheid worden wijzigingen op passende wijze toegepast of teruggedraaid.
-
Uitzonderingen worden opgevangen en op een elegante manier afgehandeld
-
Alle bewerkingen worden gelogd voor het oplossen van problemen
-
De
with
-verklaring maakt de code duidelijk en beknopt
Oproepbare objecten
De magische methode __call__
maakt het mogelijk om instanties van uw klasse als functies te laten werken. Dit is handig voor het maken van objecten die de status behouden tussen oproepen of voor het implementeren van functie-achtig gedrag met extra functies.
call
De methode __call__
wordt aangeroepen wanneer u probeert een instantie van uw klasse te bellen alsof het een functie was. Hier is een eenvoudig voorbeeld:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
# Maak instanties die zich gedragen als functies
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # Uitvoer: 10
print(triple(5)) # Uitvoer: 15
Dit voorbeeld laat zien hoe __call__
u in staat stelt objecten te maken die de status behouden (de factor) terwijl ze oproepbaar zijn als functies.
Praktijkvoorbeeld: Memoization Decorator
Laten we een memoization decorator implementeren met behulp van __call__
. Deze decorator zal functieresultaten in de cache opslaan om overbodige berekeningen te voorkomen:
import time
import functools
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
# Behoud functiemetadata (naam, docstring, enz.)
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
# Maak een sleutel van de argumenten
# Voor eenvoud gaan we ervan uit dat alle argumenten hashable zijn
key = str(args) + str(sorted(kwargs.items()))
if key not in self.cache:
self.cache[key] = self.func(*args, **kwargs)
return self.cache[key]
# Gebruik
@Memoize
def fibonacci(n):
"""Calculate the nth Fibonacci number recursively."""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Meet de uitvoeringstijd
def time_execution(func, *args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
return result
# Zonder memoization zou dit extreem langzaam zijn
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")
# Tweede oproep is direct vanwege memoization
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")
Laten we eens kijken hoe deze memoization decorator werkt:
-
Initialisatie:
-
Neemt een functie als argument
-
Maakt een cache-dictionary aan om resultaten op te slaan
-
Bewaart de metadata van de functie met behulp van
functools.update_wrapper
-
-
Oproepmethode:
-
Maakt een unieke sleutel van de functieargumenten
-
Controleert of het resultaat in de cache staat
-
Zo niet, berekent het resultaat en slaat het op
-
Retourneert het gecachte resultaat
-
-
Gebruik:
-
Toegepast als een decorator op elke functie
-
Cachet automatisch resultaten voor herhaalde oproepen
-
Behoudt functiemetadata en gedrag
-
De voordelen van deze implementatie zijn:
-
Betere prestaties, omdat het overbodige berekeningen vermijdt
-
Beter, transparantie, omdat het werkt zonder de originele functie te wijzigen
-
Het is flexibel en kan met elke functie worden gebruikt
-
Het is geheugen efficiënt en cachet resultaten voor hergebruik
-
Het behoudt functiedocumentatie
Geavanceerde Magische Methoden
Laten we nu enkele van Python’s meer geavanceerde magische methoden verkennen. Deze methoden geven je fijnmazige controle over objectcreatie, geheugengebruik en woordenboekgedrag.
nieuw voor Objectcreatie
De __new__
methode wordt aangeroepen vóór __init__
en is verantwoordelijk voor het creëren en retourneren van een nieuwe instantie van de klasse. Dit is nuttig voor het implementeren van patronen zoals singletons of onveranderlijke objecten.
Hier is een voorbeeld van een singletonpatroon met __new__
:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name=None):
# Dit zal elke keer worden aangeroepen wanneer Singleton() wordt aangeroepen
if name is not None:
self.name = name
# Gebruik
s1 = Singleton("Vivek")
s2 = Singleton("Wewake")
print(s1 is s2) # Uitvoer: True
print(s1.name) # Uitvoer: Wewake (de tweede initialisatie overschreef de eerste)
Laten we bekijken hoe deze singleton werkt:
-
Klassevariabele:
_instance
slaat de enkele instantie van de klasse op -
nieuw methode:
-
Controleert of er een instantie bestaat
-
Maakt er een aan indien dat niet het geval is
-
Geeft de bestaande instantie terug indien die bestaat
-
-
init methode:
-
Wordt elke keer aangeroepen wanneer de constructor wordt gebruikt
-
Update de attributen van de instantie
-
slots voor Geheugenoptimalisatie
De __slots__
klassevariabele beperkt welke attributen een instantie kan hebben, waardoor geheugen wordt bespaard. Dit is bijzonder handig wanneer je veel instanties van een klasse hebt met een vast aantal attributen.
Hier is een vergelijking van reguliere en slotted klassen:
import sys
class RegularPerson:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
class SlottedPerson:
__slots__ = ['name', 'age', 'email']
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
# Vergelijk geheugengebruik
regular_people = [RegularPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
slotted_people = [SlottedPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes") # Uitvoer: Grootte van een regulier persoon: 48 bytes
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes") # Uitvoer: Grootte van een persoon met slots: 56 bytes
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes") # Uitvoer: Geheugen bespaard per instantie: -8 bytes
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB") # Uitvoer: Totaal geheugen bespaard voor 1000 instanties: -7,81 KB
Het uitvoeren van deze code levert een interessant resultaat op:
Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB
Verrassend genoeg is in dit eenvoudige voorbeeld de grootte van de persoon met slots eigenlijk 8 bytes groter dan die van de reguliere persoon! Dit lijkt in tegenspraak te zijn met het gebruikelijke advies over het besparen van geheugen met __slots__
.
Dus wat is hier aan de hand? De werkelijke geheugenbesparing van __slots__
komt voort uit:
-
Het elimineren van dictionaries: Gewone Python-objecten slaan hun attributen op in een dictionary (
__dict__
), wat overhead met zich meebrengt. De functiesys.getsizeof()
houdt geen rekening met de grootte van deze dictionary. -
Het opslaan van attributen: Voor kleine objecten met weinig attributen kan de overhead van de slot-descriptors opwegen tegen de besparing van de dictionary.
-
Schaalbaarheid: Het echte voordeel wordt zichtbaar wanneer:
-
Je veel instanties hebt (duizenden of miljoenen)
-
Je objecten veel attributen hebben
-
Je attributen dynamisch toevoegt
-
Laten we een meer complete vergelijking bekijken:
# Een nauwkeurigere geheugenmeting
import sys
def get_size(obj):
"""Get a better estimate of the object's size in bytes."""
size = sys.getsizeof(obj)
if hasattr(obj, '__dict__'):
size += sys.getsizeof(obj.__dict__)
# Voeg de grootte van de dictionary-inhoud toe
size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
return size
class RegularPerson:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
class SlottedPerson:
__slots__ = ['name', 'age', 'email']
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
regular = RegularPerson("Vivek", 30, "[email protected]")
slotted = SlottedPerson("Vivek", 30, "[email protected]")
print(f"Complete Regular person size: {get_size(regular)} bytes") # Uitvoer: Grootte van een volledig gewoon persoon: 610 bytes
print(f"Complete Slotted person size: {get_size(slotted)} bytes") # Uitvoer: Grootte van een volledig slotted persoon: 56 bytes
Met deze nauwkeurigere meting zul je zien dat slotted objecten over het algemeen minder geheugen gebruiken, vooral wanneer je meer attributen toevoegt.
Belangrijke punten over __slots__
:
-
Echte geheugenbesparingen: De belangrijkste geheugenbesparingen komen voort uit het elimineren van de instantie-
__dict__
-
Dynamische beperkingen: Je kunt geen willekeurige attributen toevoegen aan ingesloten objecten
-
Overwegingen betreffende overerving: Het gebruik van
__slots__
met overerving vereist een zorgvuldige planning -
Gebruiksscenario’s: Het beste voor klassen met veel instanties en vaste attributen
-
Prestatiebonus: Kan ook snellere toegang tot attributen bieden in sommige gevallen
Ontbrekend voor standaardwaarden van een woordenboek
De methode __missing__
wordt aangeroepen door woordenboeksubklassen wanneer een sleutel niet wordt gevonden. Dit is handig voor het implementeren van woordenboeken met standaardwaarden of automatische sleutelcreatie.
Hier is een voorbeeld van een woordenboek dat automatisch lege lijsten maakt voor ontbrekende sleutels:
class AutoKeyDict(dict):
def __missing__(self, key):
self[key] = []
return self[key]
# Gebruik
groups = AutoKeyDict()
groups["team1"].append("Vivek")
groups["team1"].append("Wewake")
groups["team2"].append("Vibha")
print(groups) # Uitvoer: {'team1': ['Vivek', 'Wewake'], 'team2': ['Vibha']}
Deze implementatie biedt verschillende voordelen:
-
Geen noodzaak om te controleren of een sleutel bestaat, wat handiger is.
-
Automatische initialisatie maakt standaardwaarden aan indien nodig.
-
Vermindert de standaardcode voor het initialiseren van een woordenboek.
-
Het is flexibeler en kan elke logica voor standaardwaarden implementeren.
-
Maakt waarden alleen aan wanneer nodig, waardoor het geheugenefficiënter is.
Prestatieoverwegingen
Hoewel magische methoden krachtig zijn, kunnen ze de prestaties beïnvloeden als je ze niet voorzichtig gebruikt. Laten we wat veelvoorkomende prestatieoverwegingen verkennen en hoe je ze kunt meten.
Impact van Magische Methoden op Prestaties
Verschillende magische methoden hebben verschillende prestatie-implicaties:
Methoden voor attribuuttoegang:
-
__getattr__
,__getattribute__
,__setattr__
en__delattr__
worden vaak aangeroepen -
Complexe bewerkingen in deze methoden kunnen je code aanzienlijk vertragen
Methoden voor containers:
-
__getitem__
,__setitem__
en__len__
worden vaak opgeroepen in loops -
Inefficiënte implementaties kunnen ervoor zorgen dat je container veel langzamer is dan ingebouwde typen
Operator overloading:
-
Arithmetische en vergelijkingsoperatoren worden vaak gebruikt
-
Complexe implementaties kunnen eenvoudige bewerkingen onverwacht traag maken
Laten we de prestatie-impact van __getattr__
versus directe attribuutoegang meten:
import time
class DirectAccess:
def __init__(self):
self.value = 42
class GetAttrAccess:
def __init__(self):
self._value = 42
def __getattr__(self, name):
if name == "value":
return self._value
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
# Meet prestaties
direct = DirectAccess()
getattr_obj = GetAttrAccess()
def benchmark(obj, iterations=1000000):
start = time.time()
for _ in range(iterations):
x = obj.value
end = time.time()
return end - start
direct_time = benchmark(direct)
getattr_time = benchmark(getattr_obj)
print(f"Direct access: {direct_time:.6f} seconds")
print(f"__getattr__ access: {getattr_time:.6f} seconds")
print(f"__getattr__ is {getattr_time / direct_time:.2f}x slower")
Deze benchmark toont aanzienlijke prestatieverschillen:
Direct access: 0.027714 seconds
__getattr__ access: 0.060646 seconds
__getattr__ is 2.19x slower
Zoals je kunt zien, is het gebruik van __getattr__
meer dan twee keer zo langzaam als directe attribuutoegang. Dit maakt misschien niet uit voor af en toe benaderde attributen, maar kan significant worden in prestatiekritieke code die attributen benadert in strakke loops.
Optimalisatiestrategieën
Gelukkig zijn er verschillende manieren om magische methoden te optimaliseren.
-
Gebruik slots voor geheugenefficiëntie: Dit vermindert het geheugengebruik en verbetert de snelheid van attribuuttoegang. Het is het beste voor klassen met veel instanties.
-
Caché berekende waarden: U kunt resultaten van dure bewerkingen opslaan en de caché alleen bijwerken wanneer dat nodig is. Gebruik
@property
voor berekende attributen. -
Minimaliseer methodoproepen: Zorg ervoor dat u onnodige magische methodoproepen vermijdt en directe toegang tot attributen gebruikt wanneer mogelijk. Overweeg
__slots__
te gebruiken voor vaak benaderde attributen.
Beste Praktijken
Wanneer u magische methoden gebruikt, volg dan deze beste praktijken om ervoor te zorgen dat uw code onderhoudbaar, efficiënt en betrouwbaar is.
1. Wees Consistent
Bij het implementeren van gerelateerde magische methoden, handhaaf consistentie in gedrag:
from functools import total_ordering
@total_ordering
class ConsistentNumber:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, ConsistentNumber):
return NotImplemented
return self.value == other.value
def __lt__(self, other):
if not isinstance(other, ConsistentNumber):
return NotImplemented
return self.value < other.value
2. Return NotImplemented
Wanneer een bewerking geen zin heeft, retourneer dan NotImplemented
om Python de omgekeerde bewerking te laten proberen:
class Money:
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
# ... rest van de implementatie
3. Houd het Simpel
Magische methoden moeten eenvoudig en voorspelbaar zijn. Vermijd complexe logica die tot onverwacht gedrag kan leiden:
# Goed: Eenvoudig en voorspelbaar
class SimpleContainer:
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index]
# Slecht: Complex en potentieel verwarrend
class ComplexContainer:
def __init__(self):
self.items = []
self.access_count = 0
def __getitem__(self, index):
self.access_count += 1
if self.access_count > 100:
raise RuntimeError("Too many accesses")
return self.items[index]
4. Documenteer Gedrag
Documenteer duidelijk hoe uw magische methoden zich gedragen, vooral als ze afwijken van de standaard verwachtingen:
class CustomDict(dict):
def __missing__(self, key):
"""
Called when a key is not found in the dictionary.
Creates a new list for the key and returns it.
This allows for automatic list creation when accessing
non-existent keys.
"""
self[key] = []
return self[key]
5. Denk aan Prestaties
Wees op de hoogte van de prestatie-implicaties, vooral voor vaak aangeroepen methoden:
class OptimizedContainer:
__slots__ = ['items'] # Gebruik __slots__ voor betere prestaties
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index] # Directe toegang is sneller
6. Behandel Randgevallen
Houd altijd rekening met randgevallen en handel ze op passende wijze af:
class SafeContainer:
def __getitem__(self, key):
if not isinstance(key, (int, slice)):
raise TypeError("Index must be integer or slice")
if key < 0:
raise ValueError("Index cannot be negative")
# ... rest van de implementatie
Samenvatting
De magische methoden van Python bieden een krachtige manier om uw klassen te laten gedragen als ingebouwde typen, waardoor meer intuïtieve en expressieve code mogelijk is. In deze gids hebben we onderzocht hoe deze methoden werken en hoe ze effectief kunnen worden gebruikt.
Belangrijkste Punten
-
Objectrepresentatie:
-
Gebruik
__str__
voor gebruikersvriendelijke uitvoer -
Gebruik
__repr__
voor debuggen en ontwikkeling
-
-
Operator overloading:
-
Implementeer rekenkundige en vergelijkingsoperatoren
-
Geef
NotImplemented
terug voor niet-ondersteunde bewerkingen -
Gebruik
@total_ordering
voor consistente vergelijkingen
-
-
Gedrag van containers:
-
Implementeer sequentie- en mappingprotocollen
-
Bedenk prestaties voor veelgebruikte bewerkingen
-
Behandel randgevallen op passende wijze
-
-
Resourcebeheer:
-
Gebruik contextmanagers voor correct beheer van resources
-
Implementeer
__enter__
en__exit__
voor opruimen -
Handel uitzonderingen af in
__exit__
-
-
Prestatieoptimalisatie:
-
Gebruik
__slots__
voor geheugenefficiëntie -
Cachewaarden wanneer van toepassing
-
Minimaliseer methode-oproepen in veelgebruikte code
-
Wanneer magische methoden gebruiken
Magische methoden zijn het meest nuttig wanneer je ze nodig hebt om:
-
Maak aangepaste gegevensstructuren
-
Implementeer domeinspecifieke typen
-
Beheer resources op de juiste manier
-
Voeg speciaal gedrag toe aan uw klassen
-
Maak uw code meer Pythonic
Wanneer u magische methoden moet vermijden
Vermijd magische methoden wanneer:
-
Eenvoudige attribuuttoegang voldoende is
-
Het gedrag verwarrend of onverwacht zou zijn
-
Prestaties van cruciaal belang zijn en magische methoden overhead zouden toevoegen
-
De implementatie overdreven complex zou zijn
Onthoud dat met grote kracht grote verantwoordelijkheid komt. Gebruik magische methoden verstandig, waarbij u rekening houdt met de prestatie-implicaties en het principe van de minste verrassing. Wanneer ze op de juiste manier worden gebruikt, kunnen magische methoden de leesbaarheid en expressiviteit van uw code aanzienlijk verbeteren.
Referenties en verder lezen
Officiële Python-documentatie
-
Python Data Model – Officiële documentatie – Uitgebreide gids voor het gegevensmodel en magische methoden van Python.
-
functools.total_ordering – Documentatie voor de total_ordering decorator die automatisch ontbrekende vergelijkingsmethoden invult.
-
Python Speciale Methode Namen – Officiële referentie voor speciale methode-identificatoren in Python.
-
Verzamelingen Abstracte Basis Klassen – Leer over abstracte basis klassen voor containers die de interfaces definiëren die jouw containerklassen kunnen implementeren.
Gemeenschapsbronnen
- Een Gids voor Python’s Magische Methoden – Rafe Kettler – Praktische voorbeelden van magische methoden en veelvoorkomende gebruikssituaties.
Verder Lezen
Als je dit artikel leuk vond, vind je deze Python-gerelateerde artikelen op mijn persoonlijke blog wellicht nuttig:
-
Praktische Experimenten voor Django ORM Query Optimalisaties – Leer hoe je jouw Django ORM-queries kunt optimaliseren met praktische voorbeelden en experimenten.
-
De Hoge Kosten van Synchrone uWSGI – Begrijp de prestatie-implicaties van synchrone verwerking in uWSGI en hoe het van invloed is op jouw Python-webapplicaties.
Source:
https://www.freecodecamp.org/news/python-magic-methods-practical-guide/