Understand Decorators in Python

PythonPythonBeginner
Practice Now

Introduction

In this lab, you will gain a comprehensive understanding of decorators in Python, a powerful feature for modifying or enhancing functions and methods. We will begin by introducing the fundamental concept of decorators and exploring their basic usage through practical examples.

Building upon this foundation, you will learn how to effectively use functools.wraps to preserve important metadata of the decorated function. We will then delve into specific decorators like the property decorator, understanding its role in managing attribute access. Finally, the lab will clarify the distinctions between instance methods, class methods, and static methods, demonstrating how decorators are used in these contexts to control method behavior within classes.

This is a Guided Lab, which provides step-by-step instructions to help you learn and practice. Follow the instructions carefully to complete each step and gain hands-on experience. Historical data shows that this is a beginner level lab with a 93% completion rate. It has received a 100% positive review rate from learners.

Understanding Basic Decorators

In this step, we will introduce the concept of decorators and their basic usage. A decorator is a function that takes another function as an argument, adds some functionality, and returns another function, all without altering the source code of the original function.

First, locate the file decorator_basics.py in the file explorer on the left side of the WebIDE. Double-click to open it. We will write our first decorator in this file.

Copy and paste the following code into decorator_basics.py:

import datetime

def log_activity(func):
    """A simple decorator to log function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

Let's break down this code:

  • We define a decorator function log_activity which accepts a function func as its argument.
  • Inside log_activity, we define a nested function wrapper. This function will contain the new behavior. It prints a log message, calls the original function func, and then prints another log message.
  • The log_activity function returns the wrapper function.
  • The @log_activity syntax above the greet function is a shortcut for greet = log_activity(greet). It applies our decorator to the greet function.

Now, save the file (you can use Ctrl+S or Cmd+S). To run the script, open the integrated terminal at the bottom of the WebIDE and execute the following command:

python ~/project/decorator_basics.py

You will see the following output. Note that the datetime will vary.

Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.

Function name: wrapper
Function docstring: None

Notice two things in the output. First, our greet function is now wrapped with the logging messages. Second, the function's name and docstring have been replaced by those of the wrapper function. This can be problematic for debugging and introspection. In the next step, we will learn how to fix this.

Preserving Function Metadata with functools.wraps

In the previous step, we observed that decorating a function replaces its original metadata (like __name__ and __doc__) with the metadata of the wrapper function. Python's functools module provides a solution for this: the wraps decorator.

The wraps decorator is used inside your own decorator to copy the metadata from the original function to the wrapper function.

Let's modify our code in decorator_basics.py. Open the file in the WebIDE and update it to use functools.wraps.

import datetime
from functools import wraps

def log_activity(func):
    """A simple decorator to log function calls."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

The only changes are:

  1. We imported wraps from the functools module.
  2. We added @wraps(func) right above the definition of our wrapper function.

Save the file and run it again from the terminal:

python ~/project/decorator_basics.py

Now, the output will be different:

Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.

Function name: greet
Function docstring: A simple function to greet someone.

As you can see, the function name is correctly reported as greet, and its original docstring is preserved. Using functools.wraps is a best practice that makes your decorators more robust and professional.

Implementing Managed Attributes with @property

Python provides several built-in decorators. One of the most useful is @property, which allows you to turn a class method into a "managed attribute". This is ideal for adding logic like validation or computation to attribute access without changing the way users interact with your class.

Let's explore this by creating a Circle class. Open the file property_decorator.py from the file explorer.

Copy and paste the following code into property_decorator.py:

import math

class Circle:
    def __init__(self, radius):
        ## The actual value is stored in a "private" attribute
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """The radius setter with validation."""
        print(f"Setting radius to {value}...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """A read-only computed property for the area."""
        print("Calculating area...")
        return math.pi * self._radius ** 2

## --- Let's test our Circle class ---
c = Circle(5)

## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")

## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")

## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")

## Try to set an invalid radius (triggers the setter's validation)
try:
    c.radius = -2
except ValueError as e:
    print(f"Error: {e}")

In this code:

  • @property on the radius method defines a "getter". It's called when you access c.radius.
  • @radius.setter defines a "setter" for the radius property. It's called when you assign a value, like c.radius = 10. We've added validation here to prevent negative values.
  • The area method also uses @property but has no setter, making it a read-only attribute. Its value is calculated every time it's accessed.

Save the file and run it from the terminal:

python ~/project/property_decorator.py

You should see the following output, demonstrating how the getter, setter, and validation logic are automatically invoked:

Getting radius...
Initial radius: 5

Setting radius to 10...
Getting radius...
New radius: 10

Calculating area...
Circle area: 314.16

Setting radius to -2...
Error: Radius cannot be negative

Differentiating Instance, Class, and Static Methods

In Python classes, methods can be bound to an instance, the class, or not bound at all. Decorators are used to define these different method types.

  • Instance Methods: The default type. They receive the instance as the first argument, conventionally named self. They operate on instance-specific data.
  • Class Methods: Marked with @classmethod. They receive the class as the first argument, conventionally named cls. They operate on class-level data and are often used as alternative constructors.
  • Static Methods: Marked with @staticmethod. They do not receive any special first argument. They are essentially regular functions namespaced within a class and cannot access instance or class state.

Let's see all three in action. Open the file class_methods.py from the file explorer.

Copy and paste the following code into class_methods.py:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## 1. Instance Method
    def instance_method(self):
        print("\n--- Calling Instance Method ---")
        print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
        print(f"Can access class data: self.class_variable = '{self.class_variable}'")

    ## 2. Class Method
    @classmethod
    def class_method(cls):
        print("\n--- Calling Class Method ---")
        print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
        ## Note: Cannot access instance_variable without an instance
        print("Cannot access instance data directly.")

    ## 3. Static Method
    @staticmethod
    def static_method(a, b):
        print("\n--- Calling Static Method ---")
        print("Cannot access instance or class data directly.")
        print(f"Just a utility function: {a} + {b} = {a + b}")

## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")

## Call the instance method (requires an instance)
my_instance.instance_method()

## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works

## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works

Save the file and run it from the terminal:

python ~/project/class_methods.py

Examine the output carefully. It clearly demonstrates the capabilities and limitations of each method type.

--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28

This example provides a clear reference for when to use each type of method based on whether it needs access to instance state, class state, or neither.

Summary

In this lab, you have gained a practical understanding of decorators in Python. You started by learning how to create and apply a basic decorator to add functionality to a function. You then saw the importance of using functools.wraps to preserve the original function's metadata, a crucial best practice for writing clean and maintainable decorators.

Furthermore, you explored powerful built-in decorators. You learned to use the @property decorator to create managed attributes with custom getter and setter logic, enabling features like input validation. Finally, you distinguished between instance methods, class methods (@classmethod), and static methods (@staticmethod), understanding how each serves a different purpose within a class structure based on its access to instance and class state.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.