Skip to content

Commit ea96113

Browse files
committed
Decorators.
1 parent 841bb61 commit ea96113

5 files changed

+243
-12
lines changed

‎doc/source/10_trees_and_directed_acyclic_graphs.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Trees and directed acyclic graphs
44
=================================
55

66
The :term:`abstract data types <abstract data type>` that we met in
7-
:numref:`Chapter %s <abstract_data_types>` were all fairly simple sequences of objects that
7+
:numref:`Week %s <abstract_data_types>` were all fairly simple sequences of objects that
88
were extensible in different ways. If that were all the sum total of abstract
99
data types then the reader might reasonably wonder what all the fuss is about.
1010
In this chapter we'll look at :term:`trees <tree>` and :term:`directed acyclic
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,261 @@
11
Further object-oriented features
22
================================
33

4-
.. _decorators:
4+
This week we'll tie up a few loose ends by examining in detail some programming
5+
concepts and Python features which we have encountered but not really studied
6+
in the course so far.
57

68
Decorators
7-
~~~~~~~~~~
9+
----------
810

9-
.. _abstract_classes:
11+
In :numref:`Week %s <trees>` we encountered the
12+
:func:`functools.singledispatch` decorator, which turns a function into a
13+
:term:`single dispatch function`. More generally, a decorator is a function
14+
which takes in a function and returns another function. In other words, the
15+
following:
1016

11-
Abstract classes
12-
~~~~~~~~~~~~~~~~
17+
.. code-block:: python3
1318
19+
@dec
20+
def func(...):
21+
...
22+
23+
is equivalent to:
24+
25+
.. code-block:: python3
26+
27+
def func(...):
28+
...
29+
func = dec(func)
30+
31+
Decorators are therefore merely :term:`syntactic sugar`, but can be very useful
32+
in removing the need for boiler-plate code at the top of functions. For
33+
example, your code for :numref:`Exercise %s <ex_expr>` probably contains a lot
34+
of repeated code a lot like the following:
35+
36+
.. code-block:: python3
37+
38+
def __add__(self, other):
39+
"""Return the Expr for the sum of this Expr and another."""
40+
if isinstance(other, numbers.Number):
41+
other = Number(other)
42+
return Add(self, other)
43+
44+
We could define a decorator to clean up this code as follows:
45+
46+
.. _eg_decorator:
47+
48+
.. code-block:: python3
49+
:caption: A :term:`decorator` which casts the second argument of a method
50+
to an `expressions.Number` if that argument is a number.
51+
:linenos:
52+
53+
from functools import wraps
54+
55+
def make_other_expr(meth):
56+
"""Cast the second argument of a method to Number when needed."""
57+
@wraps(meth)
58+
def fn(self, other):
59+
if isinstance(other, numbers.Number):
60+
other = Number(other)
61+
return meth(self, other)
62+
return fn
63+
64+
Now, each time we write one of the special methods of :class:`Expr`, we can
65+
instead write something like the following:
66+
67+
.. code-block:: python3
68+
69+
@make_other_expr
70+
def __add__(self, other):
71+
"""Return the Expr for the sum of this Expr and another."""
72+
return Add(self, other)
73+
74+
Let's look closely at what the decorator in :numref:`eg_decorator` does. The
75+
decorator takes in one function, :func:`meth` an returns another one
76+
:func:`fn`. Notice that we let :func:`fn` take the same arguments as
77+
:func:`meth`. If you wanted to write a more generic decorator that worked on
78+
functions with different signatures, then you could define function as
79+
`fn(*args, **kwargs)` and pass these through to :func:`meth`.
80+
81+
The contents of :func:`fn` are what will be executed every time :func:`meth` is
82+
called. So here we check the type of :data:`other` and cast it to
83+
:class:`Number`, and then call the original :func:`meth` on the modified arguments.
84+
We could also execute code that acts on the value that :func:`meth` returns. To
85+
do this we would assign the result of :func:`meth` to a variable and then
86+
include more code after line 9.
87+
88+
Finally, notice that we have wrapped `fn` in another decorator, this time
89+
:func:`functools.wraps`. The purpose of this decorator is to copy the name and
90+
docstring from :func:`meth` to :func:`fn`. The effect of this is that if the
91+
user calls :func:`help` on a decorated function then they will see the name and
92+
docstring for the original function, and not that of the decorator.
93+
94+
Decorators which take arguments
95+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
96+
97+
Our :func:`make_at_expr` decorator doesn't have brackets after its name, and doesn't
98+
take any arguments. However :func:`functools.wrap` does have brackets, and takes a
99+
function name as an argument. How does this work? The answer is yet another
100+
wrapper function. A decorator is a function which takes a function and
101+
returns a function. :func:`functools.wrap` takes an argument (it happens to be
102+
a function but other decorators take other types) and returns a decorator
103+
function. That is, it is a function which takes in arguments and returns a
104+
function which takes a function and returns a function. It's functions all the
105+
way down!
106+
107+
The property decorator
108+
~~~~~~~~~~~~~~~~~~~~~~
109+
110+
Back in :numref:`Week %s <objects>`, we gave the
111+
:class:`~example_code.polynomial.Polynomial` class a
112+
:meth:`~example_code.polynomial.Polynomial.degree()` method:
113+
114+
.. code-block:: python3
115+
116+
def degree(self):
117+
return len(self.coefficients) - 1
118+
119+
120+
This enables the following code to work:
121+
122+
.. code-block:: ipython3
123+
124+
In [1]: from example_code.polynomial import Polynomial
125+
126+
In [2]: p = Polynomial((1, 2, 4))
127+
128+
In [3]: p.degree()
129+
Out[3]: 2
130+
131+
However, the empty brackets at the end of :func:`degree` are a bit clunky: why
132+
should we have to provide empty brackets if there are no arguments to pass?
133+
Indeed, this represents a failure of :term:`encapsulation`, because we
134+
shouldn't know or care from the outside whether
135+
meth:`~example_code.polynomial.Polynomial.degree()` is a :term:`method` or a
136+
:term:`data attribute`. Indeed, the developer of the
137+
:mod:`~example_code.polynomial` module should be able to change that
138+
implementation without changing the interface. This is where the
139+
built-in :class:`property` decorator comes in. :class:`property` transforms
140+
methods that take no arguments other than the object itself into attributes.
141+
So, if we had instead defined:
142+
143+
.. code-block:: python3
144+
145+
@property
146+
def degree(self):
147+
return len(self.coefficients) - 1
148+
149+
Then `degree` would be accessible as an :term:`attribute`:
150+
151+
.. code-block:: ipython3
152+
153+
In [1]: from example_code.polynomial import Polynomial
154+
155+
In [2]: p = Polynomial((1, 2, 4))
156+
157+
In [3]: p.degree
158+
Out[3]: 2
159+
160+
161+
The :mod:`functools` module
162+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
163+
164+
The :mod:`functools` module is part of the :ref:`Python Standard Library
165+
<library-index>`. It provides a collection of core :term:`higher order
166+
functions <higher order function>`, some of which we have already met earlier
167+
in the course. Since decorators are an important class of higher order
168+
function, it is unsurprising that :mod:`functools` provides several very useful
169+
ones. We will survey just a few here:
170+
171+
`functools.cache`
172+
Some functions can be very expensive to compute, and may be called
173+
repeatedly. A cache stores the results of previous function calls. If the
174+
function is called again with a combination of argument values that have
175+
previously been used, the function result is returned from the cache
176+
instead of the function being called again. This is a trade-off of
177+
execution time against memory usage, so one has to be careful how much
178+
memory will be consumed by the cache.
179+
`functools.lru_cache`
180+
A least recently used cache is a limited size cache where the least
181+
recently accessed items will be discarded if the cache is full. This has
182+
the advantage that the memory usage is bounded, but the drawback that cache
183+
eviction may take time, and that more recomputation may occur than in an
184+
unbounded cache.
185+
`functools.singledispatch`
186+
We met this in :numref:`Week %s <trees>`. This decorator transforms a
187+
function into a :term:`single dispatch function`.
188+
14189
.. _abstract_base_classes:
15190

16191
Abstract base classes
192+
---------------------
193+
194+
We have now on several occasions encountered classes which are not designed to
195+
be instantiated themselves, but merely serve as parent classes to concrete
196+
classes which are intended to be instantiated. Examples of these classes
197+
include :class:`numbers.Number`, :class:`example_code.groups.Group`, and the
198+
:class:`Expr`, :class:`Operator`, and :class:`Terminal` classes from
199+
:numref:`Week %s <trees>`. These classes that are only ever parents are called
200+
:term:`abstract base classes <abstract base class>`. They are abstract in the
201+
sense that they define (some of the) properties of their children, but without
202+
providing full implementations of them. They are base classes in the sense that
203+
they are intended to be inherited from.
204+
205+
Abstract base classes typically fulfil two related roles, they provide
206+
the definition of an interface that child classes can be assumed to follow, and
207+
they provide a useful way of checking that an object of a concrete class has
208+
particular properties.
209+
210+
The :mod:`abc` module
17211
~~~~~~~~~~~~~~~~~~~~~
18212

213+
The concept of an abstract base class is itself an abstraction: an
214+
abstract base class is simply a class which is designed not to be instantiated.
215+
This requires no support from particular language features. Nonetheless, there
216+
are features that a language can provide which makes the creation of useful
217+
abstract base classes easy. In Python, these features are provided by the
218+
:mod:`abc` module in the :ref:`Standard Library <library-index>`.
219+
220+
221+
Duck typing
222+
~~~~~~~~~~~
19223

20224
Glossary
21225
--------
22226

23227
.. glossary::
24228
:sorted:
25229

26-
abstract class
230+
abstract base class
27231
A class designed only to be the :term:`parent <parent class>` of other
28232
classes, and never to be instantiated itself. Abstract classes often
29233
define the interfaces of :term:`methods <method>` but leave their implementations
30-
to the concrete :term:`child classes <child class>`.
234+
to the concrete :term:`child classes <child class>`.
235+
236+
decorator
237+
A syntax for applying :term:`higher order functions <higher order
238+
function>` when defining functions. A decorator is applied by writing
239+
`@` followed by the decorator name immediately before the declaration
240+
of the function or :term:`method` to which the decorator applies.
241+
242+
duck typing
243+
The idea that the precise :term:`type` of an :term:`object` is not important, it is
244+
only important that the object has the correct :term:`methods <method>`
245+
or :term:`attributes <attribute>` for the current operation. If an
246+
object walks like a duck, and quacks like a duck then it can be taken
247+
to be a duck.
248+
249+
higher order function
250+
A function which acts on other functions, and which possibly returns
251+
another function as its result.
252+
253+
syntactic sugar
254+
A feature of the programming language which adds no new functionality,
255+
but which enables a clearer or more concise syntax. Python
256+
:term:`special methods <special method>` are a form of syntactic sugar as they enable,
257+
for example, the syntax `a + b` instead of something like `a.add(b)`.
258+
259+
260+
Exam preparation
261+
----------------

‎doc/source/8_inheritance.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,10 @@ were to instantiate :class:`Group` itself:
561561
65 def __repr__(self):
562562
563563
In fact, :class:`Group` is never supposed to be instantiated, it plays the role
564-
of an :term:`abstract class`. In other words, it's role is to provide
564+
of an :term:`abstract base class`. In other words, it's role is to provide
565565
functionality to classes that inherit from it, rather than to be the type of
566566
objects itself. We will return to this in more detail in
567-
:numref:`abstract_classes`.
567+
:numref:`abstract_base_classes`.
568568

569569
However, if we instead instantiate :class:`~example_code.groups.CyclicGroup`
570570
then everything works:

‎doc/source/9_debugging.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ set up the bisection we run:
631631

632632
.. code-block:: console
633633
634-
$ git bisect start 66a10d5d374de796827ac3152f0c507a46b73d60 HEAD --
634+
$ git bisect start HEAD 66a10d5d374de796827ac3152f0c507a46b73d60 --
635635
636636
Obviously you replace the commit ID with your starting point. ``HEAD`` is a git
637637
shorthand for the current state of the repository, so it's a suitable end point

‎doc/webgit

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 581fce18c880a3fb8eda49f8187363e32ace268a
1+
Subproject commit 980ab0a97fffdaab91555208aa27ee249e79c715

0 commit comments

Comments
 (0)