- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: Using Decorators in Python
Python's dynamic capabilities make implementing various design patterns straightforward. In this lab, you will explore the Singleton pattern using Python decorators, enabling you to easily apply this pattern to any class. By the end of the lab, you will understand how to utilize decorators to create shareable and maintainable code while effectively implementing the Singleton pattern.
Lab Info
Table of Contents
-
Challenge
Guided: Using Decorators in Python
Guided: Using Decorators in Python
Welcome to this hands-on learning experience.
Decorators in Python are like wrappers that add extra functionality to existing code without modifying it directly.
Imagine you have a gift box (your code), and you want to enhance it by wrapping it in decorative paper (a decorator). The gift remains the same, but its presentation improves, making it more useful or appealing. Similarly, decorators "wrap" functions or classes, adding new behavior.
You may have seen the common built-in
@propertydecorator to declare agettermethod:class Example: . . . @property def attribute(): . . .sIn this lab, you will learn how to create your own decorators. Specifically, you'll create a singleton decorator that will wrap this design pattern around any class.
Common Use Cases
______Logging: Automatically recording details of function calls, such as arguments, return values, and execution time
Caching: Storing the results of expensive function calls to improve performance by reusing them when the same inputs occur again
Authentication: Verifying user permissions or credentials before allowing the execution of sensitive functions
Learning Objectives:
By the end of this lab, you will create a decorator to implement the singleton pattern, ensuring a class has only one instance. This is useful for configuration management or logging.
Through practical examples, you'll learn how to build a reusable and efficient decorator.
Press the double right arrow button to begin.Â
-
Challenge
Python Decorators
Python Decorators
Decorators in Python are higher-order functions, meaning they take another function as input, add extra functionality around it, and return the enhanced version.
Like the gift box analogy being enhanced by the wrapping paper — the original function stays the same, but its functionality is enhanced.
In code, this means the decorator takes a function, wraps it with additional behavior (like logging or access control), and returns a new function with the combined functionality.
Key Benefits
----- Decorators provide several key benefits:Code Reusability: Encapsulate common functionality, such as logging or configuration.
Reduce Redundancy: Adhere to the DRY (Don't Repeat Yourself) principle by avoiding repetitive code.
Separation of Concerns: Isolate auxiliary logic from the core functionality of a function.
Simple Example
To see a simple version of this in action, open the **/demo/simple.py ** script file.
Starting at the top, the
simple_decorator()function is declared to accept another function (func) as its argument.The nested
wrapper()function then adds new actions before and after calling the passed function. Theresultsof calling this function are then returned to ensure that the decorated function produces the same output as the original.Understanding (*args, **kwargs)
______ The parameters `\*args` and `\*\*kwargs` are referred to as variable-length arguments. They allow functions to accept any number of positional and keyword arguments, respectively.This allows decorators to be flexible and able to handle functions with varying parameters.
The
simple_decorator()function then returns the nestedwrapperfunction declaration, allowing it to replace the decorated function with an enhanced version. This enhanced version still preserves the original intended behavior.Notice that it does not return
wrapper(). Doing that would execute the wrapper function immediately.This structure makes it easy to reuse the
simple_decorator, consistently applying enhancements across multiple functions in your codebase.Applying the Decorator
To apply the
simple_decorator, place it directly above theexample_function()prepending the@symbol.The
example_function()should now look like:@simple_decorator def example_function(): . . .This shorthand
@syntax makes it clear that the function is being enhanced by the decorator.Testing the Decorator
You can test this functionality by running the script:
python3 demo/simple.pyThe expected output will look like this:
Calling function example_function... Function executed. Additional functionalityThis simple example demonstrates how a decorator can enhance the behavior of a function while maintaining its original intended behavior.
Before creating your own singleton decorator, take a moment to review the singleton pattern in the next step.
-
Challenge
Understanding the Singleton Pattern
Understanding the Singleton Pattern
A singleton is a design pattern that ensures a class has only one instance while providing a global point of access to that instance.
It's often used when managing shared resources like configuration settings, logging systems, or connection pools.
By controlling the instance creation process, a singleton ensures consistency across your application.
Basic Implementation Example
There are several ways to implement the singleton pattern.
A straightforward example using the
__new__method has been provided in the **/demo/singleton.py ** script file.In a moment, you'll see the purpose for the class-level attribute
_instance. Take note that as a class-level attribute, this value will be available to every caller of theConfigurationclass.When a new object of this class is instantiated, Python calls the
__new__()method. Here, you'll see that theConfigurationclass is overriding this method in order to control the class instance creation process.The
clspassed to this method refers to the class definition, which provides access to the class-level_instanceattribute.cls vs self
______clsis used in class methods to refer to the class itself, whereasselfis used in instance methods to refer to a specific instance of the class.If the
cls._instancehas not yet been set, then a new instance is created and stored in the shared class-level attribute_instance.This shared instance is then returned to the caller.
Testing Singleton Behavior
To verify the singleton behavior of the
Configurationclass, two instances of the the class are requested inconfig1andconfig2.The identity of both of these instances are then compared using the
isoperator.Run this comparison in the Terminal using the command:
python3 demo/singleton.pyThe expected output is:
True. This means that both of these variables are pointing to the same object.Basic Singleton Implementation
----- This example demonstrates the basic concept of the singleton pattern, but lacks essential features for real-world applications.For instance, it does not ensure thread safety, which can cause issues in multi-threaded environments.
Additionally, by omitting
__init__logic, it does not support instance-specific initialization, reducing its flexibility in practical use.These limitations highlight the importance of adapting the pattern for more robust and production-ready scenarios.
With a basic understanding of the singleton pattern, you're ready to take the next step and create a reusable
singletondecorator. -
Challenge
Refactoring to a Singleton Decorator
Refactoring to a Singleton Decorator
In the previous implementation, the
Configurationclass directly managed the singleton behavior by overriding the__new__method and storing a single instance in a class-level attribute.While this approach works, it can become repetitive when applied across multiple classes.
Using a decorator reduces code duplication, improves readability, and makes the singleton logic reusable across different classes, ensuring consistency.
In this step, you'll extract the singleton logic into a decorator and apply it to the
Configurationclass.Identifying the Singleton Decorator
You can start by identifying the common elements of the singleton implementation in the same **/demo/singleton.py ** file.
Specifically, identity the elements for the single instance storage and the logic for managing this single instance.
Extracting the Singleton Components
To transform this into a functional decorator, you will need the decorator function to:
- Accept the class to be decorated as a parameter.
- Implement the instance storage for all decorated classes.
- Manage the instantiation of each decorated class.
- Then return the managed instance to the caller.
Note: If you get stuck with these refactorings, the solution will be provided below.
Accept the Class to be Decorated:
To get started, at the top of the file, declare a
singleton()function that accepts aclsparameter. This will allow this decorator to operate on the specific class being decorated.def singleton(cls):Implement the Instance Storage:
Analyzing the
_instancestorage in theConfigurationexample, it only needs to support one instance of this class.This decorator will need to support multiple instances for each class that will be decorated. This can be accomplished by using a dictionary.
Nested in the
singleton()function, declare the following local variable._instances = {}Manage the Instance Instantiation:
This task will require some refactoring of the
Configurationexamples__new__override.As a function decorator there is not a
__new__()method to override, instead you'll follow the earlier example by creating a nestedwrapper(*args, **kwargs)function.As for the instance check, since
_instancesis now a dictionary you'll need to check if it contains the givencls.Note: Since
_instancesis nested inside of a function, it is now a local variable and can be accessed directly.As before, if the
clsinstance does not exist, a new one will need to be instantiated. This also differs from the example since the class can be instantiated directly by passing the variable-length arguments.This instance will also need to be stored differently in the
_instancesdictionary, using the passedclsas the key.def wrapper(*args, **kwargs): if cls not in _instances: _instances[cls] = cls(*args, **kwargs) return _instances[cls]Return the Managed Instance:
With a completed
wrapper()function, you can return this function definition to the caller.The final
singletondecorator should now look like:def singleton(cls): _instances = {} def wrapper(*args, **kwargs): if cls not in _instances: _instances[cls] = cls(*args, **kwargs) return _instances[cls] return wrapperTesting the
singletondecoratorTo test this
singletondecorator, you'll need to apply it to theConfigurationclass and verify that multiple instances reference the same object.You will also need to remove all of the singleton logic from the
Configurationclass. For this simple implementation, you can replace the whole class body with thepassplaceholder.The new
Configurationdeclaration should now look like:@singleton class Configuration: passIf you are not sure you implemented this correctly, you can compare to the solution **/demo/singleton_decorator.py ** file.
Run the script again in the Terminal:
python3 demo/singleton.pyYou should see the same expected output of
True, meaning that both of these variables are still pointing to the same object.Congratulations on successfully implementing and testing the
singletondecorator!This is a foundational example of how decorators work. While it is a simple implementation, it provides a strong basis for exploring advanced decorator features and addressing edge cases, such as thread safety and handling arguments dynamically.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.