Skip to content

Commit fac45a5

Browse files
committed
📝 Update decorators
* Add functools.singledispatch * Add additional decorators * Python compilers * concurrency * Memory profiling
1 parent 45cd4c7 commit fac45a5

4 files changed

Lines changed: 214 additions & 65 deletions

File tree

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ Added
2424
Changed
2525
~~~~~~~
2626

27+
* 📝 Update decorators
28+
29+
* Add functools.singledispatch
30+
* Add additional decorators
31+
32+
* Python compilers
33+
* concurrency
34+
* Memory profiling
35+
2736
* 👷🔧📝 Switch to prek
2837

2938
* Remove pre-commit

docs/functions/decorators.rst

Lines changed: 201 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Decorators
22
==========
33

4-
Functions can also be passed as arguments to other functions and return the
5-
results of other functions. For example, it is possible to write a Python
6-
function that takes another function as a :term:`parameter`, embeds it in
7-
another function that does something similar, and then returns the new function.
8-
This new combination can then be used instead of the original function:
4+
Functions can also be passed as arguments to other functions, and the results of
5+
other functions can be returned. For example, it is possible to write a Python
6+
function that takes another function as a parameter, embeds it within another
7+
function that does something similar, and then returns the new function. This
8+
new combination can then be used in place of the original function:
99

1010
.. code-block:: pycon
1111
:linenos:
@@ -26,23 +26,23 @@ This new combination can then be used instead of the original function:
2626
Execute function my_func with the argument(s)
2727
('Hello', 'Pythonistas!')
2828
29-
Line 2
30-
The ``inf`` function outputs the name of the function it wraps.
31-
Line 6
32-
When finished, the ``inf`` function returns the wrapped function.
29+
Line 1
30+
The ``inf`` function prints the name of the function it wraps.
31+
Line 12
32+
When it has finished, the ``inf`` function returns the wrapped function.
3333

3434
A decorator is `syntactic sugar
3535
<https://en.wikipedia.org/wiki/Syntactic_sugar>`_ for this process and allows
36-
you to wrap one function inside another with a one-line addition. You still get
37-
exactly the same effect as with the previous code, but the resulting code is
36+
you to wrap one function around another with a single line of code. You still
37+
get exactly the same effect as with the previous code, but the resulting code is
3838
much cleaner and easier to read. Using a decorator simply consists of two parts:
3939

40-
#. the definition of the function to wrap or *decorate* other functions, and
41-
#. the use of an ``@`` followed by the decorator just before the wrapped
40+
#. defining the function that is to wrap or decorate other functions, and
41+
#. using an ``@``, followed by the decorator, immediately before the wrapped
4242
function is defined.
4343

44-
The decorator function should take a function as a :term:`parameter` and return
45-
a function, as follows:
44+
The decorator function should take a function as a parameter and return a
45+
function, as follows:
4646

4747
.. code-block:: pycon
4848
:linenos:
@@ -58,81 +58,218 @@ a function, as follows:
5858
5959
Line 1
6060
The function ``my_func`` is decorated with ``@inf``.
61-
Line 7
62-
The wrapped function is called after the decorator function is finished.
61+
Line 8
62+
The wrapped function is called once the decorator function has finished.
6363

6464
``functools``
6565
-------------
6666

67-
The Python :mod:`functools` module is intended for higher-order functions, for
68-
example functions that act on or return other functions. Mostly you can use them
69-
as decorators, such as:
67+
The Python :mod:`functools` module is designed for higher-order functions, that
68+
are functions which act on or return other functions. You can usually use them
69+
as decorators, for example:
7070

7171
:func:`functools.cache`
72-
Simple, lightweight, function cache as of Python ≥ 3.9, sometimes called
73-
*memoize*. It returns the same as :func:`functools.lru_cache` with the
74-
:term:`parameter` ``maxsize=None``, additionally creating a
75-
:doc:`/types/dicts` with the function arguments. Since old values never
76-
need to be deleted, this function is then also smaller and faster. Example:
72+
A simple, lightweight cache for functions in Python 3.9 and later, sometimes
73+
also referred to as *memoize*. It returns the same result as
74+
:func:`functools.lru_cache` with the parameter ``maxsize=None``, whilst
75+
additionally creating a :doc:`/types/dicts` containing the function
76+
arguments. As old values never need to be deleted, this function is
77+
therefore smaller and faster. An example:
7778

7879
.. code-block:: pycon
7980
:linenos:
8081
8182
>>> from timeit import timeit
82-
... from functools import cache
83-
... @cache
83+
>>> from functools import cache
84+
>>> @cache
8485
... def factorial(n):
8586
... return n * factorial(n - 1) if n else 1
86-
... timeit("factorial(8)", globals=globals())
87-
0.02631620899774134
87+
...
88+
>>> timeit("factorial(10)", number=1, globals=globals())
89+
8.74977558851242e-06
90+
>>> timeit("factorial(12)", number=1, globals=globals())
91+
4.041939973831177e-06
92+
>>> timeit("factorial(12)", number=1, globals=globals())
93+
1.8328428268432617e-06
8894
8995
Line 1
90-
imports the :mod:`timeit` module for measuring execution time.
96+
imports the :mod:`timeit` module to measure execution time.
9197
Line 2
9298
imports :func:`functools.cache`.
93-
Line 5
99+
Line 3
94100
The ``@cache`` decorator is used to store intermediate results, which
95-
can then be reused. In our case, the execution speed is increased
96-
approximately tenfold.
97-
Line 10
98-
:func:`timeit.timeit` measures the time of a call. Unless otherwise
99-
specified, the call is made one million times.
101+
can then be reused. In our case, this increases the execution speed by a
102+
factor of approximately ten.
103+
Line 7
104+
:func:`timeit.timeit` measures the time taken for a call.
105+
Line 9
106+
Only two further recursive calls need to be made, as ``factorial(10)``
107+
is already cached.
100108

101-
:func:`functools.wraps`
102-
This decorator makes the wrapper function look like the original function
103-
with its name and properties.
109+
:func:`functools.singledispatch`
110+
converts a function into a generic function. To define a generic function,
111+
it is decorated with the ``@singledispatch`` decorator:
112+
113+
.. code-block:: pycon
114+
115+
>>> from functools import singledispatch
116+
>>>
117+
>>> @singledispatch
118+
... def multiply(a, b):
119+
... raise NotImplementedError("Unsupported type")
120+
...
121+
122+
To add overloaded implementations to the function, you can use
123+
:func:`register` on the generic function as a decorator:
104124

105125
.. code-block:: pycon
106126
107-
>>> from functools import wraps
108-
>>> def my_decorator(f):
109-
... @wraps(f)
110-
... def wrapper(*args, **kwargs):
111-
... """Wrapper docstring"""
112-
... print("Call decorated function")
113-
... return f(*args, **kwargs)
114-
... return wrapper
127+
>>> @multiply.register(float)
128+
... def _(a, b):
129+
... print(a * b)
115130
...
116-
>>> @my_decorator
117-
... def example():
118-
... """Example docstring"""
119-
... print("Call example function")
131+
>>> @multiply.register(str)
132+
... def _(a, b):
133+
... print(float(a) * float(b))
120134
...
121-
>>> example.__name__
122-
'example'
123-
>>> example.__doc__
124-
'Example docstring'
135+
>>> multiply(7.0, 0.6)
136+
4.2
137+
>>> multiply("7.0", "0.6")
138+
4.2
139+
140+
For functions annotated with types, the decorator automatically infers the
141+
type of the first argument.
142+
143+
:func:`functools.wraps`
144+
This decorator ensures that the wrapped function looks exactly like the
145+
original function, with its name and attributes intact.
146+
147+
.. code-block:: pycon
125148
126-
Without ``@wraps`` decorator, the name and docstring of the wrapper method
127-
would have been returned instead:
149+
>>> from functools import wraps
150+
>>> def my_decorator(f):
151+
... @wraps(f)
152+
... def wrapper(*args, **kwargs):
153+
... """Wrapper docstring"""
154+
... print("Call decorated function")
155+
... return f(*args, **kwargs)
156+
... return wrapper
157+
...
158+
>>> @my_decorator
159+
... def example():
160+
... """Example docstring"""
161+
... print("Call example function")
162+
...
163+
>>> example.__name__
164+
'example'
165+
>>> example.__doc__
166+
'Example docstring'
167+
168+
Without the ``@wraps`` decorator, the name and docstring of the ``wrapper``
169+
method would have been returned instead:
128170

129171
.. code-block:: pycon
130172
131-
>>> example.__name__
132-
'wrapper'
133-
>>> example.__doc__
134-
'Wrapper docstring'
173+
>>> example.__name__
174+
'wrapper'
175+
>>> example.__doc__
176+
'Wrapper docstring'
177+
178+
Other typical uses for Python decorators
179+
----------------------------------------
180+
181+
Other Python compilers
182+
~~~~~~~~~~~~~~~~~~~~~~
183+
184+
Python compilers such as `Numba <https://numba.pydata.org/>`_ can be used with a
185+
decorator:
186+
187+
.. code-block:: python
188+
189+
@numba.jit(nopython=True)
190+
def dist(x, y):
191+
"""Calculate the distance"""
192+
dist = 0
193+
for i in range(len(x)):
194+
dist += (x[i] - y[i]) ** 2
195+
return dist
196+
197+
.. seealso::
198+
* :ref:`/performance/index.rst#numba`
199+
200+
Parallelisation
201+
~~~~~~~~~~~~~~~
202+
203+
The sequential execution of independent pipeline steps does not make optimal use
204+
of the processing power of processors. The `@dask.delayed
205+
<https://docs.dask.org/en/stable/delayed.html#decorator>`_ decorator creates a
206+
directed acyclic graph (DAG) to execute the tasks in parallel, which helps to
207+
reduce the overall execution time:
208+
209+
.. code-block:: pycon
210+
211+
>>> import dask
212+
>>> @dask.delayed
213+
... def inc(x):
214+
... return x + 1
215+
...
216+
>>> @dask.delayed
217+
... def double(x):
218+
... return x * 2
219+
...
220+
>>> @dask.delayed
221+
... def add(x, y):
222+
... return x + y
223+
...
224+
>>> data = range(1, 6)
225+
>>> output = []
226+
>>> for x in data:
227+
... a = inc(x)
228+
... b = double(x)
229+
... c = add(a, b)
230+
... output.append(c)
231+
...
232+
>>> total = dask.delayed(sum)(output)
233+
>>> total.compute()
234+
50
235+
>>> total.visualize()
236+
<IPython.core.display.Image object>
237+
238+
.. figure:: mydask.png
239+
240+
Memory profiling
241+
~~~~~~~~~~~~~~~~
242+
243+
The ``@memory_profiler.profile`` decorator is used to measure memory usage. It
244+
monitors the enclosed function step by step, tracking RAM usage or the amount of
245+
memory released at each individual step:
246+
247+
.. code-block:: python
248+
:linenos:
249+
250+
from memory_profiler import profile
251+
252+
253+
@profile
254+
def my_func():
255+
a = [1] * (10**6)
256+
b = [2] * (2 * 10**7)
257+
del b
258+
return a
259+
260+
The output might look like this:
261+
262+
.. code-block:: console
263+
264+
Line # Mem usage Increment Line Contents
265+
================================================
266+
4 67.3 MiB 67.3 MiB @profile
267+
5 def my_func():
268+
6 74.8 MiB 7.5 MiB a = [1] * (10 ** 6)
269+
7 227.4 MiB 152.6 MiB b = [2] * (2 * 10 ** 7)
270+
8 74.9 MiB 0.0 MiB del b
271+
9 74.9 MiB 0.0 MiB return a
135272
136-
.. tip::
137-
`cusy seminar: Advanced Python
138-
<https://cusy.io/en/our-training-courses/advanced-python.html>`_
273+
.. seealso::
274+
* `memory-profiler
275+
<https://www.python4data.science/de/latest/performance/ipython-profiler.html#Speicherprofil-erstellen:-%memit-und-%mprun>`_

docs/functions/mydask.png

57.4 KB
Loading

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ classifiers = [
1818
"Programming Language :: Python :: 3.13",
1919
"Programming Language :: Python :: 3.14",
2020
]
21-
dependencies = []
21+
dependencies = [
22+
"dask",
23+
"graphviz",
24+
]
2225
urls."Bug Tracker" = "https://github.com/veit/python-basics-tutorial/issues"
2326
urls.Homepage = "https://github.com/veit/python-basics-tutorial/"
2427

0 commit comments

Comments
 (0)