Skip to content

Commit 36de2ab

Browse files
committed
Finished non-inherited group implementation.
1 parent 0d87ebf commit 36de2ab

File tree

4 files changed

+204
-17
lines changed

4 files changed

+204
-17
lines changed

‎doc/source/3_style.rst

+40-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. _style.rst
1+
.. _style.rst:
22

33
A matter of style
44
=================
@@ -337,9 +337,9 @@ second is to use abstractions such as classes and function interfaces
337337
to split the problem up into small pieces so that each individual
338338
function or method is small enough for a reader to understand.
339339

340-
.. note::
341-
342-
Put in an example here of some horrific code that can be radically simplified.
340+
As a (somewhat contrived) example, assume that you need to create a list of all
341+
the positive integers less than 9999 which are divisible by all the numbers up
342+
to seven. You could write this in 5 difficult to understand lines:
343343

344344
.. container:: badcode
345345

@@ -348,16 +348,19 @@ function or method is small enough for a reader to understand.
348348
result = []
349349
350350
for _ in range(1, 9999):
351-
if _ % 1 == 0 and _ % 2 == 0 and _ % 3 == 0 and _ % 4 == 0 and _ % 5 == 0 and _ % 6 == 0 and _ % 7 == 0:
352-
result.append(_)
353-
print(result)
351+
if _ % 1 == 0 and _ % 2 == 0 and _ % 3 == 0 and _ % 4 == 0 \
352+
and _ % 5 == 0 and _ % 6 == 0 and _ % 7 == 0:
353+
result.append(_)
354+
355+
356+
Much better would be to write a single more abstract but simpler line:
354357

355358
.. container:: goodcode
356359

357360
.. code-block:: python3
358361
359362
result = [num for num in range(1, 9999) if all(num % x == 0 for x in range(1, 8))]
360-
print(result)
363+
361364
362365
Use comprehensions
363366
..................
@@ -515,6 +518,35 @@ instead of:
515518
if len(mysequence) > 0:
516519
# Some code using mysequence
517520
521+
.. _repetition:
522+
523+
Avoid repetitition
524+
..................
525+
526+
Programmers very frequently need to do *nearly* the same thing over and over.
527+
One obvious way to do this is to write code for the first case, then copy and
528+
paste the code for subsequent cases, making changes as required. There are a
529+
number of significant problems with this approach. First, it multiplies the
530+
amount of code that a reader has to understand, and does so in a particularly
531+
pernicious way. A reader will effectively have to play "spot the difference"
532+
between the different code versions, and hope they don't miss something. Second,
533+
it makes it incredibly easy for to get confused about which version of the code
534+
a programmer is supposed to be working on. There are few things more frustrating
535+
than attempting to fix a bug and repeatedly seeing that nothing changes, only to
536+
discover hours (or days) later that you have been working on the wrong piece of
537+
nearly-identical code. Finally, lets suppose that a bug is fixed - what happens
538+
to the near-identical clones of that code? The chance is very high that the bug
539+
stays unfixed in those versions thereby creating yet another spot the difference
540+
puzzle for the next person encountering a bug.
541+
542+
Abstractions are essentially tools for removing harmful repetition. For example,
543+
it may be possible to bundle up the repeated code in a function or class, and to
544+
encode the differences between versions in the :term:`parameters <parameter>` to
545+
the function or class constructor. If the differences between the versions of
546+
the code require different code, as opposed to different values of some
547+
quantities, then it may be possible to use :term:`inheritance` to avoid
548+
repetition. We will return to this in :numref:`Chapter %s<inheritance>`.
549+
518550

519551
Comments
520552
--------

‎doc/source/6_inheritance.rst

+131-4
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ representing groups, and objects representing elements. We'll lay out one
9393
possible configuration, which helpfully involves both inheritance and
9494
composition, as well as parametrisation of objects and delegation of methods.
9595

96-
Basic design considerations
97-
~~~~~~~~~~~~~~~~~~~~~~~~~~~
96+
Cyclic groups
97+
~~~~~~~~~~~~~
9898

9999
Let's start with the cyclic groups of order :math:`n`. These are isomorphic to
100100
the integers modulo :math:`n`, a property which we can use to create our
@@ -186,7 +186,134 @@ minimal characterisation of a group will suffice.
186186
187187
:numref:`cyclic_group` shows an implementation of our minimal conception of
188188
cyclic groups. Before considering it in any detail let's try it out to observe
189-
the concrete effects of the classes.
189+
the concrete effects of the classes:
190+
191+
.. code-block:: ipython3
192+
193+
In [1]: from example_code.groups_basic import CyclicGroup
194+
195+
In [2]: C = CyclicGroup(5)
196+
197+
In [3]: print(C(3) * C(4))
198+
2_C5
199+
200+
We observe that we are able to create the cyclic group of order 5. Due to the
201+
definition of the :meth:`__call__` :term:`special method` at line 35, we are
202+
then able to create elements of the group by calling the group object. The group
203+
operation then has the expected effect:
204+
205+
.. math::
206+
207+
3_{C_5} \cdot 4_{C_5} &\equiv (3 + 4) \operatorname{mod} 5\\
208+
&= 2\\
209+
&\equiv 2_{C_5}
210+
211+
Finally, if we attempt to make a group element with a value which is not an
212+
integer between 0 and 5, an exception is raised.
213+
214+
.. code-block:: ipython3
215+
216+
In [4]: C(1.5)
217+
---------------------------------------------------------------------------
218+
ValueError Traceback (most recent call last)
219+
<ipython-input-4-a5d8472d4486> in <module>
220+
----> 1 C(1.5)
221+
222+
~/docs/principles_of_programming/object-oriented-programming/example_code/groups_basic.py in __call__(self, value)
223+
38 def __call__(self, value):
224+
39 '''Provide a convenient way to create elements of this group.'''
225+
---> 40 return Element(self, value)
226+
41
227+
42 def __str__(self):
228+
229+
~/docs/principles_of_programming/object-oriented-programming/example_code/groups_basic.py in __init__(self, group, value)
230+
4 class Element:
231+
5 def __init__(self, group, value):
232+
----> 6 group._validate(value)
233+
7 self.group = group
234+
8 self.value = value
235+
236+
~/docs/principles_of_programming/object-oriented-programming/example_code/groups_basic.py in _validate(self, value)
237+
30 '''Ensure that value is a legitimate element value in this group.'''
238+
31 if not (isinstance(value, Integral) and 0 <= value < self.order):
239+
---> 32 raise ValueError("Element value must be an integer"
240+
33 f" in the range [0, {self.order})")
241+
34
242+
243+
ValueError: Element value must be an integer in the range [0, 5)
244+
245+
We've seen :term:`composition` here: on line 4
246+
:class:`~example_code.groups_basic.Element`, is associated with a group object.
247+
This is a classic *has a* relationship: an element has a group. We might have
248+
attempted to construct this the other way around with classes having elements,
249+
however this would have immediately hit the issue that elements have exactly one
250+
group, while a group might have an unlimited number of elements. Object
251+
composition is typically most successful when the relationship is uniquely
252+
defined.
253+
254+
General linear groups
255+
~~~~~~~~~~~~~~~~~~~~~
256+
257+
We still haven't encountered inheritance, though. Where does that come into the
258+
story? Well first we'll need to introduce at least one more family of groups.
259+
For no other reason than convenience, let's choose :math:`G_n`, the general
260+
linear group of degree :math:`n`. The elements of this group can be
261+
represented as :math:`n\times n` invertible square matrices. At least to the
262+
extent that real numbers can be represented on a computer, we can implement this
263+
group as follows:
264+
265+
.. code-block:: python3
266+
:caption: A basic implementation of the general linear group of a given
267+
degree.
268+
:name: general_linear_group
269+
:linenos:
270+
271+
class GeneralLinearGroup:
272+
'''The general linear group represented by degree x degree matrices.'''
273+
def __init__(self, degree):
274+
self.degree = degree
275+
276+
def _validate(self, value):
277+
'''Ensure that value is a legitimate element value in this group.'''
278+
value = np.asarray(value)
279+
if not (value.shape == (self.degree, self.degree)):
280+
raise ValueError("Element value must be a "
281+
f"{self.degree} x {self.degree}"
282+
"square array.")
283+
284+
def operation(self, a, b):
285+
return a @ b
286+
287+
def __call__(self, value):
288+
'''Provide a convenient way to create elements of this group.'''
289+
return Element(self, value)
290+
291+
def __str__(self):
292+
return f"G{self.degree}"
293+
294+
def __repr__(self):
295+
return f"{self.__class__.__name__}({repr(self.degree)})"
296+
297+
We won't illustrate the operation of this class, though the reader is welcome to
298+
:keyword:`import` the :mod:`example_code.groups_basic` module and experiment.
299+
Instead, we simply note that this code is very, very similar to the
300+
implementation of :class:`~example_code.groups_basic.CyclicGroup` in
301+
:numref:`cyclic_group`. The only functionally important differences are the
302+
definitions of the :meth:`_validate` and :meth:`operation` methods.
303+
`self.order` is also renamed as `self.degree`, and `C` is replaced by `G` in the
304+
string representation. It remains the case that there is a large amount of
305+
code repetition between classes. For the reasons we touched on in
306+
:numref:`repetition`, this is a highly undesirable state of affairs.
307+
308+
Inheritance
309+
-----------
310+
311+
Suppose, instead of copying much of the same code, we had a prototype
312+
:class:`Group` class, and :class:`CyclicGroup` and :class:`GeneralLinearGroup`
313+
simply specified the ways in which they differ from the prototype. This would
314+
avoid the issues associated with repeating code, and would make it obvious how
315+
the different group implementations differ. This is exactly what inheritance
316+
does.
190317

191318
Glossary
192319
--------
@@ -215,7 +342,7 @@ Glossary
215342
classes.
216343

217344
parent class
218-
A class from which another class, referred to as a :term:`child class`
345+
A class from which another class, referred to as a :term:`child class`,
219346
inherits.
220347

221348
subclass

‎doc/source/example_code.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ example\_code.euclid module
1212
:undoc-members:
1313
:show-inheritance:
1414

15-
example\_code.groups module
16-
---------------------------
15+
example\_code.groups\_basic module
16+
----------------------------------
1717

18-
.. automodule:: example_code.groups
18+
.. automodule:: example_code.groups_basic
1919
:members:
2020
:undoc-members:
2121
:show-inheritance:

‎example_code/groups.py renamed to ‎example_code/groups_basic.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from numbers import Integral
2+
import numpy as np
23

34

45
class Element:
@@ -28,9 +29,9 @@ def __init__(self, order):
2829

2930
def _validate(self, value):
3031
'''Ensure that value is a legitimate element value in this group.'''
31-
if not (isinstance(value, Integral) and 0 <= value < self.size):
32+
if not (isinstance(value, Integral) and 0 <= value < self.order):
3233
raise ValueError("Element value must be an integer"
33-
f" in the range [0, {self.size})")
34+
f" in the range [0, {self.order})")
3435

3536
def operation(self, a, b):
3637
return (a + b) % self.order
@@ -44,3 +45,30 @@ def __str__(self):
4445

4546
def __repr__(self):
4647
return f"{self.__class__.__name__}({repr(self.order)})"
48+
49+
50+
class GeneralLinearGroup:
51+
'''The general linear group represented by degree x degree matrices.'''
52+
def __init__(self, degree):
53+
self.degree = degree
54+
55+
def _validate(self, value):
56+
'''Ensure that value is a legitimate element value in this group.'''
57+
value = np.asarray(value)
58+
if not (value.shape == (self.degree, self.degree)):
59+
raise ValueError("Element value must be a "
60+
f"{self.degree} x {self.degree}"
61+
"square array.")
62+
63+
def operation(self, a, b):
64+
return a @ b
65+
66+
def __call__(self, value):
67+
'''Provide a convenient way to create elements of this group.'''
68+
return Element(self, value)
69+
70+
def __str__(self):
71+
return f"G{self.degree}"
72+
73+
def __repr__(self):
74+
return f"{self.__class__.__name__}({repr(self.degree)})"

0 commit comments

Comments
 (0)