Inleiding tot Geavanceerde Python
Als je de basis van Python onder de knie hebt, is het tijd om je kennis naar het volgende niveau te tillen. In dit artikel verkennen we drie krachtige Python concepten die je code eleganter, efficiënter en professioneler maken: decorators, generators en context managers.
Deze geavanceerde technieken worden veel gebruikt in professionele Python codebases en het beheersen ervan zal je onderscheiden als een ervaren Python developer.
Decorators: Code Verfraaien en Uitbreiden
Decorators zijn een van Python's meest elegante features. Ze stellen je in staat om het gedrag van functies of klassen te wijzigen zonder de oorspronkelijke code aan te passen.
Basis Decorator Concept
Een decorator is eigenlijk een functie die een andere functie als argument neemt en een nieuwe functie teruggeeft:
def mijn_decorator(func):
def wrapper(*args, **kwargs):
print(f"Functie {func.__name__} wordt uitgevoerd...")
resultaat = func(*args, **kwargs)
print(f"Functie {func.__name__} is voltooid!")
return resultaat
return wrapper
# Decorator gebruiken
@mijn_decorator
def zeg_hallo(naam):
return f"Hallo, {naam}!"
# Test
print(zeg_hallo("Marie"))
# Output:
# Functie zeg_hallo wordt uitgevoerd...
# Hallo, Marie!
# Functie zeg_hallo is voltooid!
Praktische Decorator Voorbeelden
Timing Decorator
import time
import functools
def meet_tijd(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_tijd = time.time()
resultaat = func(*args, **kwargs)
eind_tijd = time.time()
print(f"{func.__name__} duurde {eind_tijd - start_tijd:.4f} seconden")
return resultaat
return wrapper
@meet_tijd
def langzame_berekening():
total = 0
for i in range(1000000):
total += i * i
return total
# Test
result = langzame_berekening()
# Output: langzame_berekening duurde 0.1234 seconden
Caching Decorator
def cache(func):
cache_data = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache_data:
print(f"Cache hit voor {args}")
return cache_data[args]
print(f"Cache miss voor {args}, berekening uitvoeren...")
resultaat = func(*args)
cache_data[args] = resultaat
return resultaat
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Test
print(fibonacci(10)) # Eerste keer: veel berekeningen
print(fibonacci(8)) # Tweede keer: meeste waarden uit cache
Authentication Decorator
def vereist_login(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Simuleer user check
gebruiker_ingelogd = True # In werkelijkheid: check session/token
if not gebruiker_ingelogd:
return {"error": "Login vereist"}
return func(*args, **kwargs)
return wrapper
@vereist_login
def gevoelige_data():
return {"data": "Geheime gebruikersinformatie"}
# Test
print(gevoelige_data())
Generators: Geheugen-Efficiënte Iteratie
Generators zijn een krachtige manier om iterables te maken die waarden on-demand produceren, wat geheugen bespaart bij grote datasets.
Basis Generator Syntaxis
def tel_tot(maximum):
"""Generator die telt van 1 tot maximum"""
nummer = 1
while nummer <= maximum:
yield nummer
nummer += 1
# Generator gebruiken
teller = tel_tot(5)
print(type(teller)) #
# Door generator itereren
for getal in teller:
print(getal)
# Output: 1, 2, 3, 4, 5
Generator Expressions
# List comprehension (gehele lijst in geheugen)
kwadraten_lijst = [x**2 for x in range(1000000)]
# Generator expression (waarden on-demand)
kwadraten_gen = (x**2 for x in range(1000000))
print(f"Lijst grootte: {len(kwadraten_lijst)} items")
print(f"Generator object: {kwadraten_gen}")
# Memory usage verschil is enorm!
import sys
print(f"Lijst geheugen: {sys.getsizeof(kwadraten_lijst)} bytes")
print(f"Generator geheugen: {sys.getsizeof(kwadraten_gen)} bytes")
Praktische Generator Voorbeelden
Bestand Lezen Generator
def lees_grote_bestand(bestandsnaam):
"""Generator voor het lezen van grote bestanden regel voor regel"""
with open(bestandsnaam, 'r', encoding='utf-8') as bestand:
for regel in bestand:
yield regel.strip()
def filter_regels_met_woord(bestandsnaam, zoekwoord):
"""Generator die alleen regels met specifiek woord teruggeeft"""
for regel in lees_grote_bestand(bestandsnaam):
if zoekwoord.lower() in regel.lower():
yield regel
# Gebruik (werkt met bestanden van gigabytes!)
# for regel in filter_regels_met_woord('groot_bestand.txt', 'python'):
# print(regel)
Fibonacci Generator
def fibonacci_generator():
"""Oneindige Fibonacci generator"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
def fibonacci_tot(limiet):
"""Fibonacci getallen tot een bepaalde limiet"""
for getal in fibonacci_generator():
if getal > limiet:
break
yield getal
# Test
print("Fibonacci getallen tot 100:")
for fib in fibonacci_tot(100):
print(fib, end=" ")
# Output: 0 1 1 2 3 5 8 13 21 34 55 89
Context Managers: Netjes Resource Beheer
Context managers zorgen ervoor dat resources correct worden opgezet en opgeruimd, zelfs wanneer er fouten optreden.
De 'with' Statement
# Traditionele manier (foutgevoelig)
bestand = open('data.txt', 'r')
content = bestand.read()
bestand.close() # Kan vergeten worden!
# Met context manager (veilig)
with open('data.txt', 'r') as bestand:
content = bestand.read()
# Bestand wordt automatisch gesloten, zelfs bij fouten!
Eigen Context Manager Maken
Met klasse (__enter__ en __exit__)
class DatabaseVerbinding:
def __init__(self, db_naam):
self.db_naam = db_naam
self.verbinding = None
def __enter__(self):
print(f"Verbinding maken met database: {self.db_naam}")
# Simuleer database verbinding
self.verbinding = f"Verbinding met {self.db_naam}"
return self.verbinding
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Database verbinding sluiten: {self.db_naam}")
if exc_type:
print(f"Fout opgetreden: {exc_val}")
self.verbinding = None
return False # Propageer eventuele exceptions
# Gebruik
with DatabaseVerbinding("klanten_db") as db:
print(f"Werken met: {db}")
# Simuleer database operaties
# Output:
# Verbinding maken met database: klanten_db
# Werken met: Verbinding met klanten_db
# Database verbinding sluiten: klanten_db
Met contextlib decorator
from contextlib import contextmanager
import time
@contextmanager
def meet_uitvoeringstijd():
start = time.time()
print("Timer gestart...")
try:
yield start
finally:
eind = time.time()
print(f"Uitvoeringstijd: {eind - start:.4f} seconden")
# Gebruik
with meet_uitvoeringstijd():
# Simuleer wat werk
time.sleep(1)
total = sum(range(100000))
# Output:
# Timer gestart...
# Uitvoeringstijd: 1.0056 seconden
Geavanceerde Context Manager
import tempfile
import os
from contextlib import contextmanager
@contextmanager
def tijdelijk_bestand(inhoud, suffix='.txt'):
"""Context manager voor tijdelijke bestanden"""
temp_bestand = None
try:
# Maak tijdelijk bestand
with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as f:
f.write(inhoud)
temp_bestand = f.name
print(f"Tijdelijk bestand aangemaakt: {temp_bestand}")
yield temp_bestand
finally:
# Ruim op
if temp_bestand and os.path.exists(temp_bestand):
os.unlink(temp_bestand)
print(f"Tijdelijk bestand verwijderd: {temp_bestand}")
# Gebruik
with tijdelijk_bestand("Hello, World!\nDit is een test.") as bestand:
with open(bestand, 'r') as f:
print("Bestandsinhoud:")
print(f.read())
# Bestand wordt automatisch verwijderd na gebruik
Alles Samen: Praktisch Voorbeeld
Hier is een voorbeeld dat alle drie concepten combineert:
import functools
import time
from contextlib import contextmanager
# Decorator voor logging
def log_functie_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Aanroep: {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"[LOG] Resultaat: {result}")
return result
return wrapper
# Context manager voor performance monitoring
@contextmanager
def monitor_performance():
start = time.time()
geheugen_start = get_memory_usage() # Hypothetische functie
yield
eind = time.time()
geheugen_eind = get_memory_usage()
print(f"Performance Report:")
print(f"- Tijd: {eind - start:.4f}s")
print(f"- Geheugen: {geheugen_eind - geheugen_start}MB")
# Generator voor data processing
def proces_grote_dataset(data_generator):
"""Generator die grote datasets efficiënt verwerkt"""
for item in data_generator:
# Simuleer complexe verwerking
verwerkt_item = item ** 2 + item
yield verwerkt_item
@log_functie_calls
def data_analyse_pipeline():
"""Hoofdfunctie die alles combineert"""
# Grote dataset generator
data = (x for x in range(1000000))
with monitor_performance():
# Verwerk data met generator
resultaten = proces_grote_dataset(data)
# Neem alleen eerste 10 resultaten
eerste_resultaten = []
for i, resultaat in enumerate(resultaten):
if i >= 10:
break
eerste_resultaten.append(resultaat)
return eerste_resultaten
# Test
resultaten = data_analyse_pipeline()
Best Practices en Tips
Decorators
- Gebruik altijd
@functools.wraps
om metadata te behouden - Maak decorators herbruikbaar en parametriseerbaar
- Test decorators grondig, vooral edge cases
- Documenteer wat je decorator doet en waarom
Generators
- Gebruik generators voor grote datasets om geheugen te besparen
- Generators zijn eenmalig - maak nieuwe instance voor hergebruik
- Combineer generators voor complexe data pipelines
- Test met
next()
voor debugging
Context Managers
- Gebruik altijd context managers voor resource management
- Handle exceptions correct in
__exit__
- Maak herbruikbare context managers voor veel voorkomende taken
- Test wat er gebeurt bij exceptions
Veelgemaakte Fouten
Decorator Valkuilen
# FOUT: Decorator wordt uitgevoerd bij import
def slechte_decorator(func):
print("Deze print wordt uitgevoerd bij import!") # FOUT
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# GOED: Print alleen bij functie aanroep
def goede_decorator(func):
def wrapper(*args, **kwargs):
print("Deze print wordt uitgevoerd bij aanroep") # GOED
return func(*args, **kwargs)
return wrapper
Generator Valkuilen
# FOUT: Generator proberen hergebruiken
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] - Generator is uitgeput!
# GOED: Nieuwe generator maken
def maak_generator():
return (x for x in range(5))
gen1 = maak_generator()
gen2 = maak_generator()
print(list(gen1)) # [0, 1, 2, 3, 4]
print(list(gen2)) # [0, 1, 2, 3, 4]
Volgende Stappen
Nu je deze geavanceerde concepten beheerst, kun je:
- Metaclasses: Nog geavanceerder - klassen die klassen maken
- Async/Await: Asynchrone programmering voor performance
- Type Hints: Betere code documentatie en IDE support
- Design Patterns: Klassieke patronen implementeren in Python
- C Extensions: Python uitbreiden met C code voor snelheid
Conclusie
Decorators, generators en context managers zijn essentiële tools in de gereedschapskist van elke gevorderde Python developer. Ze maken je code niet alleen eleganter en efficiënter, maar ook professioneler en onderhoudsbaarder.
Het beheersen van deze concepten opent de deur naar geavanceerde Python programmering en helpt je om complexe problemen op te lossen met elegante oplossingen.
Wil je Meer Leren?
Onze geavanceerde Python cursussen behandelen deze onderwerpen in detail met praktische oefeningen en echte projecten.
Neem Contact Op Bekijk Cursussen