Decorators modify function behavior with @decorator syntax.
Generators
Use yield to produce values lazily. Memory-efficient for large datasets.
Decorators modify function behavior with @decorator syntax.
Use yield to produce values lazily. Memory-efficient for large datasets.
def timer(func): import time def wrapper(*a, **kw): start = time.time() result = func(*a, **kw) print(f"{func.__name__}: {time.time()-start:.4f}s") return result return wrapper @timer def slow(): import time; time.sleep(1) def count_up_to(n): i = 1 while i <= n: yield i i += 1
# Generator def countdown(n): while n > 0: yield n n -= 1 for num in countdown(3): print("Countdown:", num) # Generator expression squares = (x * x for x in range(1, 6)) print("Sum of squares:", sum(squares)) # Decorator def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print("Done") return result return wrapper @logger def add(a, b): return a + b @logger def greet(name): return f"Hi, {name}!" print(add(2, 3)) print(greet("Alice"))
Write a logger decorator.
def logger(func):
def wrapper(*a, **kw):
print(f"Calling {func.__name__}")
return func(*a, **kw)
return wrapper
yield vs return?
yield pauses execution and produces a value; return exits the function.
They produce one value at a time without storing the entire sequence in memory.