Welcome to the final lesson in Module 3, @name! Darwin here with an advanced final lesson.
In nature, animals adapt to their environment - chameleons gain the ability to change color, bats develop echolocation, migratory birds develop magnetic navigation. In Python, decorators allow a similar adaptation - they modify the behavior of functions and classes without changing their code!
1# Regular function
2def observe_species(name):
3 print(f"Observation: {name}")
4
5# Function with decorator - automatic logging!
6@log_observation
7def observe_species(name):
8 print(f"Observation: {name}")
9# Now every call is logged automatically!A decorator is a function that takes another function (or class) and returns a modified version.
Safari Analogy: It's like equipping a researcher - binoculars don't change the researcher, but they extend their observation capabilities!
1def my_decorator(func):
2 """Decorator - takes a function, returns modified version"""
3 def wrapper():
4 print("Something before the function")
5 func() # Call the original function
6 print("Something after the function")
7 return wrapper
8
9@my_decorator
10def say_hello():
11 print("Hello!")
12
13say_hello()
14# Output:
15# Something before the function
16# Hello!
17# Something after the functionThe
syntax is shorthand for:@decorator
1def say_hello():
2 print("Hello!")
3
4say_hello = my_decorator(say_hello) # Manual decoratingA decorator is a higher-order function - a function that operates on functions!
1def simple_decorator(func):
2 """
3 Decorator - wrapper pattern
4
5 1. Accept a function
6 2. Define wrapper
7 3. Return wrapper
8 """
9 def wrapper():
10 print(f"[BEFORE] Calling {func.__name__}")
11 result = func()
12 print(f"[AFTER] Finished {func.__name__}")
13 return result
14 return wrapper
15
16@simple_decorator
17def greet():
18 print("Hello!")
19 return "Done"
20
21# greet is now wrapper, not the original function!
22greet()
23# [BEFORE] Calling greet
24# Hello!
25# [AFTER] Finished greetFor the decorator to work with functions that accept arguments, use
*args and **kwargs:1def log_decorator(func):
2 def wrapper(*args, **kwargs):
3 """Wrapper accepts any arguments"""
4 print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
5 result = func(*args, **kwargs)
6 print(f"Result: {result}")
7 return result
8 return wrapper
9
10@log_decorator
11def add(a, b):
12 return a + b
13
14@log_decorator
15def greet(name, greeting="Hello"):
16 return f"{greeting}, {name}!"
17
18add(5, 3)
19# Calling add with args=(5, 3), kwargs={}
20# Result: 8
21
22greet("Darwin", greeting="Hi")
23# Calling greet with args=('Darwin',), kwargs={'greeting': 'Hi'}
24# Result: Hi, Darwin!Problem: the decorator changes the function's
__name__ and __doc__!1def my_decorator(func):
2 def wrapper(*args, **kwargs):
3 return func(*args, **kwargs)
4 return wrapper
5
6@my_decorator
7def important_function():
8 """This function is important"""
9 pass
10
11print(important_function.__name__) # "wrapper" - wrong!
12print(important_function.__doc__) # None - no documentation!Solution: Use
@functools.wraps!1from functools import wraps
2
3def my_decorator(func):
4 @wraps(func) # Preserve original function metadata
5 def wrapper(*args, **kwargs):
6 return func(*args, **kwargs)
7 return wrapper
8
9@my_decorator
10def important_function():
11 """This function is important"""
12 pass
13
14print(important_function.__name__) # "important_function" - correct!
15print(important_function.__doc__) # "This function is important" - preserved!IMPORTANT: Always use
@wraps(func) in your decorators!For a decorator to accept parameters, you need three levels of functions:
1def repeat(times):
2 """Decorator with parameter - repeat function N times"""
3 def decorator(func):
4 @wraps(func)
5 def wrapper(*args, **kwargs):
6 for i in range(times):
7 result = func(*args, **kwargs)
8 return result
9 return wrapper
10 return decorator
11
12@repeat(times=3) # Decorator parameter!
13def say_hello():
14 print("Hello!")
15
16say_hello()
17# Hello!
18# Hello!
19# Hello!Structure:
repeat(times) - returns the decoratordecorator(func) - the actual decoratorwrapper(*args, **kwargs) - the function wrapperYou already know some of Python's built-in decorators!
1class Species:
2 def __init__(self, name, population):
3 self._name = name
4 self._population = population
5
6 @property # Getter
7 def name(self):
8 return self._name
9
10 @property
11 def population(self):
12 return self._population
13
14 @population.setter # Setter
15 def population(self, value):
16 if value < 0:
17 raise ValueError("Population cannot be negative")
18 self._population = value
19
20 @classmethod # Class method
21 def from_dict(cls, data):
22 return cls(data['name'], data['population'])
23
24 @staticmethod # Static method
25 def is_valid_name(name):
26 return len(name) > 01import time
2from functools import wraps
3
4def timer(func):
5 """Measure function execution time"""
6 @wraps(func)
7 def wrapper(*args, **kwargs):
8 start = time.time()
9 result = func(*args, **kwargs)
10 end = time.time()
11 print(f"⏱️ {func.__name__} took {end - start:.4f}s")
12 return result
13 return wrapper
14
15@timer
16def analyze_dna_sequence(sequence):
17 """Simulate DNA analysis - time-consuming operation"""
18 time.sleep(2) # Simulation
19 return f"Analyzed {len(sequence)} nucleotides"
20
21result = analyze_dna_sequence("ATCGATCG")
22# ⏱️ analyze_dna_sequence took 2.0015s1from functools import wraps
2from datetime import datetime
3
4def log_calls(func):
5 """Log every function call"""
6 @wraps(func)
7 def wrapper(*args, **kwargs):
8 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
9 print(f"[{timestamp}] Call: {func.__name__}")
10 print(f" Arguments: args={args}, kwargs={kwargs}")
11
12 try:
13 result = func(*args, **kwargs)
14 print(f" ✓ Success: {result}")
15 return result
16 except Exception as e:
17 print(f" ✗ Error: {e}")
18 raise
19 return wrapper
20
21@log_calls
22def observe_species(name, location, count):
23 """Record a species observation"""
24 if count < 0:
25 raise ValueError("Count cannot be negative")
26 return f"Recorded: {count}x {name} in {location}"
27
28observe_species("Lion", "Serengeti", 12)
29# [2024-01-15 14:30:45] Call: observe_species
30# Arguments: args=('Lion', 'Serengeti', 12), kwargs={}
31# ✓ Success: Recorded: 12x Lion in Serengeti1from functools import wraps
2
3def cache(func):
4 """Cache results - don't recalculate the same thing twice"""
5 cached_results = {}
6
7 @wraps(func)
8 def wrapper(*args):
9 if args in cached_results:
10 print(f"💾 Cache hit for {args}")
11 return cached_results[args]
12
13 print(f"🔄 Computing for {args}")
14 result = func(*args)
15 cached_results[args] = result
16 return result
17
18 return wrapper
19
20@cache
21def fibonacci(n):
22 """Calculate the n-th Fibonacci number"""
23 if n < 2:
24 return n
25 return fibonacci(n - 1) + fibonacci(n - 2)
26
27print(fibonacci(5))
28# 🔄 Computing for (5,)
29# 🔄 Computing for (4,)
30# ...
31# 💾 Cache hit for (2,) # Reuse!
32# 5Note: Python has a built-in
@functools.lru_cache!1from functools import lru_cache
2
3@lru_cache(maxsize=128) # Remembers the last 128 results
4def fibonacci(n):
5 if n < 2:
6 return n
7 return fibonacci(n - 1) + fibonacci(n - 2)1from functools import wraps
2
3def require_authorization(authorization_code):
4 """Decorator with parameter - requires authorization"""
5 def decorator(func):
6 @wraps(func)
7 def wrapper(*args, **kwargs):
8 # Check if the first argument is a valid code
9 if len(args) == 0 or args[0] != authorization_code:
10 raise PermissionError(f"Unauthorized access to {func.__name__}")
11
12 # Remove authorization code from arguments
13 return func(*args[1:], **kwargs)
14 return wrapper
15 return decorator
16
17@require_authorization("SAFARI_2024")
18def access_classified_data(species_name):
19 """Access to classified data - requires authorization"""
20 return f"Classified data about {species_name}: [CLASSIFIED]"
21
22# Correct authorization
23print(access_classified_data("SAFARI_2024", "Black Rhinoceros"))
24# "Classified data about Black Rhinoceros: [CLASSIFIED]"
25
26# No authorization - error!
27try:
28 print(access_classified_data("WRONG_CODE", "Black Rhinoceros"))
29except PermissionError as e:
30 print(f"Error: {e}")1from functools import wraps
2import time
3
4def retry(max_attempts=3, delay=1):
5 """Retry operation on error"""
6 def decorator(func):
7 @wraps(func)
8 def wrapper(*args, **kwargs):
9 for attempt in range(1, max_attempts + 1):
10 try:
11 return func(*args, **kwargs)
12 except Exception as e:
13 print(f"Attempt {attempt}/{max_attempts} failed: {e}")
14 if attempt < max_attempts:
15 print(f"Retrying in {delay}s...")
16 time.sleep(delay)
17 else:
18 print("All attempts exhausted")
19 raise
20 return wrapper
21 return decorator
22
23@retry(max_attempts=3, delay=0.5)
24def unstable_connection(success_rate=0.3):
25 """Simulate an unstable connection"""
26 import random
27 if random.random() > success_rate:
28 raise ConnectionError("Connection interrupted")
29 return "Success!"
30
31# Automatically retries on error
32result = unstable_connection(success_rate=0.8)Decorators can be stacked - apply multiple at once!
1@decorator1
2@decorator2
3@decorator3
4def my_function():
5 pass
6
7# Equivalent to:
8# my_function = decorator1(decorator2(decorator3(my_function)))Order matters - executed bottom to top!
1from functools import wraps
2
3def bold(func):
4 @wraps(func)
5 def wrapper(*args, **kwargs):
6 return f"**{func(*args, **kwargs)}**"
7 return wrapper
8
9def italic(func):
10 @wraps(func)
11 def wrapper(*args, **kwargs):
12 return f"_{func(*args, **kwargs)}_"
13 return wrapper
14
15@bold
16@italic
17def greet(name):
18 return f"Hello, {name}"
19
20print(greet("Darwin"))
21# "**_Hello, Darwin_**"
22# Order: greet → italic → boldDecorators can also modify entire classes!
1def singleton(cls):
2 """Class decorator - Singleton pattern"""
3 instances = {}
4
5 @wraps(cls)
6 def get_instance(*args, **kwargs):
7 if cls not in instances:
8 instances[cls] = cls(*args, **kwargs)
9 return instances[cls]
10
11 return get_instance
12
13@singleton
14class DatabaseConnection:
15 def __init__(self, host):
16 self.host = host
17 print(f"Connecting to {host}...")
18
19# First instance
20db1 = DatabaseConnection("localhost") # Prints: "Connecting to localhost..."
21
22# "Second" instance - actually the same one!
23db2 = DatabaseConnection("localhost") # Prints nothing
24
25print(db1 is db2) # True - same object!1from functools import wraps
2from datetime import datetime
3import time
4from typing import Callable, Any
5
6# === DECORATORS ===
7
8def log_observation(func: Callable) -> Callable:
9 """Log all species observations"""
10 @wraps(func)
11 def wrapper(*args, **kwargs):
12 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
13 print(f"📝 [{timestamp}] Observation: {func.__name__}")
14 result = func(*args, **kwargs)
15 print(f" ✓ Recorded: {result}")
16 return result
17 return wrapper
18
19def measure_time(func: Callable) -> Callable:
20 """Measure operation execution time"""
21 @wraps(func)
22 def wrapper(*args, **kwargs):
23 start = time.time()
24 result = func(*args, **kwargs)
25 elapsed = time.time() - start
26 print(f" ⏱️ Time: {elapsed:.3f}s")
27 return result
28 return wrapper
29
30def validate_population(func: Callable) -> Callable:
31 """Validate population data before saving"""
32 @wraps(func)
33 def wrapper(self, population: int, *args, **kwargs):
34 if not isinstance(population, int):
35 raise TypeError("Population must be an integer")
36 if population < 0:
37 raise ValueError("Population cannot be negative")
38 return func(self, population, *args, **kwargs)
39 return wrapper
40
41def require_auth(authorization_level: str):
42 """Require authorization level"""
43 def decorator(func: Callable) -> Callable:
44 @wraps(func)
45 def wrapper(self, auth_code: str, *args, **kwargs):
46 if auth_code != f"SAFARI_{authorization_level}":
47 raise PermissionError(
48 f"Required level: {authorization_level}"
49 )
50 return func(self, *args, **kwargs)
51 return wrapper
52 return decorator
53
54def cache_result(func: Callable) -> Callable:
55 """Cache computation results"""
56 cache = {}
57
58 @wraps(func)
59 def wrapper(*args):
60 if args in cache:
61 print(f" 💾 Cache hit")
62 return cache[args]
63
64 result = func(*args)
65 cache[args] = result
66 return result
67
68 return wrapper
69
70# === CLASS WITH DECORATORS ===
71
72class Species:
73 """
74 Species class with Safari decorators
75
76 Demonstration: logging, timing, validation, authorization, cache
77 """
78
79 _registry = {}
80
81 def __init__(self, scientific_name: str, common_name: str, population: int):
82 self.scientific_name = scientific_name
83 self.common_name = common_name
84 self._population = population
85 self.observations = []
86
87 Species._registry[scientific_name] = self
88
89 @property
90 def population(self) -> int:
91 """Getter - regular property"""
92 return self._population
93
94 @population.setter
95 @validate_population # Validation decorator!
96 def population(self, value: int) -> None:
97 """Setter with automatic validation"""
98 self._population = value
99
100 @log_observation # Automatic logging
101 @measure_time # Time measurement
102 def add_observation(self, location: str, count: int, date: str) -> str:
103 """
104 Add observation - automatically logged and timed
105
106 Decorators: @log_observation, @measure_time
107 """
108 time.sleep(0.1) # Simulate operation
109 obs = {
110 "location": location,
111 "count": count,
112 "date": date
113 }
114 self.observations.append(obs)
115 return f"{count}x {self.common_name} in {location}"
116
117 @cache_result # Cache results
118 def calculate_biodiversity_score(self) -> float:
119 """
120 Calculate biodiversity score - with cache
121
122 Time-consuming operation - cache saves time!
123 """
124 print(f" 🔄 Computing score for {self.common_name}...")
125 time.sleep(0.5) # Simulate complex computations
126
127 total_obs = sum(obs["count"] for obs in self.observations)
128 unique_locations = len(set(obs["location"] for obs in self.observations))
129
130 return (total_obs * unique_locations) / (self.population + 1)
131
132 @require_auth("ADMIN") # Requires ADMIN authorization
133 def delete_all_observations(self, auth_code: str) -> str:
134 """
135 Delete all observations - requires ADMIN authorization
136
137 Decorator: @require_auth("ADMIN")
138 """
139 count = len(self.observations)
140 self.observations.clear()
141 return f"Deleted {count} observations"
142
143 @classmethod
144 def get_total_population(cls) -> int:
145 """Total population of all species"""
146 return sum(s.population for s in cls._registry.values())
147
148 @staticmethod
149 @cache_result # Even static methods can be cached!
150 def calculate_extinction_risk(population: int, habitat_loss: float) -> str:
151 """
152 Calculate extinction risk - with cache
153
154 Args:
155 population: Number of individuals
156 habitat_loss: Habitat loss (0.0-1.0)
157 """
158 print(f" 🔄 Computing risk...")
159 time.sleep(0.3)
160
161 risk_score = (1000 - population) * habitat_loss
162
163 if risk_score > 800:
164 return "Critical"
165 elif risk_score > 500:
166 return "High"
167 elif risk_score > 200:
168 return "Medium"
169 else:
170 return "Low"
171
172 def __repr__(self) -> str:
173 return f"Species('{self.common_name}', pop={self.population})"
174
175# === DEMONSTRATION ===
176
177print("=== SAFARI SYSTEM WITH DECORATORS ===\n")
178
179# Creating species
180lion = Species("Panthera leo", "Lion", 120)
181rhino = Species("Diceros bicornis", "Rhinoceros", 45)
182
183# 1. Observations - automatic logging and timing
184print("\n1. OBSERVATIONS (with @log_observation and @measure_time):")
185lion.add_observation("Serengeti", 12, "2024-01-15")
186lion.add_observation("Masai Mara", 8, "2024-01-16")
187
188# 2. Population validation
189print("\n2. VALIDATION (@validate_population):")
190try:
191 lion.population = -10 # Error - negative!
192except ValueError as e:
193 print(f"✗ Validation error: {e}")
194
195try:
196 lion.population = "hundred" # Error - not int!
197except TypeError as e:
198 print(f"✗ Type error: {e}")
199
200lion.population = 125 # OK
201print(f"✓ Population updated: {lion.population}")
202
203# 3. Cache - biodiversity score
204print("\n3. CACHE (@cache_result):")
205print("First call:")
206score1 = lion.calculate_biodiversity_score()
207print(f"Score: {score1:.2f}")
208
209print("\nSecond call (from cache):")
210score2 = lion.calculate_biodiversity_score()
211print(f"Score: {score2:.2f}")
212
213# 4. Authorization
214print("\n4. AUTHORIZATION (@require_auth):")
215try:
216 lion.delete_all_observations("WRONG_CODE")
217except PermissionError as e:
218 print(f"✗ Access denied: {e}")
219
220result = lion.delete_all_observations("SAFARI_ADMIN")
221print(f"✓ {result}")
222
223# 5. Cache in static method
224print("\n5. CACHE IN STATIC METHOD:")
225print("First call:")
226risk1 = Species.calculate_extinction_risk(50, 0.8)
227print(f"Risk: {risk1}")
228
229print("\nSecond call (from cache):")
230risk2 = Species.calculate_extinction_risk(50, 0.8)
231print(f"Risk: {risk2}")
232
233print("\nDifferent parameters (new computation):")
234risk3 = Species.calculate_extinction_risk(500, 0.3)
235print(f"Risk: {risk3}")In this lesson you learned:
@decorator syntax*args and **kwargs@functools.wraps to preserve metadata@property, @classmethod, @staticmethod)Before moving on:
@wrapsSafari Analogy: Decorators are evolutionary adaptations - a chameleon with chromatophores, a bat with echolocation - they extend capabilities without changing the fundamental nature! 🦎✨
Congratulations! You've completed Module 3 - Object-Oriented Programming! You've mastered classes, inheritance, encapsulation, magic methods, type hints, and decorators. You're ready for the next challenges in Python Safari! 🎓🦁🐘