Transcript
A decorator is a function that accepts a function and returns a function.
Decorators accept a function and return a function
We've defined an is_prime
function here:
>>> def is_prime(number):
... for n in range(2, number//2):
... if number % n == 0:
... return False
... return True
This function accept a number
and returns either True
or False
depending on whether that number is prime or not.
When we call is_prime
with a really big number, it takes a while (a half second or so) to check if number is prime:
>>> is_prime(73729259)
True
If we call it again with the same number (73729259
) it still takes a while because it does the same exact work again:
>>> is_prime(73729259)
True
We'd like to we speed up is_prime
so that whenever it won't do the same work twice if it's called with the same number again.
We can do this with the lru_cache
decorator (from Python's functools
module):
>>> from functools import lru_cache
The lru_cache
decorator accepts a function and returns a new function that wraps around the original function:
>>> is_prime = lru_cache(is_prime)
We're now pointed our is_prime
variable to whatever lru_cache
gave back to us (yes this is a little bit weird looking).
We can call is_prime
just as before, but we're not calling our original is_prime
function, we're calling the function that lru_cache
returned to us:
>>> is_prime(73729259)
True
The first time we call is_prime
with any number, it'll be slow. But the second time is_prime
will be fast (you can try it out yourself to see):
>>> is_prime(73729259)
True
The lru_cache
decorator returned a new function object to us which we're pointing our is_prime
variable to. When we call this new function, the new function calls our original is_prime
function (which we had passed to lru_cache
) and it caches the return value for each argument that it sees.
Every time this new function is called, it stores the inputs (the given function arguments) and the output (return value) that corresponds to those inputs.
Note: lru_cache
accepts a function as of Python 3.8 (before Python 3.8 you needed to call lru_cache(128)(my_function)
to get the same behavior).
The decorator syntax: using the @
symbol to apply decorators
This syntax for using a decorator is a little unusual:
>>> is_prime = lru_cache(is_prime)
We don't usually use decorators like this in Python. Instead, we tend to use a special decorator syntax, using an @
symbol.
If you want to make a function and then apply a decorator to your function you can use the @
symbol:
>>> from functools import lru_cache
>>> @lru_cache
... def is_prime(number):
... for n in range(2, number//2):
... if number % n == 0:
... return False
... return True
...
We're defining our is_prime
function here anew and we're decorating it with the lru_cache
decorator.
We're indicating to Python that we'd like to:
- Define the
is_prime
function - Take that function object and pass it to the
lru_cache
decorator - Take the return value we get from calling
lru_cache
and point ouris_prime
variable to that new function
So the is_prime
variable now points to the function that came back from calling lru_cache
(with our original is_prime
function):
Just as before, if we call is_prime
, the first time it's going to be slow:
>>> is_prime(73729259)
True
But if we call it with the same number again, it'll be fast the second time:
>>> is_prime(73729259)
True
And the reason is, is_prime
points to the function that came back from calling lru_cache
:
>>> is_prime
<functools._lru_cache_wrapper object at 0x0000017FD7EFE7C0>
The is_prime
variable doesn't point to our original is_prime
function. Instead it points to another function (a callable object technically) which wraps around our original is_prime
function.
Decorator functions, decorator classes, function decorators, and class decorators
I previously defined a decorator as a function that accepts a function and returns a function. That was a bit of a lie. All three of the "function" nouns in that sentence can instead be "class".
A decorator can be a class which accepts a function and returns an instance of that class (a "decorator class" that can be used as a "function decorator").
A decorator can also be a function which accepts a class (a "decorator function" that can be used as a "class decorator").
Decorators could be implemented as functions or classes
Let's look at a decorator that is implemented using a class rather than a function.
We have a Square
class that uses the property
decorator for making an auto-updating attribute:
class Square:
def __init__(self, width):
self.width = width
@property
def area(self):
return self.width**2
That property
decorator accepted a function but it didn't return a function, it returned a property object:
>>> Square.area
<property object at 0x000001CED0DC1360>
The property
decorator tha's built-in to Python is actually a class:
>>> property
<class 'property'>
The built-in property
decorator is a class which accepts a function (the area
method in our case) and returns an instance of the property
class.
So this function decorator (a decorator used for decorating functions) isn't a decorator function so much as a decorator class (meaning it's implemented using a class under the hood). But the thing we care about is that it's a callable that accepts a function, so we can use it the same way as any other decorator.
Aside: while functools.lru_cache
isn't a function but it actually returns a callable class instance (a functools._lru_cache_wrapper
object) rather than a function.
Decorating a class with a class decorator
We can also decorate classes.
We can make this same Square
class as before by using the dataclass
decorator:
@dataclass
class Square:
width: float
@property
def area(self):
return self.width**2
Note: the dataclasses
module was added in Python 3.7.
Square
is now a class that accepts a width
argument (even though we didn't make have a __init__
method):
>>> s = Square(2)
The dataclass
class decorator made our initializer for us. It also made a __repr__
method, so our Square
objects have a nice string representation:
>>> s
Square(width=2)
This all happened because we decorated our our Square
class with the dataclass
decorator.
Summary
A decorator is a callable (usually a function though sometimes a class) that accepts either a function or a class and returns a new function or class that wraps around the original one.
You can decorate a function or decorate a class while defining that function or class by using an @
symbol. The decorator syntax uses an @
symbol, the name of the decorator, and then the body of your function or class starting on the next line.
from Planet Python
via read more
No comments:
Post a Comment