**Views:**

^{28}

**Visibility:**Unlisted (only visible to those who know the link)

**Image:**ubuntu2004

**Kernel:**SageMath 9.1

# 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:

a = 6/5

type(a)

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:

a.factor()

The code for this particular method forms part of the class `Rational`

, and we can have a look at the corresponding code:

a.factor??

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:

class MyCompNumb: def __init__(self, RealPart, ImaginaryPart): self.Re = RealPart self.Im = ImaginaryPart

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 always`self`

, which will be this new instance, followed by some more arguments. In the case above, these are the inputs`RealPart, ImaginaryPart`

of the real and imaginary part of our complex number. What our concrete method above does is simply to store these values inside`self`

as**attributes**`self.Re, self.Im`

.

Let's see this class in "action". First, we create a new instance, representing the number $1+2\cdot i$, and call it `z`

:

z = MyCompNumb(1, 2) type(z)

Our class cannot do anything interesting yet, but we *can* access the internal attributes `Re, Im`

:

z.Re

These more or less work like any variables that we used before, e.g. we can also change their values:

z.Re = 3 z.Re

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:

class MyCompNumb: def __init__(self, RealPart, ImaginaryPart): self.Re = RealPart self.Im = ImaginaryPart def norm(self): return sqrt(self.Re^2 + self.Im^2)

z = MyCompNumb(1, 2) z.norm()

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 $3+4i$ 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:

w = MyCompNumb(3,4) w.complex_conjugate() > <__main__.MyCompNumb object at 0x6fd0d766f60>

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__())`

.

class MyCompNumb: def __init__(self, RealPart, ImaginaryPart): self.Re = RealPart self.Im = ImaginaryPart def norm(self): return sqrt(self.Re^2 + self.Im^2) def complex_conjugate(self): return MyCompNumb(self.Re, -self.Im) def __repr__(self): return repr(self.Re)+' + '+repr(self.Im)+' * i'

w = MyCompNumb(3,4) w

This looks reasonable on first glance, but testing a few more examples, we quickly see some issues:

w.complex_conjugate()

MyCompNumb(0,6)

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 $(-\pi, \pi]$. 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:

def is_nth_root_of_unity(self, n): phi = self.arg() return self.norm()==1 and simplify(phi/(2*pi)*n).is_integer() MyCompNumb.is_nth_root_of_unity = is_nth_root_of_unity

z = MyCompNumb(0,1) z.is_nth_root_of_unity(4)

z = MyCompNumb(1/sqrt(2),1/sqrt(2)) z.is_nth_root_of_unity(8)

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:

class MyCompNumbWithArg(MyCompNumb): def __init__(self, RealPart, ImaginaryPart, Phi): MyCompNumb.__init__(self, RealPart, ImaginaryPart) self.Phi = Phi def complex_conjugate(self): return MyCompNumbWithArg(self.Re, -self.Im, -self.Phi) def __repr__(self): return repr(self.Re)+' + '+repr(self.Im)+' * i with arg = '+repr(self.Phi)

z = MyCompNumbWithArg(1,1,pi/4); z

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):

isinstance(z, MyCompNumb)

We inherited the methods from the old function, which is why the following works:

z.norm()

On the other hand, we have overwritten the functions

`__init__`

and`__repr__`

to adapt to the additional argument`Phi`

. Note that by calling`MyCompNumb.__init__`

we make sure that all the attributes set in`MyCompNumb`

will also be available in`MyCompNumbWithArg`

.

Additionally, we have modified the function

`complex_conjugate`

. Otherwise, it would return instances of`MyCompNumb`

and not`MyCompNumbWithArg`

.

Here are a few things we can now do with our class:

#### Exercise

Modify the

`__init__`

method of`MyCompNumbWithArg`

to include a check that the given argument`Phi`

actually makes sense (e.g. something like`MyCompNumbWithArg(1,0,pi)`

should not be allowed). Raise a`ValueError`

if it does not make sense.Modify the method

`arg`

to give back the chosen argument (instead of the one in $(-\pi, \pi]$).Write a method

`log`

which makes sense for complex numbers with an argument.*Mathematical hint:*What is $\log(z)$ for $z=r e^{i \phi}$?*Philosophical hint:*Is $\log(z)$ naturally a complex number, or a complex number with an argument?*Python hint:*Given a real number`r`

you can compute its logarithm with`r.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).

# For exercises below, add new methods here: class MyCompNumb: def __init__(self, RealPart, ImaginaryPart): self.Re = RealPart self.Im = ImaginaryPart def norm(self): return sqrt(self.Re^2 + self.Im^2) def complex_conjugate(self): return MyCompNumb(self.Re, -self.Im) def __repr__(self): return repr(self.Re)+' + '+repr(self.Im)+' * i' def arg(self): if self.Re == 0 and self.Im == 0: raise ValueError('Argument only defined for nonzero complex numbers') if self.Im >= 0: result = arccos(self.Re / self.norm()) else: result = -arccos(self.Re / self.norm()) return result def __add__(self, other): return MyCompNumb(self.Re + other.Re, self.Im + other.Im)

#### 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 $\mathbb{R}^2$ with sides parallel to the $x$ and $y$-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:

R = Rectangle(0,2,-3,5); R > Rectangle [0, 2] x [-3, 5] R.area() > 16 R + (-1,-1) > Rectangle [-1, 1] x [-4, 4] R.is_square() > False

**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:

z = MyCompNumb(2,0) 3+z

z == 2 # maybe should be True

z.is_zero()

In each case, the problem could be fixed in principle:

One could extend the methods

`__add__`

etc. to first check if`other`

is maybe an`Integer`

/`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*class`ComplexNumber`

.New elements are created via an

*instance*of the parent class:

from sage.rings.complex_field import ComplexField_class CC = ComplexField_class(42); CC

z = CC(1,3); z

These elements then remember their parents:

z.parent()

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:

CC(0.43)

CC(int(2))

CC(4/5)

Then, if you told your child class how to add, multiply, etc its elements (via functions

`_add_, _mul_, ...`

) and it encounters something of the form`int + ComplexNumber`

, it will silently convert (**coerce**) the other class into a complex number, and then do the arithmetic operation:

z + int(6)

This also works for things like the comparison that we saw above:

w = CC(2) w == 2

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 parent`C`

, convert both elements into this, and then perform the arithmetic there. Take the following example:

a = CC(1,3); a

R.<x,y> = PolynomialRing(QQ,2) b = x^2 + 3*y; b

c = a+b; c

c.parent()

Your parent and child class should inherit from some general classes (in our case

`Field`

and`FieldElement`

). This gives them many useful methods, which you don't have to worry about at all. Look at the code of the following functions:

CC.is_field??

CC.zero??

Then, if you have implemented a function `__eq__`

for checking equality and call `z.is_zero()`

, it will just check whether

z == z.parent().zero()

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 that`CC[x,y]`

is an integral domain:

CC[x,y].is_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 $(\mathrm{sign}(Re), \mathrm{sign}(Im))$.

**Solution** (uncomment to see)

#### Exercise

Write a method `is_in_quadrant`

which takes a number `quad`

from $1$ to $4$ 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)