Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Lecture 7 : Classes and algebraic structures
References:
Summary:
In this lecture, we discuss how to define new classes in Python (and SageMath). We learn how to give them useful methods and how to inherit from other classes. We discuss how to implement arithmetic operations in Python and, if time permits, how to use the parent/element framework in SageMath to implement more general algebraic structures there.
A brief overview of classes
Without discussing it in much detail, we have already used many examples of classes defined in SageMath. Take the following example:
We see that there exists a class (namely Rational
, defined in sage.rings.rational
) which implements rational numbers. The concrete object a
that we created is an instance of this class.
One of the things that we can do now is use methods of the class above, for the specific instance a
that we created:
The code for this particular method forms part of the class Rational
, and we can have a look at the corresponding code:
Now we want to learn how to create such nice classes for ourselves!
Creating new classes
Our running example will be a toy implementation of the complex numbers. Here is a first basic version:
That's certainly very basic! Still, we see some of the important structure:
We begin with the keyword
class
followed by the name that we choose and a:
. In the following indented block, we can define methods of our new class.The most fundamental method is always called
__init__
, which is used when creating new instances of our class (i.e. when we create a new object representing a complex number). Its first argument is alwaysself
, which will be this new instance, followed by some more arguments. In the case above, these are the inputsRealPart, ImaginaryPart
of the real and imaginary part of our complex number. What our concrete method above does is simply to store these values insideself
as attributesself.Re, self.Im
.
Let's see this class in "action". First, we create a new instance, representing the number , and call it z
:
Our class cannot do anything interesting yet, but we can access the internal attributes Re, Im
:
These more or less work like any variables that we used before, e.g. we can also change their values:
Most of the rest of the lecture, we will discuss how to make this class more interesting and usable.
Writing methods
The main feature of classes is that we can give them methods, i.e. functions defined inside the class, which we can call like a.factor()
above. Here is a first small addition to our toy class:
As you see, the first argument of any method is always called self
and contains the object calling the method.
Exercise
Create the complex number and compute its norm.
Solution (uncomment to see)
Exercise
Add a method complex_conjugate
to the class above, which returns the complex conjugate of the number.
Solution (uncomment to see)
When we try to test the new code, we encounter a problem:
In order to get a useful, readable output, we need to tell the class how an instance should be represented when trying to print it. To do this, we can define a method __repr__
. It is supposed to return a string which represents the value of the object. Then, when writing print(a)
(or just a
), it secretly calls print(a.__repr__())
.
This looks reasonable on first glance, but testing a few more examples, we quickly see some issues:
One of the assignments is to write a better version of the __repr__
function, but for now the version above is good enough.
Exercise
Write a function arg
which computes the argument of the complex number, i.e. its angle with the positive real axis measured in the interval . If you are done early: add the optional argument max_arg
so that the argument is instead specified in the interval from max_arg - 2 pi
to max_arg
.
Solution (uncomment to see)
Before moving on, we'll mention one more trick associated to methods of classes, called monkey patching. The idea is that instead of declaring a method at the definition of a class, you can also attach it later, as follows:
This should be done with some care, but in some cases (e.g. in exercises below) it can help to not always repeat all the code we already had before.
Inheritance of classes
Sometimes we don't want to create a new class from scratch, but instead just add some feature to an existing class. In this case, we can let the new class inherit from the old one. This means that, by default, the methods and attributes of the old class are transferred to the new one, but one can add additional features (or overwrite methods from the old class).
As an example, let's create a variant of the class MyCompNumb
above, which in addition to the real and imaginary part remembers explicitly one choice of argument. Here is a way to do this:
What we see here:
The class from which we inherit (the parent class) is given in parentheses behind the name of the new class (the child class). We could also specify multiple parent classes (e.g. it is very often advisable to inherit from
SageObject
to get some basic methods like the option to save into a file). Apart from taking over the methods from the parents, the instances of the new class also count as instances of the old class (every complex number with argument is still an example of a complex number):
We inherited the methods from the old function, which is why the following works:
On the other hand, we have overwritten the functions
__init__
and__repr__
to adapt to the additional argumentPhi
. Note that by callingMyCompNumb.__init__
we make sure that all the attributes set inMyCompNumb
will also be available inMyCompNumbWithArg
.
Additionally, we have modified the function
complex_conjugate
. Otherwise, it would return instances ofMyCompNumb
and notMyCompNumbWithArg
.
Here are a few things we can now do with our class:
Exercise
Modify the
__init__
method ofMyCompNumbWithArg
to include a check that the given argumentPhi
actually makes sense (e.g. something likeMyCompNumbWithArg(1,0,pi)
should not be allowed). Raise aValueError
if it does not make sense.Modify the method
arg
to give back the chosen argument (instead of the one in ).Write a method
log
which makes sense for complex numbers with an argument.
Mathematical hint: What is for ?
Philosophical hint: Is naturally a complex number, or a complex number with an argument?
Python hint: Given a real numberr
you can compute its logarithm withr.log()
.
Solution (uncomment to see)
Arithmetic operations
One thing we of course want to do with complex numbers is to add, subtract, multiply and divide them. This can again be done using special methods, called __add__
, __sub__
, __mul__
. Unfortunately, these must be present in the original declaration of the class (and cannot be monkey patched).
Exercise
Write the functions __sub__
, __mul__
and __truediv__
for the multiplication, subtraction and division of complex numbers and test them in some examples.
If you are done early, you can also do __pow__
for the "raise to power" function (say, for integer exponents).
Solution (uncomment to see)
See here for more on the Python perspective on arithmetic operations.
Playing around - a class for rectangles
Let's take a bit of time and practise all of the things above in a new example.
Exercise
Write a class Rectangle
which implements rectangles in the plane with sides parallel to the and -axis. Think about some interesting functions that you could give this class, and implement them. Here is a bit of inspiration of what this class could do:
Solution (uncomment to see)
Implementing algebraic structures in SageMath
Above we learned how to do basic arithmetic with our handmade complex numbers. However, many natural operations that we would hope to have will still not work. Here are a few examples:
In each case, the problem could be fixed in principle:
One could extend the methods
__add__
etc. to first check ifother
is maybe anInteger
/Rational
etc and have special methods for each of these (and maybe have also methods__radd__
,__rmul__
etc).Similarly, one can implement the method
__eq__
for checking equality.One can by hand write a method
is_zero
.
However, as you can imagine, this would be a lot of work and the resulting code could easily break if the user applies it in a situation you did not foresee.
A better way to do this is to use the coercion and category framework in SageMath. See also the primer on categories, parents and elements in SageMath. A rough overview, explained with the example of the actual complex numbers CC
in SageMath:
Instead of only having a class for complex numbers, one would have two classes: a parent
ComplexField_class
and an element classComplexNumber
.New elements are created via an instance of the parent class:
These elements then remember their parents:
For the parent class, you need to program some method which converts reasonable other data types (
int, float, Rational, ...
) into elements of that parent class:
Then, if you told your child class how to add, multiply, etc its elements (via functions
_add_, _mul_, ...
) and it encounters something of the formint + ComplexNumber
, it will silently convert (coerce) the other class into a complex number, and then do the arithmetic operation:
This also works for things like the comparison that we saw above:
Even better, since many classes have a functorial definition in SageMath, it is in some cases possible to have arithmetic operations between elements of parents
A,B
such that neither can be converted into the other. The idea is to find a common parentC
, convert both elements into this, and then perform the arithmetic there. Take the following example:
Your parent and child class should inherit from some general classes (in our case
Field
andFieldElement
). This gives them many useful methods, which you don't have to worry about at all. Look at the code of the following functions:
Then, if you have implemented a function __eq__
for checking equality and call z.is_zero()
, it will just check whether
Moreover, via the category framework we have that SageMath knows about properties of its parent (and element) classes, and how they behave under basic operations. For instance, since
CC
is a field, we know thatCC[x,y]
is an integral domain:
To summarize: by working with parent/child classes and the coercion and category framework it is possible to save yourself a lot of work when implementing a new algebraic structure, and many constructions work almost by magic.
However, this is also a relatively special problem, and explaining all the details goes beyond the scope of this lecture. Still, if you plan to implement a particular type of group/module/ring/etc as your final project, it might be worth to look into this further!
Assignments
Exercise
For the class MyCompNumb
above, write a good method __repr__
which also works well for negative/vanishing real and imaginary parts. Test whether this method works for all combinations of .
Solution (uncomment to see)
Exercise
Write a method is_in_quadrant
which takes a number quad
from to and says whether the complex number is in the corresponding quadrant of the plane of complex numbers (recall that the numbers with positive real and imaginary value are the first quadrant, and then it goes in counter-clockwise order).
Solution (uncomment to see)