Demystifying Decorators • Parts 3 to 7
The second 'Demystifying Decorators' article, which brings this mini-series on decorators to a conclusion
Let's complete our decorators journey. If you haven't done so already, you should start with the first article in this series, which covers Parts 1 and 2. It's important you're comfortable with the contents of Parts 1 and 2 before diving into the details of this article, which deals with Parts 3 to 7.
The journey I'm taking in 'Demystifying Decorators' is a step-by-step approach that's not rushed, with each step building in complexity from the previous one. Part 3 completes the 'Introduction to Decorators' segment. Part 4 consolidates knowledge with another example of a decorator. Then, Parts 5, 6, and 7 look at more complex uses and types of decorators, some of which are uncommon but still useful to help you understand decorators fully.
Let's get started with Part 3.
Part 3 • Using the @
Notation When Using Decorators
Let's start with the final code block you wrote in Part 2:

A reminder that the last line, which uses .__closure__
, is there for demonstration purposes only, and you won't need to use this technique in real code.
This gives me an opportunity to briefly recap Parts 1 and 2.
You use a decorator when you want to add functionality to an existing function. Common decorators allow you to add logging to a function or time its execution. But there are many more uses.
Let's break down the code above, and this will lead nicely to the main topic in Part 3:
You define
store_arguments()
, which accepts a parameter you callfunc
.The function
store_arguments()
is the decorator. You can use it to decorate other functions.The parameter
func
represents a function, which you pass to the decoratorstore_arguments()
when you call it. The function you pass tofunc
is the function you want to decorate.
You also define
inner()
withinstore_arguments()
. This is a function within a function.This
inner()
function accepts*args
and**kwargs
. Therefore, it can accept any number of positional and keyword arguments.This
inner()
function callsfunc()
within it, but it also performs some additional tasks. Recall thatfunc
is the function you want to decorate. In general, the inner function in a decorator can run additional tasks before and after callingfunc()
.And
inner()
should also return the value thatfunc()
returns.All this means that
inner()
is a replacement forfunc()
since it performs the same operations asfunc()
and returns the same value, but it also performs additional tasks.Note that
inner()
includes a reference todata
, which is a list defined in the outer function. The functioninner()
has access todata
even whenstore_arguments()
has finished its execution. This is a property of closures, which we discussed in Part 1.
The decorator,
store_arguments()
, returnsinner
, which is the decorated version offunc
.In the main code, you define
my_special_function()
and then you decorate it. The decoration happens with this line:
You redefine the name
my_special_function
so it now refers to the function returned bystore_arguments(my_special_function)
, which is the decorated function.
You can name functions anything you want, of course. However, it's common to see the inner function within the decorator function named wrapper
or a name that includes wrapper
within it. This highlights how this inner function wraps the original function within it. So, let's make this small change in name to align the code with the norm when dealing with decorators:
Let's dwell on the line that does the decoration for a few more sentences since it's key to how decorators work and also to what's coming next. Apologies for the repetition!
You call the decorator, the function store_arguments()
, and you pass the function you wish to decorate to it, which is my_special_function
. You pass the name of the function without calling it.
The decorator returns a decorated version of the original function—the inner function inner()
. So, in this case, this is a decorated version of my_special_function
. You reassign this decorated function to the same name, my_special_function
. Therefore, the name my_special_function
now refers to the decorated version.
The @
notation
But if you've used or seen decorators, they were most certainly used with the @
notation. This is syntactic sugar that makes decorators easier to use, but it also makes them more obscure for beginners. But not anymore (after the following few paragraphs).
Here's the line doing the decoration again:
You can remove this line and add the following line just before the definition of my_special_function
:
Here's the code that uses this notation to decorate a function:
The @store_arguments
just before the function definition decorates the function. It's equivalent to the line you used to decorate the function until now, which is still shown as a comment in the code above to stress the transition from one way of decorating to another. The @
notation is just a shortcut to make decoration easier.
We'll get back to the "longer" way of decorating later in this article whenever we need to understand what the @
syntax is doing in later, more complex, examples.
Wrapping properly
There's another minor issue we can resolve in Part 3 as we complete the introductory part of this decorators journey.
Let's inspect the decorated function, my_special_function()
:
You add a one-line docstring to my_special_function()
, which you'll use soon. Then, you pass the function to print()
—note that you don't call my_special_function
within print()
. You also show the function's help text using the built-in help()
. Here are the outputs from these two lines:
<function store_arguments.<locals>.wrapper at 0x1043ac900>
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
When you print the function to show the function object, Python doesn't show you my_special_function
. Instead, it shows you what the name my_special_function
actually refers to, which is the function named wrapper
. And wrapper
is a local variable in store_arguments()
. This is correct, but it's not useful when using the decorated function. This hides the fact you're calling a decorated version of my_special_function()
.
The help text also suffers from the same issue. It shows the function name as wrapper
and shows its signature. The program doesn't display the docstring you added to my_special_function()
, either. There's no useful help about my_special_function()
.
This result comes from the fact that you replaced the original function with a new one that performs the same tasks plus a bit more.
However, you can also pass the decorated function's metadata, including its name and docstring. This makes the decorated version even more like the original one.
As it happens, you need a decorator for this, which you can find in the functools
module:
This decorator is one you haven't defined. It's part of the Python standard library. It's also a bit different as it has parentheses and an argument. You'll deal with this type of decorator in Part 5. The wraps
decorator passes the func
metadata to wrapper
. Here's the output from the code now:
<function my_special_function at 0x1028c0900>
Help on function my_special_function in module __main__:
my_special_function(name, repeat)
This is a really special function. Trust me!
The function name is now displayed as my_special_function
, and the help text also shows information about my_special_function
, including its correct signature and the very useful, ahem, docstring!
It's a good practice to always use the functools.wraps
decorator when defining your own decorators.
Part 4 • The 'Limit Uses' Decorator
I don't apologise for taking a while to get to this point. I didn't want to rush the introductory parts that cover how decorators work and the fundamentals of how to use them.
But now, we can consolidate these fundamentals with a new example, and we'll speed up a bit, too.
Let's create a decorator that limits any function it's used on to only three uses. So, you can only call the decorated function three times. If you try to call the decorated function a fourth time, you'll get a message telling you that you've run out of attempts.
Since you now know the anatomy of a decorator, let's build this decorator using a different order of steps from the ones you've seen so far.
Step 1 • The decorator skeleton
A decorator is a function that accepts one parameter, which you normally name func
. And the decorator has an inner function, often called wrapper()
. The decorator function returns this inner function:
You call this decorator limit_to_three_uses()
. The wrapper()
inner function always has *args
and **kwargs
as parameters to enable you to use the decorator on any function.
Step 2 • The wrapping
The wrapper()
inner function is called a wrapper since it wraps the original function within it. The wrapper()
function is the replacement for the original function. Therefore, it should call the original function and return the same value the original function returns:
You call func()
with *args
and **kwargs
as arguments. This means func()
can be any function.
Step 3 • The counter
So far, this code is common to any decorator. Now, you focus on the specific decorator that limits the number of calls of a function. You need to keep track of how many times the decorated function is called so that you can limit it to three.
The counter needs to be a variable that's available to all the decorated function calls. It's a variable that's shared by all the decorated function calls. Therefore, you can define it within the closure:
You define counter
as a local variable in limit_to_three_uses()
, but wrapper()
still has access to it.
Now, you can use counter
within wrapper()
to decide whether to call the function func()
depending on how many times you've already called it:
Hope you noticed the 'Warning' in the comments. If you're using an IDE, it probably displayed a warning or some red squiggly lines to show you have an error. But let's not rely on the IDE and run the code to see Python's error messages. First, define a function to test this decorator:
This gives the following error:
Traceback (most recent call last):
File "...", line 23, in <module>
print(do_something(5, 9))
~~~~~~~~~~~~^^^^^^
File "...", line 7, in wrapper
if counter < 3:
^^^^^^^
UnboundLocalError: cannot access local variable 'counter'
where it is not associated with a value
The error is on line 7 when you use counter
in the if
statement if counter < 3:
. The error claims that you're using a local variable that's not associated with a value.
But surely, you define counter
on line 2, in the outer function limit_to_three_uses()
. What's going on?
The error message provides another clue with the phrase "local variable". Note that the error is raised within wrapper()
. Therefore, it seems that counter
is a local variable within wrapper()
. This wasn't a problem in the examples in Parts 1, 2, and 3.
The issue here arises because of the following line within wrapper()
:
counter += 1
This line is similar to counter = counter + 1
and, therefore, Python assumes that counter
is a local variable within wrapper()
. The name counter
in wrapper()
doesn't refer to the same object counter
in the outer function, limit_to_three_uses()
.
When you write counter
within wrapper()
, Python first looks in the local scope, and since counter += 1
creates a local variable within wrapper()
, the code raises an error since this variable doesn't have a value.
You'd like to clarify that you want to use the counter
that's defined in the enclosing scope rather than create a new local variable within wrapper
. Python refers to the enclosing scope as the non-local scope. And there's a nonlocal
keyword you can use in the inner function to make sure you use the same counter
in the enclosing scope:
And the problem is now gone. This code works:
45
You call the decorated function do_something(5, 9)
, which returns 45
.
However, to see the decoration in action, you'll need to call this function several times:
And here's the output from this code:
45
6
40
You've reached the maximum number of uses of 'do_something'
None
The first three calls return the value you expect from the function. However, when you call the decorated function a fourth time, you get a warning telling you that you can't use this function anymore.
Note that this decorated function doesn't return a value when this happens—or, more accurately, it returns None
.
One final change to the code, which is not needed in this example but it's a good practice:
You use the functools.wraps
decorator, as you learnt earlier in this article.
Part 5 • Decorators With Parameters
But the decorator limit_to_three_uses()
has some limits, if you excuse the pun. You can only use it to limit a function to three uses. Can we make it more flexible so that you can decide the limit each time you use the decorator?
The current definition of the decorator hardcodes the value 3
within it when you write the line if counter < 3:
. Ideally, you'd like to pass this value as an argument each time you use the decorator.
But where can you add the new parameter to accept this variable?
Decorator functions accept one argument: the function you want to decorate. When you use the @
decorator syntax, the function you define right after the decorator is passed to the decorator function as its argument. There's no place for another argument.
A common solution to this problem defines yet another function with—wait for this—another level of nesting. Yes, you already have two levels of nesting. You'll add a third soon.
Let's build this in steps.
Step 1 • Some refactoring
In the code you have so far, the decorator is limit_to_three_uses()
. Let's refactor this and rename the function using the generic and rather boring name decorator()
:
The rest of the code is commented out for now since limit_to_three_uses
doesn't exist.
The name decorator
is not a very descriptive name. Neither is wrapper
. Well, in a sense, they are very descriptive, but only in a general sense. They don't tell you anything about what they do in terms of the decorated function.
But that's OK because neither of these names will be exposed to the main code. The name wrapper
is already local to the outer function, now called decorator
. The name wrapper
is not used outside the decorator. And the name decorator
will also be removed from the global scope in the next step.
Step 2 • A third function. An extra layer of nesting
Next, you enclose the decorator along with its inner function within a new function. You can call this new function limit_uses()
. This is the function you'll use in your main code to decorate other functions. It only has one parameter in this example:
Although only one line is highlighted as changed in the code above, the rest of the decorator code has one more indent, too.
So, limit_uses()
is a function that accepts one argument, limit
.
This new function, limit_uses()
, is technically not a decorator, even though many will call it a decorator. A decorator takes a function as an argument and returns a function. But limit_uses()
takes an integer as an argument, not a function.
However, even though limit_uses()
isn't technically a decorator, it contains a decorator: The inner function you renamed decorator()
. This inner function in limit_uses()
is the same decorator you had in Part 4. Therefore, limit_uses()
can return this decorator so that the decorator can be accessed in the main program:
There are two changes in this version:
The
if
statement withinwrapper()
now useslimit
, the parameter you create when you definelimit_uses()
, rather than hardcoding the integer3
.The outermost function
limit_uses()
now returnsdecorator
.
So, limit_uses()
isn't a decorator. But it returns a decorator. This makes limit_uses()
a decorator factory. You can use it to create decorators with different limits on how often you can use the decorated function.
You can use limit_uses()
in the same place as you use the standard decorator, like the ones you learnt about earlier. However, the function limit_uses()
takes an argument:
You decorate do_something()
by adding @limit_uses(3)
just before its definition. Run this code. You'll see it gives the same output as the earlier version from Part 4. You can only run this function three times. However, you can pass a different integer when you use limit_uses()
to set a different limit.
But why does this work if limit_uses()
isn't a decorator?
So, limit_uses()
isn't a decorator. It's a decorator factory. So how can you use it in the same place you placed decorators earlier, and it still works?
Let's break this down by expanding the @
syntactic sugar notation into the fuller version. You can refer back to Part 4 if you need to.
Instead of using the syntactic sugar with @
, you can write the following line after the function definition:
The decorator factory limit_uses(3)
, which is a function, returns a decorator. Therefore, the notation @limit_uses(3)
is equivalent to @decorator
, but it also has access to the variable limit=3
.
As limit_uses(3)
returns a decorator, limit_uses(3)(do_something)
returns the decorated function since you pass do_something
to the decorator returned by limit_uses(3)
.
Take a deep breath here before reading the following sentence. So, do_something
now refers to the function returned by the decorator that's returned by limit_uses(3)
.
You have a decorator factory that you can use in the same way as the standard decorator. Although limit_uses
is technically a decorator factory and not a decorator, most people won't bother with the technical difference. They will say that limit_uses
is a decorator that accepts a parameter. And that's fine with me—I do so, too.
But there's another way of achieving this functionality. Time for Part 6.
Do you want to join a forum to discuss Python further with other Pythonistas? Upgrade to a paid subscription here on The Python Coding Stack to get exclusive access to The Python Coding Place's members' forum. More Python. More discussions. More fun.
And you'll also be supporting this publication. I put plenty of time and effort into crafting each article. Your support will help me keep this content coming regularly and, importantly, will help keep it free for everyone.
Part 6 • Using a Class as a Decorator
I ended Part 2 with a few summary thoughts, which included this one:
A decorator is a function that accepts another function as an argument and returns yet another function.
There are lots of "functions" in the statement.
But this is Python. And in Python, duck typing rules. Duck typing tells us that what matters is not what an object is but what it can do.
Functions are callable. You can replace "function" with "callable" in the bulleted statement above.
Functions aren't the only callable objects in Python. Yes, classes are also callable. But so are some other objects.
Hold that thought. We'll get back to it.
Step 1 • Creating a limit_uses
class
Let's create a class called limit_uses
. I'm deliberately "breaking" the naming convention for classes with a lower snake case name. I want this class to be a direct replacement for the limit_uses()
function from Part 5. It can be used like a function, so it's fine to bend the convention!
I kept the code you used to test the previous decorator, but it's commented for now. The commented code is unchanged from the code in Part 5.
The class limit_uses
has an .__init__()
method that accepts one argument, which is assigned to the data attribute .limit
.
This data attribute .limit
—or you can call it an instance variable if you prefer—serves the same purpose as limit
in the function-based decorator in Part 5.
In the function-based decorator, limit
is a local variable in the outer function that's also accessible by the inner functions since they're closures.
But in the class limit_uses
, an instance of the class has access to the data attribute self.limit
. This is how classes work, after all!
Therefore, limit_uses(3)
now creates an instance of limit_uses
with .limit
having the value of 3
.
But in the function version in Part 5, limit_uses(3)
was a function that returns a decorator function. Let's see how we can adapt the class-based version to match this.
Step 2 • Creating a decorator within the class
Let's rephrase the last thought in the previous section. You want limit_uses(3)
, whether it's a decorator function (as in Part 5) or a class (as in this Part), to return a decorator callable. I used the term "callable" instead of "function"—it's more general, and it fits the duck typing philosophy.
So, you can make the instance of the limit_uses
class callable by adding .__call__()
in the class definition.
But you don't want limit_uses(3)
to give you any callable. Instead, you want it to return a decorator callable. Recall that a decorator accepts a single argument, which is a function:
See the similarity? The .__call__()
special method is equivalent to the decorator()
inner function in the function-based solution in Part 5.
This decorator still has access to the limit value since .limit
is a data attribute, and every instance has access to it.
But you also need a counter to keep track of the number of function calls. You need to keep track of the number of times the decorated function is called. Recall that wrapper()
will be the decorated function when you complete this code.
In the function-based decorator, you relied on closures to ensure the decorator had access to counter
. You don't need that now you're using classes:
The data attribute .counter
can be accessed by any instance of the limit_uses
class.
Finally, you can finish off wrapper()
, which is the inner function within .__call__()
. The code in wrapper()
is very similar to the code in the wrapper()
function from Part 5:
The class limit_uses
creates objects that are callable. And these objects are decorators since the class's .__call__()
method follows the decorator pattern.
So, you can use @limit_uses(3)
in the same way as you did earlier:
You can do this since @limit_uses(3)
returns a callable that's a decorator. This code gives the same output as its counterpart in Part 5 when you defined a function-based decorator instead of a class-based one.
Which version do you prefer, the function-based version from Part 5 or the class-based version shown here in Part 6?
Part 7 • Decorating Classes
Let's keep this final Part brief. You've already covered everything you need to know about decorators. But here's one more bit.
Remember this:
A decorator is a callable that accepts another callable as an argument and returns yet another callable.
Up to Part 5, all these callables were functions. In Part 6, the decorator was a callable object created by a class, but you still used this decorator to decorate a function.
Let's now use a function to decorate a class. So, the decorator in this section is a function. But the callable you pass to the decorator is a class, and the decorator returns a class.
I'll move quickly through this section.
Imagine you have several classes that don't have a .__repr__()
special method, and you want an easy way to add it retrospectively. You can use a decorator. Let's call it add_repr()
. You define a function-based decorator:
Previously, your function-based decorators accepted a function—you used the parameter name func
. In this case, you'll pass a class to add_repr()
, which is why you choose the parameter name cls
. A class is still a callable!
And a decorator should return a callable. In this case, you'll return the same class since you'll make changes directly to the class rather than create a new one:
Since you want to add a .__repr__()
method to a class that doesn't already have one, you can define a function that will serve as this special method:
You define a function called __repr__()
. You can call this function anything you want since this is not the class yet. But you want to insert this function as a method in the class, so you use the name __repr__()
to mirror the name of the .__repr__()
special method. This is the same reason you use self
as the variable name even though you don't define __repr__()
within a class since you'll transpose this function into a class soon.
You create a generator called attributes
within __repr__()
using a generator expression that uses the attributes of the object. Then, you create and return a string using this generator.
Finally, you assign this function to cls.__repr__
. Therefore, the class now has a .__repr__()
special method!
The decorator is mostly done. Let's create a class to test this on:
This class doesn't have a .__repr__()
special method. So, you get the default representation when you show the repr()
string representation, which is not very informative:
<__main__.SomeClass object at 0x104e55cc0>
Time to decorate the class:
That's it. You simply add @add_repr
before the class definition. Here's the output now:
SomeClass(first='hello', second='bye')
This is the expected repr()
string representation.
You pass the class SomeClass
to the decorator function add_repr
. The decorator returns the same class, but the class now has a .__repr__()
method.
One last change, for completeness. If you use this decorator on a class that already has a .__repr__()
method, the decorator will override the existing .__repr__()
. Not good. Let's fix this:
The definition of the __repr__()
function and the assignment to cls.__repr__
are now within an if
block that checks whether the class already has the attribute .__repr__
.
Final Words
There's plenty to digest in this decorator tour de force. Depending on how new you are to this topic, you may need to re-read Parts 1-7 again in full or in part.
You may never need to write your own decorator. But next time you have to use the @
syntax with an existing decorator, you'll really know what's happening behind the scenes to give you the decorator magic!
And perhaps you'll have some code in which you want to retrofit some functionality, or you want to flexibly add functionality to functions, and you'll reach out to decorators…
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
You can also support this publication by making a one-off contribution of any amount you wish.
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
Appendix: Code Blocks
Code Block #1
def store_arguments(func):
data = []
def inner(*args, **kwargs):
data.append((args, kwargs))
value = func(*args, **kwargs)
return value
return inner
def my_special_function(name, repeat):
return name.upper() * repeat
my_special_function = store_arguments(my_special_function)
print(my_special_function("James", 3))
print(my_special_function("Maive", 2))
print(
my_special_function.__closure__[0].cell_contents
)
Code Block #2
my_special_function = store_arguments(my_special_function)
Code Block #3
def store_arguments(func):
data = []
def wrapper(*args, **kwargs):
data.append((args, kwargs))
value = func(*args, **kwargs)
return value
return wrapper
def my_special_function(name, repeat):
return name.upper() * repeat
my_special_function = store_arguments(my_special_function)
Code Block #4
my_special_function = store_arguments(my_special_function)
Code Block #5
@store_arguments
Code Block #6
def store_arguments(func):
data = []
def wrapper(*args, **kwargs):
data.append((args, kwargs))
value = func(*args, **kwargs)
return value
return wrapper
@store_arguments
def my_special_function(name, repeat):
return name.upper() * repeat
# my_special_function = store_arguments(my_special_function)
print(my_special_function("James", 3))
print(my_special_function("Maive", 2))
print(
my_special_function.__closure__[0].cell_contents
)
Code Block #7
def store_arguments(func):
data = []
def wrapper(*args, **kwargs):
data.append((args, kwargs))
value = func(*args, **kwargs)
return value
return wrapper
@store_arguments
def my_special_function(name, repeat):
"""This is a really special function. Trust me!"""
return name.upper() * repeat
print(my_special_function)
help(my_special_function)
Code Block #8
from functools import wraps
def store_arguments(func):
data = []
@wraps(func)
def wrapper(*args, **kwargs):
data.append((args, kwargs))
value = func(*args, **kwargs)
return value
return wrapper
@store_arguments
def my_special_function(name, repeat):
"""This is a really special function. Trust me!"""
return name.upper() * repeat
print(my_special_function)
help(my_special_function)
Code Block #9
def limit_to_three_uses(func):
def wrapper(*args, **kwargs):
...
return wrapper
Code Block #10
def limit_to_three_uses(func):
def wrapper(*args, **kwargs):
output = func(*args, **kwargs)
return output
return wrapper
Code Block #11
def limit_to_three_uses(func):
counter = 0
def wrapper(*args, **kwargs):
output = func(*args, **kwargs)
return output
return wrapper
Code Block #12
def limit_to_three_uses(func):
counter = 0
def wrapper(*args, **kwargs):
# Warning:
# There's still something missing here!
if counter < 3:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
Code Block #13
# ...
@limit_to_three_uses
def do_something(first, second):
return first * second
print(do_something(5, 9))
Code Block #14
def limit_to_three_uses(func):
counter = 0
def wrapper(*args, **kwargs):
nonlocal counter
if counter < 3:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
@limit_to_three_uses
def do_something(first, second):
return first * second
print(do_something(5, 9))
Code Block #15
# ...
print(do_something(5, 9))
print(do_something(2, 3))
print(do_something(4, 10))
print(do_something(5, 4))
Code Block #16
from functools import wraps
def limit_to_three_uses(func):
counter = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal counter
if counter < 3:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
Code Block #17
from functools import wraps
def decorator(func):
counter = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal counter
if counter < 3:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
# @limit_to_three_uses
# def do_something(first, second):
# return first * second
#
# print(do_something(5, 9))
# print(do_something(2, 3))
# print(do_something(4, 10))
# print(do_something(5, 4))
Code Block #18
from functools import wraps
def limit_uses(limit):
def decorator(func):
counter = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal counter
if counter < 3:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
# @limit_to_three_uses
# def do_something(first, second):
# return first * second
#
# print(do_something(5, 9))
# print(do_something(2, 3))
# print(do_something(4, 10))
# print(do_something(5, 4))
Code Block #19
from functools import wraps
def limit_uses(limit):
def decorator(func):
counter = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal counter
if counter < limit:
counter += 1
output = func(*args, **kwargs)
return output
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
return decorator
# @limit_to_three_uses
# def do_something(first, second):
# return first * second
#
# print(do_something(5, 9))
# print(do_something(2, 3))
# print(do_something(4, 10))
# print(do_something(5, 4))
Code Block #20
# ...
@limit_uses(3)
def do_something(first, second):
return first * second
print(do_something(5, 9))
print(do_something(2, 3))
print(do_something(4, 10))
print(do_something(5, 4))
Code Block #21
do_something = limit_uses(3)(do_something)
Code Block #22
class limit_uses:
def __init__(self, limit):
self.limit = limit
# @limit_uses(3)
# def do_something(first, second):
# return first * second
#
# print(do_something(5, 9))
# print(do_something(2, 3))
# print(do_something(4, 10))
# print(do_something(5, 4))
Code Block #23
class limit_uses:
def __init__(self, limit):
self.limit = limit
def __call__(self, func):
def wrapper(*args, **kwargs):
...
return wrapper
Code Block #24
class limit_uses:
def __init__(self, limit):
self.limit = limit
self.counter = 0
def __call__(self, func):
def wrapper(*args, **kwargs):
...
return wrapper
Code Block #25
class limit_uses:
def __init__(self, limit):
self.limit = limit
self.counter = 0
def __call__(self, func):
def wrapper(*args, **kwargs):
if self.counter < self.limit:
self.counter += 1
return func(*args, **kwargs)
else:
print(
f"You've reached the maximum number "
f"of uses of {func.__name__!r}"
)
return wrapper
Code Block #26
# ...
@limit_uses(3)
def do_something(first, second):
return first * second
print(do_something(5, 9))
print(do_something(2, 3))
print(do_something(4, 10))
print(do_something(5, 4))
Code Block #27
def add_repr(cls):
...
Code Block #28
def add_repr(cls):
...
return cls
Code Block #29
def add_repr(cls):
def __repr__(self):
attributes = (
f"{key}={value!r}"
for key, value in vars(self).items()
)
return f"{type(self).__name__}({', '.join(attributes)})"
cls.__repr__ = __repr__
return cls
Code Block #30
# ...
class SomeClass:
def __init__(self, first, second):
self.first = first
self.second = second
print(
repr(SomeClass("hello", "bye"))
)
Code Block #31
# ...
@add_repr
class SomeClass:
def __init__(self, first, second):
self.first = first
self.second = second
print(
repr(SomeClass("hello", "bye"))
)
Code Block #32
def add_repr(cls):
if not hasattr(cls, "__repr__"):
def __repr__(self):
attributes = (
f"{key}={value!r}"
for key, value in vars(self).items()
)
return f"{type(self).__name__}({', '.join(attributes)})"
cls.__repr__ = __repr__
return cls
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Cool, I did not know about @wraps, and it never occurred to me to use a class as a decorator. Sweet!
Just want to say very nicely done Stephen!