We use cookies to enhance your experience on the site
CodeWorlds

Decorators - adapting to the environment

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!

What are decorators?

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 function

The

@decorator
syntax is shorthand for:

1def say_hello():
2    print("Hello!")
3
4say_hello = my_decorator(say_hello)  # Manual decorating

How do decorators work?

A 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 greet

Decorators with function arguments

For 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!

functools.wraps - preserving metadata

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!

Decorators with parameters

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:

  1. repeat(times)
    - returns the decorator
  2. decorator(func)
    - the actual decorator
  3. wrapper(*args, **kwargs)
    - the function wrapper

Built-in decorators

You already know some of Python's built-in decorators!

@property, @classmethod, @staticmethod

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) > 0

Practical decorator examples

1. Timing decorator - measuring execution time

1import 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.0015s

2. Logger decorator - automatic logging

1from 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 Serengeti

3. Cache decorator - memoization

1from 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# 5

Note: 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)

4. Access control decorator - authorization

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}")

5. Retry decorator - automatic retrying

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)

Stacking multiple decorators

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 → bold

Class decorators

Decorators 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!

Safari example - complete system with decorators

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}")

Summary

In this lesson you learned:

  • ✅ What decorators are and how they work
  • ✅ The
    @decorator
    syntax
  • ✅ Creating decorators with
    *args
    and
    **kwargs
  • ✅ Using
    @functools.wraps
    to preserve metadata
  • ✅ Decorators with parameters (three levels of functions)
  • ✅ Built-in decorators (
    @property
    ,
    @classmethod
    ,
    @staticmethod
    )
  • ✅ Practical examples: timing, logging, cache, authorization, retry
  • ✅ Stacking multiple decorators
  • ✅ Class decorators
  • ✅ Complete Safari system with decorators

Checkpoint

Before moving on:

  • [ ] You understand how decorators modify functions
  • [ ] You can create a simple decorator
  • [ ] You know when to use
    @wraps
  • [ ] You understand decorators with parameters
  • [ ] You can stack multiple decorators

Safari 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! 🎓🦁🐘

Go to CodeWorlds