Python/Amazing Functools Features in Python.md
... ...
@@ -0,0 +1,296 @@
1
+https://python.plainenglish.io/amazing-functools-features-in-python-3130684a9c37
2
+
3
+# Amazing Functools Features in Python | by Vivek K. Singh | Python in Plain English
4
+[
5
+
6
+![Vivek K. Singh](https://miro.medium.com/v2/resize:fill:88:88/1*GuOBdOw0etYP-byKmBgjAw.jpeg)
7
+
8
+
9
+
10
+](https://vivekhere.medium.com/?source=post_page---byline--3130684a9c37--------------------------------)
11
+
12
+[
13
+
14
+![Python in Plain English](https://miro.medium.com/v2/resize:fill:48:48/1*VA3oGfprJgj5fRsTjXp6fA@2x.png)
15
+
16
+
17
+
18
+](https://python.plainenglish.io/?source=post_page---byline--3130684a9c37--------------------------------)
19
+
20
+I was recently reading Django’s Source Code, and I came across the @wraps decorator, which led me to the functools docs, where I discovered some fantastic functools features. That discovery led to the creation of this article.
21
+
22
+This tutorial will teach you how to use some fantastic functools methods to make your life simpler.
23
+
24
+What is `functools?`
25
+--------------------
26
+
27
+functools is a Python built-in module that contains Higher Order functions that can interact with other functions. A complete functools documentation may be found [here](https://docs.python.org/3/library/functools.html).
28
+
29
+Let’s see some decorators in action.
30
+
31
+lru\_cache
32
+----------
33
+
34
+When invoking a function with the same arguments, this decorator in the functools module saves n number of function calls in cache, which saves a lot of time.
35
+Assume for the sake of demonstration that we have a very large function that takes a long time to execute. The function _a\_heavy\_operation()_ takes 3 seconds to execute in this example.
36
+
37
+```
38
+import time
39
+start = time.time()
40
+def a_heavy_operation():
41
+ time.sleep(3)
42
+ return 11 + 22
43
+print(a_heavy_operation())
44
+print(a_heavy_operation())
45
+print(time.time() - start)
46
+# Output
47
+# 33
48
+# 33
49
+# 6.024240255355835
50
+```
51
+
52
+
53
+It takes about 6 seconds to run the above code. To the above function, we’ll add lru cache.
54
+
55
+```
56
+import time
57
+from functools import lru_cache
58
+start = time.time()
59
+@lru_cache()
60
+def a_heavy_operation():
61
+ time.sleep(3)
62
+ return 11 + 22
63
+print(a_heavy_operation())
64
+print(a_heavy_operation())
65
+print(time.time() - start)
66
+# Output
67
+# 33
68
+# 33
69
+# 3.0158064365386963
70
+```
71
+
72
+
73
+Take a look at how using lru cache made our code run faster. Python saved the function’s cache and retrieved the cached value, reducing our execution time.
74
+
75
+wraps
76
+-----
77
+
78
+Wraps is used in functools to keep the function details. When we decorate a function, the function’s information is gone. We utilise the @wraps decorator on the decorator wrapper function to prevent this.
79
+
80
+Take a look at this code to see what I mean.
81
+
82
+```
83
+from functools import lru_cache
84
+
85
+def my_decorator(func):
86
+ def log(*args, **kwargs):
87
+ print("Running ")
88
+ return func(*args, *kwargs)
89
+ return log
90
+
91
+@my_decorator
92
+def add(a, b):
93
+ """my beautiful doc"""
94
+ return a + b
95
+```
96
+
97
+
98
+Run the above code in -i mode using, `python -i file.py`
99
+Let's see what we have:
100
+
101
+```
102
+>>> add(1,2)
103
+Running
104
+3
105
+>>> add(3,4)
106
+Running
107
+7
108
+>>> add.__name__
109
+log
110
+>>> add.__doc__
111
+>>>
112
+```
113
+
114
+
115
+We can see that our decorator is operating properly in the previous example, since it is consistently “Running” on each run. However, our function’s information has been lost, and it is unable to return the name or the docstring.
116
+
117
+We have @wraps to help us with this problem. Make the changes below to the code.
118
+
119
+```
120
+from functools import wraps
121
+
122
+def my_decorator(func):
123
+ @wraps(func)
124
+ def log(*args, **kwargs):
125
+ print("Running ")
126
+ return func(*args, *kwargs)
127
+ return log
128
+
129
+@my_decorator
130
+def add(a, b):
131
+ """my beautiful doc"""
132
+ return a + b
133
+```
134
+
135
+
136
+Now again run the code using `python -i file.py`
137
+
138
+```
139
+>>> add(1,2)
140
+Running
141
+3
142
+>>> add.__name__
143
+'add'
144
+>>> add.__doc__
145
+'my beautiful doc'
146
+>>>
147
+```
148
+
149
+
150
+Voila! The function information is now saved in our function.
151
+
152
+singledispatch
153
+--------------
154
+
155
+To create a generic function, singledispatch is utilised. Generic functions are those that perform the same operation on a variety of data types.
156
+
157
+Assume I want to create a function that returns the first value from an iterable of several data types.
158
+
159
+```
160
+def return_first_element(data):
161
+ if isinstance(data, list):
162
+ print(data[0])
163
+ elif isinstance(data, str):
164
+ print(data.split()[0])
165
+ elif isinstance(data, dict):
166
+ print(list(data.values())[0] )
167
+ else:
168
+ print(print(data))
169
+```
170
+
171
+
172
+Now run `python -i file.py` to run the code in interactive mode.
173
+
174
+```
175
+>>> return_first_element({"Age":20, "Height": 180})
176
+20
177
+>>> return_first_element("Hello Mr Python")
178
+Hello
179
+>>> return_first_element([12,432,563])
180
+12
181
+>>>
182
+```
183
+
184
+
185
+Our function is effective, but it isn’t clean. Using if/elif/else statements to create generic functions is not recommended in Python. So, what’s the solution? singledispatch, of course.
186
+
187
+Let’s make a few modifications to our code.
188
+
189
+```
190
+from functools import singledispatch
191
+
192
+@singledispatch
193
+def return_first_el(data):
194
+ return data
195
+
196
+@return_first_el.register(list)
197
+def _(data):
198
+ return data[0]
199
+
200
+@return_first_el.register(dict)
201
+def _(data):
202
+ return list(data.values())[0]
203
+
204
+@return_first_el.register(str)
205
+def _(data):
206
+ return data.split()[0]
207
+```
208
+
209
+
210
+To check the results, run the code again in interactive mode with _python -i file.py._
211
+
212
+```
213
+>>> return_first_el({"Age":20, "Height": 180})
214
+20
215
+>>> return_first_el("Hello Mr Python")
216
+'Hello'
217
+>>> return_first_el([124, 765, 897])
218
+124
219
+>>> return_first_el({12,31,1})
220
+{1, 12, 31}
221
+```
222
+
223
+
224
+Look how our `return_first_el` function acted as a fallback function when no data type matched for ‘set’.
225
+
226
+Look at how much cleaner our code is now; the singledispatch made it easier to add more data types, and each datatype now gets its own place where we can perform further operations on the data.
227
+
228
+total\_ordering
229
+---------------
230
+
231
+The total\_ordering decorator saves a ton of time in Object Oriented Progrmming.
232
+Consider this example, the below class declares a class `Man` with name and age property and (=) \_\_**eq\_\_** and (<) \_\_l**t\_\_** dunder methods.
233
+
234
+```
235
+class Man:
236
+ def __init__(self, name, age):
237
+ self.name = name
238
+ self.age = age
239
+ def __eq__(self, o):
240
+ return self.age == o.age
241
+ def __lt__(self, o):
242
+ return self.age < o.age
243
+```
244
+
245
+
246
+Let’s see what happens if we run the code.
247
+
248
+```
249
+>>> obj = Man("Vivek", 20)
250
+>>> obj2 = Man("Alex", 24)
251
+>>> obj = obj
252
+>>> obj == obj2
253
+False
254
+>>> obj < obj2
255
+True
256
+>>> obj >= obj2
257
+Traceback (most recent call last):
258
+ File "<stdin>", line 1, in <module>
259
+TypeError: '>=' not supported between instances of 'Man' and 'Man'
260
+```
261
+
262
+
263
+Our code worked for (==) and (<), but it didn’t work when we used an operator that wasn’t defined in the class. Given that we create at least one operator dunder method and \_\_eq\_\_ method, @total\_ordering generates the,>,=,>=, and more comparison operators for our class.
264
+
265
+Let’s add our decorator just above the class.
266
+
267
+```
268
+from functools import total_ordering
269
+
270
+@total_ordering
271
+class Man:
272
+.....
273
+.....
274
+```
275
+
276
+
277
+Now again run thee code in interactive mode to see the results
278
+
279
+```
280
+>>> o = Man("Vivek", 20)
281
+>>> b = Man("Alex", 24)
282
+>>> o == b
283
+False
284
+>>> o >= b
285
+False
286
+>>> o <= b
287
+True
288
+```
289
+
290
+
291
+Take a look at how total ordering generated our class’s comparison operators.
292
+
293
+Conclusion
294
+----------
295
+
296
+I hope you found this post useful; I strongly advise you to study the documentation in order to fully comprehend the internal mechanics of these higher level functions. If you enjoyed this, please consider following me on [Twitter](https://twitter.com/vivekthedev), where I share stuff related to Python, Web development, and open source software. I’ll see you there.
... ...
\ No newline at end of file