Python Decorators and Descriptors - 2 | Chapter 2: Python Decorators and Descriptors | Python Advance
K12 Students

Academics

AI-Powered learning for Grades 8–12, aligned with major Indian and international curricula.

Academics
Professionals

Professional Courses

Industry-relevant training in Business, Technology, and Design to help professionals and graduates upskill for real-world careers.

Professional Courses
Games

Interactive Games

Fun, engaging games to boost memory, math fluency, typing speed, and English skillsβ€”perfect for learners of all ages.

games

Interactive Audio Lesson

Listen to a student-teacher conversation explaining the topic in a relatable way.

Introduction to Decorators

Unlock Audio Lesson

Signup and Enroll to the course for listening the Audio Lesson

0:00
Teacher
Teacher

Today, we'll learn about decorators in Python! Can anyone tell me what a decorator is?

Student 1
Student 1

Is it something that modifies a function's behavior?

Teacher
Teacher

Exactly! A decorator is a design pattern that allows modifying a function or method without changing its source code. Think of it as a wrapper that adds behavior to our functions. Can you remember this with the acronym 'WAP', which stands for 'Wrap and Add Behavior'?

Student 2
Student 2

How does it work?

Teacher
Teacher

Great question! A decorator takes a function as an input and returns a new function. For example, let’s say we have a simple function that prints 'Hello'. With a decorator, we can print messages before and after calling that function.

Student 3
Student 3

Can you show an example?

Teacher
Teacher

Sure! Let’s look at the code that shows a basic decorator. Remember, anytime you see '@decorator_name' before a function definition, it's being decorated!

Teacher
Teacher

In summary, decorators allow us to extend functions without modifying their code directly.

Function Decorators: Concept and Usage

Unlock Audio Lesson

Signup and Enroll to the course for listening the Audio Lesson

0:00
Teacher
Teacher

Now that we understand the concept of decorators, let’s discuss their practical applications. Can anyone think of how we might use decorators?

Student 4
Student 4

Maybe for logging function calls?

Teacher
Teacher

Correct! We can create a logging decorator that logs function details. Here's an example that demonstrates this. It shows the function call parameters and return values.

Student 1
Student 1

That sounds useful! It could help in debugging.

Teacher
Teacher

Absolutely! Logging helps track what happens during execution. To remember this, think 'WALD' - 'Wrap, Add Log Details'.

Student 2
Student 2

What are other applications of decorators?

Teacher
Teacher

Decorators can also handle access control, memoization, timing, and more. Each of these enhances our functions in unique ways.

Teacher
Teacher

To wrap up, decorators provide versatile solutions across programming needs - remember their potential!

Decorators with Parameters

Unlock Audio Lesson

Signup and Enroll to the course for listening the Audio Lesson

0:00
Teacher
Teacher

Now, let's talk about decorators that accept parameters. Why might we want a decorator with arguments?

Student 3
Student 3

It could help to customize the behavior of the decorator based on input values!

Teacher
Teacher

Exactly! For instance, we might want a decorator that repeats a function call a specific number of times. We can achieve this using nested functions.

Student 4
Student 4

Can you show that example?

Teacher
Teacher

Sure! Let's see how we can create a repeat decorator. Remember that 'RAP' helps you remember 'Repeat And Parameterize'.

Student 1
Student 1

Got it! This looks powerful because we can customize the number of times a function is executed.

Teacher
Teacher

Yes, parameters broaden the utility of decorators. Keep practicing this concept!

Class Decorators

Unlock Audio Lesson

Signup and Enroll to the course for listening the Audio Lesson

0:00
Teacher
Teacher

Let’s shift gears and explore class decorators. Can anyone explain how a class decorator differs from a function decorator?

Student 2
Student 2

Is it because it modifies or wraps the entire class instead of just a single function?

Teacher
Teacher

Exactly right! Class decorators can add attributes or even modify methods across the class.

Student 3
Student 3

Do you have an example?

Teacher
Teacher

Yes! Let’s consider a class decorator that adds an attribute. To remember this, think 'CADA' - 'Class Attribute Design Add'.

Student 4
Student 4

Wow, that’s cool! Can decorators help with things like logging too?

Teacher
Teacher

Absolutely. We can log all methods within a class, making tracking even easier. Great observation!

Descriptors: Protocol and Custom Descriptors

Unlock Audio Lesson

Signup and Enroll to the course for listening the Audio Lesson

0:00
Teacher
Teacher

Finally, we’ll explore descriptors. What do you think a descriptor does?

Student 1
Student 1

Is it related to managing attribute access in a class?

Teacher
Teacher

Yes! Descriptors allow you to define methods for getting, setting, and deleting attributes. This provides enhanced control over how attributes behave.

Student 3
Student 3

Could we create a descriptor that enforces types?

Teacher
Teacher

Absolutely! Descriptors can be tailored to enforce validation. Remember the term 'VAD' for 'Validate Attribute Data' to help remember this.

Student 2
Student 2

This seems powerful for ensuring data integrity!

Teacher
Teacher

Indeed! Implementing descriptors can greatly enhance our class designs. Keep practicing these concepts!

Introduction & Overview

Read a summary of the section's main ideas. Choose from Basic, Medium, or Detailed.

Quick Overview

This section introduces Python decorators and descriptors, explaining how they modify or manage function and class behaviors.

Standard

The section covers the purpose and use of decorators for altering function behaviors, and descriptors for managing attribute access in classes. It includes examples for function decorators, class decorators, and built-in decorators like @property, @staticmethod, and @classmethod, along with an overview of the descriptor protocol.

Detailed

Detailed Summary

In this section, we explore Python decorators and descriptors which provide powerful tools for modifying functions and managing attributes in classes.

Decorators

  • Definition: A decorator is a higher-order function that allows for the alteration of a function’s behavior without modifying its source code. It wraps the original function, adding pre- or post-processing tasks.
  • Basic Usage: Decorators can be used with a simple syntax, by prefixing a function definition with @decorator_name, leading to concise and readable code.

Examples:

  1. Basic Decorator: Demonstrates how decorators can log messages around a function’s execution.
  2. Parameterized Decorators: Decorators can also accept arguments, requiring nested functions for implementation.
  3. Class Decorators: They can add attributes or wrap methods within a class, expanding their functionality.

Built-in Decorators:

  • @property, @staticmethod, @classmethod: These decorators facilitate attribute management and class method definitions, enhancing encapsulation and organization.

Descriptors

  • Definition: Descriptors are objects that define methods __get__, __set__, and __delete__, providing precise control over how attributes are accessed.
  • Usage: Useful for implementing type checks, validation, and lazy evaluations in class attributes.

Custom Descriptors:

This section concludes with custom descriptors that enforce type constraints on attributes, an essential technique for data integrity in classes.

Overall, mastering decorators and descriptors enables more elegant and efficient coding practices in Python.

Youtube Videos

#44 Python Tutorial for Beginners | Decorators
#44 Python Tutorial for Beginners | Decorators
Decorators in Python - Advanced Python 13 - Programming Tutorial
Decorators in Python - Advanced Python 13 - Programming Tutorial

Audio Book

Dive deep into the subject with an immersive audiobook experience.

Introduction to Decorators

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

A decorator in Python is a design pattern that allows you to modify the behavior of a function or class method without changing its source code. It’s a higher-order function that takes a function as input and returns a new function with added or altered behavior. Think of a decorator as a wrapper that "decorates" a function by extending or modifying its behavior.

Detailed Explanation

Decorators are functions that enhance another function's behavior without changing its actual code. To visualize it, imagine you have a beautifully wrapped gift (the function) inside a box (the decorator) that adds special features like glitter and stickers (enhancements), making it more attractive when presented. When you unwrap the gift, the core remains unchanged.

Examples & Analogies

Think about how a chef can add spices to a dish without changing its primary ingredients. Just like spices can enhance flavor, decorators enhance function behavior.

Basic Function Decorator Example

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

def my_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

def say_hello():
    print("Hello!")

decorated_func = my_decorator(say_hello)
decorated_func()

Output:

Before the function runs
Hello!
After the function runs

Using the @ syntax for decorators:

@my_decorator
def say_hello():
    print("Hello!")
say_hello()

This is equivalent to say_hello = my_decorator(say_hello).

Detailed Explanation

This chunk presents an example of a basic decorator in action. The my_decorator function takes another function, wraps it in a new function (the 'wrapper'), and modifies its behavior by printing messages before and after the wrapped function runs. The @ syntax is a shorthand for applying decorators, making the code easy to read and maintain.

Examples & Analogies

Imagine a ticket at a movie theater where the ticket signifies entry into the show (the base function). The usher (the decorator) checks your ticket at the door, adds some popcorn to your experience, and then lets you in. The core activity of watching the movie (the original function) remains unchanged.

Function Decorators: Concept and Usage

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

Decorators are widely used to add logging, access control, memoization, timing, and more.

Example: Logging Decorator

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args {args} and kwargs {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

add(3, 4)

Output:

Calling add with args (3, 4) and kwargs {}
add returned 7

Notice how the wrapper preserves the arguments and return value.

Detailed Explanation

This section explains the practical uses of decorators, particularly for logging function calls. The logger decorator records when a function is called, its parameters, and what it returns, providing valuable insights for debugging or monitoring. The decorator wraps any function it decorates, adding an extra layer of functionality without modifying the function's code.

Examples & Analogies

Think of a security camera in a shopping mall. The camera records video of all customer activity (logging) but doesn't interfere with the customers' shopping experience (the core function). This way, you can review footage if any issues arise, just like you would use logs to troubleshoot code.

Decorators with Parameters

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

Sometimes decorators themselves need parameters. This requires an extra level of nested functions.

Example: Decorator with Arguments

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")
greet("Alice")

This produces:

Hello, Alice!
Hello, Alice!
Hello, Alice!

Explanation:
● repeat(3) returns decorator, which receives greet.
● decorator returns wrapper, which calls greet 3 times.

Detailed Explanation

This chunk introduces decorators that require parameters for additional flexibility. In the example, repeat takes a number n and creates a decorator that makes the decorated function run n times. This involves creating two nested functions: one for the decorator itself and another to wrap the target function.

Examples & Analogies

Picture a trainer at a sports camp who has students practice a drill multiple times. The trainer criticizes and retests after every repetition (the decorator with parameters), ensuring better performance. Here, the greet function is practiced three times to make sure the students learn effectively.

Class Decorators

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

Decorators can also be applied to classes. They receive the class as an argument and can modify or replace it.

Example: Class Decorator that Adds an Attribute

def add_class_attr(cls):
    cls.extra_attribute = "Added by decorator"
    return cls

@add_class_attr
class MyClass:
    pass

print(MyClass.extra_attribute) # Output: Added by decorator

Example: Class Decorator That Wraps Methods

def method_logger(cls):
    for attr_name, attr_value in cls.__dict__.items():
        if callable(attr_value):
            setattr(cls, attr_name, logger(attr_value))
    return cls

@method_logger
class Calculator:
    def add(self, x, y):
        return x + y

calc = Calculator()
calc.add(5, 7)

This applies the earlier logger decorator to all methods.

Detailed Explanation

This section covers how decorators can be used with classes. Class decorators can modify class attributes or wrap methods in a similar way to function decorators. One example adds an extra attribute to a class, while another example demonstrates wrapping all methods of a class with a logging functionality. This shows decorators' versatility and how they enhance class behavior.

Examples & Analogies

Envision a teacher (the decorator) who brings in extra materials for a class (the decorated class). In the first instance, the teacher adds a new resource (attribute), and in the second, the teacher ensures all students keep a journal of their learning (logging method calls).

Built-in Decorators: @property, @staticmethod, and @classmethod

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

Python provides some powerful built-in decorators that are commonly used to manage class behaviors.

2.5.1 @property
@property allows you to use methods like attributes. It’s a way to add getter, setter, and deleter methods in a Pythonic way.

class Circle:
    def __init__(self, radius):
        self._radius = radius
    @property
    def radius(self):
        return self._radius
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    @property
    def area(self):
        import math
        return math.pi * (self._radius ** 2)
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53981633974483
c.radius = 10
print(c.area) # 314.1592653589793

Using @property improves encapsulation by allowing validation and calculation without changing the attribute access syntax.

Detailed Explanation

The @property decorator enables class methods to behave like attributes, allowing for attributes to be dynamically calculated or validated. In the example, Circle uses properties to control access to its radius and area, ensuring that the radius remains positive. This encapsulation helps maintain data integrity while providing a clean interface.

Examples & Analogies

Imagine a modern thermostat that adjusts the home temperature based on the current weather. The desired climate is set (the property), and changes are made automatically without needing to access and adjust the internal mechanisms, keeping users engaged without exposing the complex systems.

Static Methods and Class Methods

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

2.5.2 @staticmethod
A static method is a method inside a class that does not operate on an instance or the class itself.

class Math:
    @staticmethod
    def add(x, y):
        return x + y
print(Math.add(3, 4)) # 7

Static methods are used for utility functions logically related to the class.

2.5.3 @classmethod
Class methods receive the class (cls) as the first argument and can modify class state.

class Person:
    population = 0
    def __init__(self, name):
        self.name = name
        Person.population += 1
    @classmethod
    def get_population(cls):
        return cls.population
p1 = Person("Alice")
p2 = Person("Bob")
print(Person.get_population()) # 2

Detailed Explanation

These sections explain @staticmethod and @classmethod. A static method doesn't need access to an instance or class; it's like a regular function that lives inside a class. A class method, on the other hand, has access to the class state and can modify it. This functionality allows classes to manage data differently depending on their needs.

Examples & Analogies

Think of static methods as kitchen tools like a blender (which can prepare various foods without any reliance on a particular meal). Class methods are like an administrator who oversees operations (the class) for all activities (instances) happening in a building, keeping track of records and statistics.

Descriptor Protocol: __get__, __set__, and __delete__

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

What is a Descriptor?
A descriptor is an object attribute with β€œbinding behavior,” meaning it defines methods to control attribute access. Descriptors are the low-level mechanism behind properties, methods, and more.

The descriptor protocol defines three methods:
● __get__(self, instance, owner) β€” Retrieve the attribute value.
● __set__(self, instance, value) β€” Set the attribute value.
● __delete__(self, instance) β€” Delete the attribute.

Descriptors are used as class variables. When accessed via an instance, Python calls the descriptor’s methods.

Example: Simple Descriptor

class Descriptor:
    def __get__(self, instance, owner):
        print("Getting attribute")
        return instance._value
    def __set__(self, instance, value):
        print("Setting attribute")
        instance._value = value
    def __delete__(self, instance):
        print("Deleting attribute")
        del instance._value
class MyClass:
    attr = Descriptor()
obj = MyClass()
obj.attr = 42 # Setting attribute
print(obj.attr) # Getting attribute, Output: 42
del obj.attr # Deleting attribute

Detailed Explanation

The descriptor is a more advanced mechanism that controls how attributes are accessed. The __get__, __set__, and __delete__ methods allow precise control over getting, setting, and deleting values of attributes defined in a class. In the example, a Descriptor class manages the access to an attribute _value, providing additional functionality like logging each action.

Examples & Analogies

Imagine a library system where books (attributes) have strict control (descriptors). When you borrow a book, the librarian records who checks it out (getting), adds tracking for overdue returns (setting), and removes it from the system when returned (deleting). This ensures the library keeps accurate records while managing the access of its collection.

Creating Custom Descriptors for Attribute Management

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

Descriptors provide fine control over attribute access. Let’s create a descriptor that validates attribute values.

Example: Typed Attribute Descriptor

class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"Expected {self.expected_type}")
        instance.__dict__[self.name] = value
    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute")
class Person:
    name = Typed('name', str)
    age = Typed('age', int)
    def __init__(self, name, age):
        self.name = name
        self.age = age
p = Person("Alice", 30)
print(p.name) # Alice
p.age = 31 # Works fine
# p.age = "Thirty" # Raises TypeError: Expected 

Detailed Explanation

This chunk introduces custom descriptors that enforce specific data types for class attributes. The Typed descriptor checks if the assigned value matches the expected type, raising an error if it doesn't. This provides a powerful way to control attribute values, ensuring that class instances maintain valid states.

Examples & Analogies

Think of a strict school where students (class instances) are enrolled in specific grades (attribute types). Each student must show they meet the requirements for their gradeβ€”if a student claims to be in fifth grade but hasn't learned necessary skills, they won't be allowed to proceed, similar to how the Typed descriptor prevents invalid assignments.

Summary of Key Concepts

Unlock Audio Book

Signup and Enroll to the course for listening the Audio Book

In this chapter, you learned:
● What decorators are, and how to use function, parameterized, and class decorators.
● Python’s built-in decorators @property, @staticmethod, and @classmethod for managing class behaviors.
● The descriptor protocol (get, set, delete), the foundation of attribute management in Python.
● How to create custom descriptors to add controlled, reusable attribute access logic such as validation and type checking.

Detailed Explanation

This section summarizes the key points covered, emphasizing the significance of decorators and descriptors in Python programming. Understanding these concepts enables developers to write cleaner, more maintainable code that is easier to extend and debug.

Examples & Analogies

Consider a toolkit that includes various specialized tools (decorators and descriptors) for different tasks in a workshop. Knowing which tools to use for specific jobs helps craftsmen work more efficiently, just as decorators and descriptors help programmers manage and control code behavior effectively.

Definitions & Key Concepts

Learn essential terms and foundational ideas that form the basis of the topic.

Key Concepts

  • Decorators: Functions that wrap other functions to modify their behavior without altering their source codes.

  • Function Decorators: Specifically designed decorators for modifying standard functions, with various applications.

  • Class Decorators: Decorators that can modify or enhance class definitions instead of single functions.

  • Descriptors: Objects that customize the behavior of class attributes through defined methods.

Examples & Real-Life Applications

See how the concepts apply in real-world scenarios to understand their practical implications.

Examples

  • Basic Decorator: Demonstrates how decorators can log messages around a function’s execution.

  • Parameterized Decorators: Decorators can also accept arguments, requiring nested functions for implementation.

  • Class Decorators: They can add attributes or wrap methods within a class, expanding their functionality.

  • Built-in Decorators:

  • @property, @staticmethod, @classmethod: These decorators facilitate attribute management and class method definitions, enhancing encapsulation and organization.

  • Descriptors

  • Definition: Descriptors are objects that define methods __get__, __set__, and __delete__, providing precise control over how attributes are accessed.

  • Usage: Useful for implementing type checks, validation, and lazy evaluations in class attributes.

  • Custom Descriptors:

  • This section concludes with custom descriptors that enforce type constraints on attributes, an essential technique for data integrity in classes.

  • Overall, mastering decorators and descriptors enables more elegant and efficient coding practices in Python.

Memory Aids

Use mnemonics, acronyms, or visual cues to help remember key information more easily.

🎡 Rhymes Time

  • To decorate we wrap tight, adding flair, making functions right.

πŸ“– Fascinating Stories

  • Imagine a house where decorators change the paint, and each time you enter, there's something new. That's how decorators modify functions!

🧠 Other Memory Gems

  • Use 'VAD' for Descriptors: Validate Attribute Data when using them to implement controlled access.

🎯 Super Acronyms

WAP

  • Wrap and Add Behavior for remembering what decorators do.

Flash Cards

Review key concepts with flashcards.

Glossary of Terms

Review the Definitions for terms.

  • Term: Decorator

    Definition:

    A higher-order function that modifies a function or method's behavior without changing its source code.

  • Term: Descriptor

    Definition:

    An object attribute with binding behavior, defining methods to control attribute access in classes.

  • Term: Higherorder function

    Definition:

    A function that takes another function as an argument or returns a function as a result.

  • Term: @property

    Definition:

    A built-in decorator that allows methods to be accessed as attributes, providing property-like behavior.

  • Term: @staticmethod

    Definition:

    A built-in decorator that defines a method that does not operate on an instance or class.

  • Term: @classmethod

    Definition:

    A built-in decorator that defines a method that receives the class as the first argument.