|
1 | 1 | Further object-oriented features
|
2 | 2 | ================================
|
3 | 3 |
|
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. |
5 | 7 |
|
6 | 8 | Decorators
|
7 |
| -~~~~~~~~~~ |
| 9 | +---------- |
8 | 10 |
|
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: |
10 | 16 |
|
11 |
| -Abstract classes |
12 |
| -~~~~~~~~~~~~~~~~ |
| 17 | +.. code-block:: python3 |
13 | 18 |
|
| 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 | + |
14 | 189 | .. _abstract_base_classes:
|
15 | 190 |
|
16 | 191 | 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 |
17 | 211 | ~~~~~~~~~~~~~~~~~~~~~
|
18 | 212 |
|
| 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 | +~~~~~~~~~~~ |
19 | 223 |
|
20 | 224 | Glossary
|
21 | 225 | --------
|
22 | 226 |
|
23 | 227 | .. glossary::
|
24 | 228 | :sorted:
|
25 | 229 |
|
26 |
| - abstract class |
| 230 | + abstract base class |
27 | 231 | A class designed only to be the :term:`parent <parent class>` of other
|
28 | 232 | classes, and never to be instantiated itself. Abstract classes often
|
29 | 233 | 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 | +---------------- |
0 commit comments