Practical Decorators: Reuven M. Lerner - Pycon 2019 Reuven@Lerner - Co.Il - @reuvenmlerner
Practical Decorators: Reuven M. Lerner - Pycon 2019 Reuven@Lerner - Co.Il - @reuvenmlerner
[email protected] • @reuvenmlerner
1
Free “Better developers”
weekly newsletter
Corporate Python
training
Weekly Python
Exercise
2
3
4
Let’s decorate a function!
See this: But think this:
@mydeco
return a + b return a + b
add = mydeco(add)
5
Three callables!
(2) The decorator
function
@mydeco
return a + b
from mydeco(add),
return wrapper
from mydeco(add),
Executes once,
def mydeco(func): when we decorate
the function
return wrapper
Executes each time
the decorated
function runs
8
Wow, decorators are cool!
9
Better yet:
Decorators are useful
10
Example 1: Timing
How long does it take for a function to run?
11
My plan
• But it’ll keep track of the time before and after doing so
12
def logtime(func):
start_time = time.time()
outfile.write(f'{time.time()}\t{func.__name__}\t{total_time}\n')
return result
return wrapper
13
@logtime
time.sleep(2)
return a + b
@logtime
time.sleep(3)
return a * b
14
1556147289.666728 slow_add 2.00215220451355
15
def logtime(func): (1) The decorated
function
def wrapper(*args, **kwargs):
start_time = time.time()
outfile.write(f'{time.time()}\t{func.__name__}\t{total_time}\n')
return result
16
Example 2: Once per min
Raise an exception if we try to run
a function more than once in 60 seconds
17
Limit
def once_per_minute(func): (1) The decorated
function
(2) The decorator
18
We need “nonlocal”!
def once_per_minute(func):
last_invoked = 0
nonlocal last_invoked
last_invoked = time.time()
return wrapper
19
We need “nonlocal”!
def once_per_minute(func): Executes once,
when we decorate
last_invoked = 0 the function
nonlocal last_invoked
last_invoked = time.time()
print(add(3, 3))
21
Example 3: Once per n
Raise an exception if we try to run
a function more than once in n seconds
22
Remember
When we see this: We should think this:
@once_per_minute
return a + b return a + b
add = once_per_minute(add)
23
So what do we do now?
This code: Becomes this:
@once_per_n(5)
return a + b return a + b
add = once_per_n(5)(add)
24
That’s right: 4 callables!
(1) The decorated
def add(a, b):
function
return a + b
(2) The decorator
(3) The return value
from once_per_n(5),
itself a callable, invoked on “add”
add = once_per_n(5)(add)
25
How does this
look in code?
For four callables,
we need three levels of function!
26
def once_per_n(n): (2) The decorator
def middle(func): (1) The decorated
function
last_invoked = 0
nonlocal last_invoked
from middle(func)
return func(*args, **kwargs)
return wrapper
nonlocal last_invoked
last_invoked = time.time()
28
Does it work?
print(slow_add(2, 2))
print(slow_add(3, 3))
29
Example 4: Memoization
Cache the results of function calls,
so we don’t need to call them again
30
def memoize(func): (1) The decorated function
cache = {}
else:
return cache[args]
(3) The return value
from memoize(func),
return wrapper
assigned back to the function
31
Executes once, when we
decorate the function
def memoize(func):
else:
return cache[args]
return wrapper
32
Does it work?
@memoize
print("Running add!")
return a + b
@memoize
print("Running mul!")
return a * b
33
Caching NEW value for add(3, 7)
Running add!
10
print(add(3, 7))
Caching NEW value for mul(3, 7)
print(add(3, 7)) 21
21
34
Wait a second…
35
Pickle to the rescue!
• If all this doesn’t work, you can always call the function!
36
def memoize(func):
cache = {}
t = (pickle.dumps(args), pickle.dumps(kwargs))
if t not in cache:
else:
return cache[t]
return wrapper
37
Example 5: Attributes
Give many objects the same attributes,
but without using inheritance
38
Setting class attributes
39
Let’s improve __repr__
def fancy_repr(self):
40
Our implementation
(2) The decorator
c.__repr__ = fancy_repr
o = c(*args, **kwargs)
return o
41
Our 2nd implementation
(2) The decorator
c.__repr__ = fancy_repr
42
Does it work?
@better_repr
class Foo():
self.x = x
self.y = y
print(f)
I'm a Foo, with vars {'x': 10, 'y': [10, 20, 30]}
43
Wait a moment!
We set a class attribute.
Can we also change object attributes?
44
Of course.
45
Let’s give every object
its own birthday
46
Our implementation
(2) The decorator
o = c(*args, **kwargs)
o._created_at = time.time()
(3) The returned object —
47
Does it work?
@object_birthday
class Foo():
self.x = x
<__main__.Foo object at 0x106c82f98>
self.y = y
1556536616.5308428
print(f)
print(f._created_at)
48
Let’s do both!
def object_birthday(c):
to the class
o = c(*args, **kwargs)
o._created_at = time.time()
return o
Add an attribute
49
Conclusions
• Decorators let you DRY up your callables
50
Questions?
• Get the code + slides from this talk:
• http://PracticalDecorators.com/
• Or contact me:
• Twitter: @reuvenmlerner
51