-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecoratorExamples.py
More file actions
160 lines (130 loc) · 4.83 KB
/
Copy pathdecoratorExamples.py
File metadata and controls
160 lines (130 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
from functools import wraps # this decorator preserves function metadata
####################### BASIC DECORATOR AND OVERVIEW ##########################
# Let's say we have this function:
def sayMyName_original(firstName, lastName):
return f"{firstName} {lastName}"
# If we want to supplement its functionality, we can make a wrapper function.
# But if we have numerous functions and want to add similar functionality to
# many of them, that's when decorators can come in handy:
# Decorators let us easily mass-produce wrappers!
#
# A decorator is a function that takes a function as an argument and returns a
# function that wraps the passed function.
#
# This means we're making:
# a function
# which makes a wrapper function
# which runs a function.
#
# The below decorator creates a wrapper that adds "Ser " before the value
# returned by the wrapped function.
def knight(myFunction):
print("Creating wrapped function...")
@wraps(myFunction)
def wrapper(*args, **kwargs):
print("Wrapper is about to run function...")
result = myFunction(*args, **kwargs)
print("Wrapper is finished running function!")
return f"Ser {result}"
return wrapper
# The print functions within the wrapper are there to demonstrate how your
# wrapper can make modifications to behavior before and/or after the
# internal function is called. You can do things like modify arguments,
# modify return values, or track meta behavior like how long it took for
# a function to execute.
# Above we made a decorator, a function that returns a wrapped function.
# Now let's decorate our initial function.
# We just pass our initial function into the decorating function, which then
# returns a wrapped version of our initial function.
sayMyName = knight(sayMyName_original)
# Now let's see the results:
myName = sayMyName("Jon", "Arbuckle")
print(f"My name is {myName}.")
print()
# However, there's a more common way to apply decorators, one that uses a
# special syntax and makes the process more convenient.
# We can apply the decorator right when we're declaring our initial function:
@knight
def sayYourName(firstName, lastName):
return f"{firstName} {lastName}"
# That did the same thing as the previous approach, except:
# - it looks a bit prettier
# - it doesn't create an extra throwaway function name
# Now let's see the results:
yourName = sayYourName("Jon", "Arbuckle")
print(f"Your name is {yourName}.")
# As expected, the output is just the same as the previous method we used.
print('\n')
########################## DECORATOR WITH ARGUMENTS ###########################
#
# To do this, we're going to need to make a function that RETURNS a decorator.
# This means we're making:
# a function
# which makes a decorator function
# which makes a wrapper function
# which runs a function.
# We've added one more function layer!
def decoratorMaker(htmlTag):
def decoratingFunction(myFunction):
@wraps(myFunction)
def wrapper(*args, **kwargs):
result = myFunction(*args, **kwargs)
return f'<{htmlTag}>{result}</{htmlTag}>'
return wrapper
return decoratingFunction
applyTag = decoratorMaker # alias is unnecessary, just helps conceptualize
@applyTag("b")
@applyTag("i")
def bodyText(text):
return text
print( bodyText("Heyo!") )
# We can see that the decorator was used to add 2 HTML tags around the text.
print('\n')
############################# SOME NOTES ######################################
#
# 1. The order that multiple decorators are applied can matter.
#
# 2. Each decorator adds an additional function call to calling a function;
# consider that for both your stack and your speed.
#
####################### SOME PRACTICAL EXAMPLES ###############################
# only applies the decorators if we're testing
TEST_MODE = True
# a decorator that outputs time spent executing its function
def testSpeed(myFunction):
if not TEST_MODE:
return myFunction
import time
@wraps(myFunction)
def wrapper(*args, **kwargs):
timeBeforeRun = time.process_time_ns()
result = myFunction(*args, **kwargs)
timeElapsed = time.process_time_ns() - timeBeforeRun
print(f"{myFunction.__name__} took {timeElapsed} nanoseconds.")
return result
return wrapper
# a decorator that tracks how many times a function has been called
def countCalls(myFunction):
if not TEST_MODE:
return myFunction
@wraps(myFunction)
def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
result = myFunction(*args, **kwargs)
print(f"{myFunction.__name__} called {wrapper.count} time(s) total.")
return result
wrapper.count = 0
return wrapper
@testSpeed
@countCalls
def countToTenMillion():
for i in range(0, 10000000):
pass
print("Finished counting!")
countToTenMillion()
print()
countToTenMillion()
print()
countToTenMillion()
# For more thorough explanations:
# https://stackoverflow.com/questions/739654/how-do-i-make-function-decorators-and-chain-them-together