# 3. Objects and abstraction¶

In this chapter we will take a first look at the representation of
abstract mathematical objects and operations as data objects in a
computer program. We will learn about what it means for objects to have
a type, and how to create new types using the `class`

keyword.

## 3.1. Abstraction in action¶

Consider this line of Python code:

```
print(a + b)
```

What does it do? Well, assuming that `a`

and `b`

are suitably defined, it
prints their sum. This, however, begs the questions: what is “suitably
defined”, and what is “sum”? For example:

```
In [1]: a = 1
In [2]: b = 2
In [3]: print(a + b)
3
```

You’re unlikely to be surprised that Python can add integers. On the other hand it turns out we can also add strings:

```
In [1]: a = 'fr'
In [2]: b = 'og'
In [3]: print(a + b)
'frog'
```

So the meaning of `+`

depends on what is being added. What happens if
we add an integer to a string?

```
In [1]: a = 1
In [2]: b = 'og'
In [3]: print(a + b)
--------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [3], line 1
----> 1 print(a + b)
TypeError: unsupported operand type(s) for +: 'int' and 'str'
```

In this error, Python is complaining that `+`

does not make sense if
the items being added (the operands) are an integer and a
string. This makes our understanding of “suitably defined” more
concrete: clearly some pairs of objects can be added and others
can’t. However, we should be careful about the conclusions we draw. We
might be tempted to believe that we can add two values if they are of
the same type. However, if we try this with a pair of sets then we’re
also in trouble:

```
In [1]: a = {1, 2}
In [2]: b = {2, 3}
In [3]: print(a + b)
--------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [3], line 1
----> 1 print(a + b)
TypeError: unsupported operand type(s) for +: 'set' and 'set'
```

Conversely we might suspect that two values can be added only if they are of the same type. However it is perfectly legal to add an integer and a floating point value:

```
In [1]: a = 1
In [2]: b = 2.5
In [3]: print(a + b)
3.5
```

In Python, the operator `+`

encodes an abstraction for addition. This means
that `+`

stands for the addition operation, whatever that may mean for
a particular pair of operands. For the purposes of the abstraction,
everything which is specific to the particular operands is
ignored. This includes, for example,
the mechanism by which the addition is calculated and the value of the
result. This enables a programmer to think about the relatively simple
mathematical operation of addition, rather than the potentially
complex or messy way it might be implemented for particular data.

An *abstraction* is a mathematical object with a limited set of
defined properties. For the purposes of the abstraction, any other
properties that an object may have are disregarded.

An abstraction is a purely mathematical concept, but it is one which maps to one or more concrete realisations in code. Sometimes the abstract mathematical concept and its concrete realisation match so perfectly that it is difficult to distinguish the two. In those circumstances, we usually conflate the terminology for the abstraction and the code object. “Type” is one such example, and we turn to that now.

## 3.2. Types¶

In the previous section, we observed that addition may or may not be defined, depending on what the types of its operands are. In doing so, we skirted the question of what it means for an object to have type.

A *type* or *class* is an abstraction defined by a set of possible values, and
a set of operators valid for objects of that type.

Every object in Python has a type. This is true for primitive numeric
types, such as `int`

, `float`

, and `complex`

; for sequences such as
string (`str`

), `tuple`

, and `list`

; and also for more complex types
such as `set`

and dictionary (`dict`

). Indeed, the
Python concept of type goes much further, as we discover if we call
`type`

on various objects:

```
In [1]: type(1)
Out[1]: int
In [2]: type(abs)
Out[2]: builtin_function_or_method
```

So `1`

is an object of type `int`

, which means that it comes with all of
Python’s operations for integer arithmetic. `abs()`

, on the other hand,
is a built-in function, so its defining operation is that it can be
called on one or more suitable arguments (for example `abs(1)`

). If
every object has a type, what about types themselves? What is the type
of `int`

?

```
In [1]: type(int)
Out[1]: type
```

We see that `int`

is the type of integer objects, and is itself an
object with type `type`

. That rather invites the question of what
is the type of `type`

?

```
In [1]: type(type)
Out[1]: type
```

This actually makes perfect sense, because `type`

is simply the
type of types.

We will return to types in much more detail later. At this stage, the take-home message is that essentially everything you will encounter in Python is an object, and every object has a type.

Note

In Python, the term
“class” is essentially synonymous with “type”, so “what is the class
of `foo`

” is the same as saying “what is the type of `foo`

”. However
the two terms are not synonyms when used in code. `type`

can be
used to determine the type of an object, while `class`

is
used to define new types.

## 3.3. Defining new types¶

Python has a rich set of built-in types. These form powerful building blocks for the language, but one very soon finds mathematical abstractions which do not have implementations among the built-in types of the Python interpreter. For example, the built-in types do not include a matrix or multidimensional array type. The ability to make new data types which provide concrete implementations of further mathematical abstractions is central to effectively exploiting abstraction in programming.

As an example, lets suppose that we want to work with real polynomials in one variable. That is to say, functions of the form:

The set of all polynomials is a well-defined (though infinite) set of different values, with a number of well-defined properties. For example, we can add and multiply polynomials, resulting in a new polynomial. We can also evaluate a polynomial for a particular value of \(x\), which would result in a real value.

This is the mathematical abstraction of a polynomial. How would we
represent this abstraction in Python code? A polynomial is
characterised by its set of coefficients, so we could in principle
represent a polynomial as a `tuple`

of coefficient
values. However, the addition of tuples is concatenation, and
multiplication of two tuples isn’t even defined, so this would be a
very poor representation of the mathematics: a polynomial represented
as a tuple of coefficients would not behave the way a mathematician
would expect. Instead, what we need to do is make a new type whose
operations match the mathematical properties of a polynomial.

### 3.3.1. Classes and constructors¶

The Python keyword for declaring a new type is
`class`

. Just like a function declaration, this creates a new
indented block. In this case, the block contains all of the function
declarations which define the operations on this new type. Let’s make
a very simple implementation:

```
class Polynomial:
def __init__(self, coefs):
self.coefficients = coefs
```

We’ll interpret the \(i\)-th coefficient as the coefficient of \(x^i\). This will simplify the program logic, but take care because mathematicians usually write coefficients from largest power of \(x\) to smallest, and this is the opposite of that. Executing this code in a Python interpreter would enable us to create a simple polynomial, and inspect its coefficients:

```
In [7]: f = Polynomial((0, 1, 2))
In [8]: f.coefficients
Out[8]: (0, 1, 2)
```

The three lines of Python defining the `Polynomial`

class contain
several important concepts and Python details that it is important to
understand.

The class definition statement opens a new block, so
just like a function definition, it starts with
the keyword followed by the name of the class we are defining, and
ends with a colon. User-defined classes in Python (i.e. classes not
built into the language) usually have CapWords names. This means
that all the words in the name are capitalised and run together without spaces. For
example, if we decided to make a separate class for complex-valued
polynomials, we might call it `ComplexPolynomial`

.

Inside the class definition, i.e. indented inside the block, is a
function called `__init__()`

. Functions defined inside a class
definition are called methods. The `__init__()`

method has a
rather distinctive form of name, starting and ending with two
underscores. Names of this format are used in the Python language for
objects which have special meaning in the Python language. The
`__init__()`

method of a class has special meaning in Python as
the constructor of a class. When we write:

```
In [7]: f = Polynomial((0, 1, 2))
```

This is called instantiating an object of type
`Polynomial`

. The following steps occur:

Python creates an object of type

`Polynomial`

.The

`__init__()`

special method of`Polynomial`

is called. The new`Polynomial`

object is passed as the first parameter (`self`

), and the`tuple`

`(0, 1, 2)`

is passed as the second parameter (`coefs`

).The name

`f`

in the surrounding scope is associated with the`Polynomial`

.

Note

Notice that `Polynomial.__init__()`

doesn’t return anything. The role
of the `__init__()`

method is to set up the object, `self`

; it
is not to return a value. `__init__()`

never returns a value.

### 3.3.2. Attributes¶

Let’s now look at what happened inside the `__init__()`

method. We
have just one line:

```
self.coefficients = coefs
```

Remember that `self`

is the object we are setting up, and `coefs`

is the
other parameter to `__init__()`

. This line of code creates a new
name inside this `Polynomial`

object, called
`coefficients`

, and associates this new name with the object passed as
the argument to the `Polynomial`

constructor. Names such as
this are called attributes. We create an attribute
just by assigning to it, and we can then read back the attribute using
the same syntax, which is what we did here:

```
In [8]: f.coefficients
Out[8]: (0, 1, 2)
```

Attributes can be given any name which is allowed for a Python name in general - which is to say sequences of letters, numbers and underscores starting with a letter or an underscore. Special significance attaches to names starting with an underscore, so these should be avoided in your own names unless you intend to create a private attribute.

### 3.3.3. Methods¶

We have already met the special method `__init__()`

,
which defines the class constructor. A much more typical case is an
ordinary method, without a special underscore name. For example,
suppose we wish to be able to access the degree of a polynomial, then
we might add a `degree()`

method to our class:

```
class Polynomial:
def __init__(self, coefs):
self.coefficients = coefs
def degree(self):
return len(self.coefficients) - 1
```

Observe that the new method is indented inside the `class`

block at the same level as the `__init__()`

method. Observe also
that it too takes `self`

as its first parameter. A key difference from
the `__init__()`

method is that `degree()`

now returns a
value, as most functions do. We can now use our new method to recover
the degree of our polynomial.

```
In [1]: f = Polynomial((0, 1, 2))
In [2]: f.degree()
Out[2]: 2
```

To clarify the role of the `self`

parameter it helps to understand
that `f.degree()`

is just a short way of writing
`Polynomial.degree(f)`

. Like attributes, methods can have any allowed Python
name. Attributes and methods on an object form part of the same
namespace, so you can’t have an attribute and a method with the same
name. If you try, then the name will be overwritten with whichever was defined
later, and the attribute or method defined first will no longer be accessible
under that name. This is unlikely to be what you wanted.

Note

The object itself is always passed as the first argument to a method.
Technically, it is possible to name the first parameter any legal Python
name, but there is a **very** strong convention that the first parameter to
any instance method is called `self`

. **Never, ever** name this
parameter anything other than `self`

, or you will confuse every Python
programmer who reads your code!

### 3.3.4. String representations of objects¶

Remember that a key reason for defining new classes is to enable users
to reason about the resulting objects at a higher mathematical level. An
important aid to the user in doing this is to be able to look at the
object. What happens if we print a `Polynomial`

?

```
In [1]: f = Polynomial((0, 1, 2))
In [2]: print(f)
<Polynomial object at 0x104960dd0>
```

This is less than useful. By default, Python just prints the class of the object and the memory address at which this particular object is stored. This is, however, not so surprising if we think about the situation in a little more depth. How was Python supposed to know what sort of string representation makes sense for this object? We will have to tell it.

The way we do so is using another special method. The special
method name for the human readable string representation of an object is
`__str__()`

. It takes no arguments other than the object itself.
Listing 3.1 provides one possible implementation of this method.

```
1def __str__(self):
2
3 coefs = self.coefficients
4 terms = []
5
6 # Degree 0 and 1 terms conventionally have different representation.
7 if coefs[0]:
8 terms.append(str(coefs[0]))
9 if self.degree() > 0 and coefs[1]:
10 terms.append(f"{coefs[1]}x")
11
12 # Remaining terms look like cx^d, though factors of 1 are dropped.
13 terms += [f"{'' if c == 1 else c}x^{d}"
14 for d, c in enumerate(coefs[2:], start=2) if c]
15
16 # Sum polynomial terms from high to low exponent.
17 return " + ".join(reversed(terms)) or "0"
```

This somewhat longer piece of code results from the fact that the linear and constant terms in a polynomial are usually represented slightly differently from the higher-order terms. Having added this new method to our class, we can now observe the result:

```
In [2]: f = Polynomial((1, 2, 0, 1, 5))
In [3]: print(f)
5x^4 + x^3 + 2x + 1
```

In fact, Python provides not one, but two special
methods which convert an object to a
string. `__str__()`

is called by `print()`

and also by
`str`

. Its role is to provide the string representation which
is best understood by humans. In mathematical code, this will usually
be the mathematical notation for the object. In contrast, the
`__repr__()`

method is called by `repr()`

and also provides
the default string representation printed out by the Python command
line. By convention, `__repr__()`

should return a string which a
user might type in order to recreate the object. For example:

```
def __repr__(self):
return type(self).__name__ + "(" + repr(self.coefficients) + ")"
```

`type(self).__name__`

simply evaluates to the class name, in this case
`Polynomial`

. This is better than hard-coding the class name because, as we will
see in Chapter 7, this implementation of
`__repr__()`

might well end up being inherited by a class with a
different name. Notice that in order to help ensure consistency of
representations we call `repr()`

on the coefficients in this case, whereas
in the `__str__()`

method we called `str`

.

We can now observe the difference in the result:

```
In [2]: f = Polynomial((1, 2, 0, 4, 5))
In [3]: f
Out[3]: Polynomial((1, 2, 0, 4, 5))
```

When using f-strings, the `repr()`

of a an
object can be inserted instead of the `str`

by using the `!r`

modifier.
For example, we could just as well have written the method above as:

```
def __repr__(self):
return f"{type(self).__name__}({self.coefficients!r})"
```

### 3.3.5. Object equality¶

## Video: object equality and test driven development

Imperial students can also watch this video on Panopto

When are two objects equal? For built-in types Python has equality rules which broadly match the mathematical identities that you might expect. For example, two numbers of different types are equal if their numerical value is equal:

```
In [1]: 2 == 2.0
Out[1]: True
In [2]: 2.0 == 2+0j
Out[2]: True
```

Similarly, intrinsic sequence types are equal when their contents are equal:

```
In [3]: (0, 1, "f") == (0., 1+0j, 'f')
Out[3]: True
In [4]: (0, 1, "f") == (0., 1+0j, 'g')
Out[4]: False
In [5]: (0, 1, "f") == (0., 1+0j)
Out[5]: False
```

This mathematically pleasing state of affairs doesn’t, however, automatically carry over to new classes. We might expect that two identically defined polynomials might compare equal:

```
In [6]: from example_code.polynomial import Polynomial
In [7]: a = Polynomial((1, 0, 1))
In [8]: b = Polynomial((1, 0, 1))
In [9]: a == b
Out[9]: False
```

The reason for this is obvious when one thinks about it: Python has no way to
know when two instances of a new class should be considered equal. Instead, it
falls back to comparing the unique identity of every object. This is accessible
using the built-in function `id()`

:

```
In [10]: id(a)
Out[10]: 4487083344
In [11]: id(b)
Out[11]: 4488256096
```

This is a perfectly well-defined equality operator, but not a very
mathematically useful one. Fortunately, Python allows us to define a more useful
equality operator using the `__eq__()`

special method. This
takes the current object and the object it is being compared to, and returns
`True`

or `False`

depending on whether the objects should be
considered equal. When we write `a == b`

in Python, what actually happens is
`a.__eq__(b)`

.

A basic implementation of `__eq__()`

that checks that the other
object is a `Polynomial`

with the same
coefficients is:

```
def __eq__(self, other):
return isinstance(other, Polynomial) and \
self.coefficients == other.coefficients
```

Equipped with this method, `Polynomial`

equality now behaves as we might expect.

```
In [1]: from example_code.polynomial import Polynomial
In [2]: a = Polynomial((1, 0, 1))
In [3]: b = Polynomial((1, 0, 1))
In [4]: a == b
Out[4]: True
```

### 3.3.6. Defining arithmetic options on objects¶

It’s all very well to be able to compare our polynomial objects, but
we won’t really have captured the mathematical abstraction involved
unless we have at least some mathematical operations. We have already
observed that objects of some classes can be added. Is this true for
`Polynomial`

s?

```
In [2]: a = Polynomial((1, 0))
In [3]: b = Polynomial((1,))
In [4]: a + b
--------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [4], line 1
----> 1 a + b
TypeError: unsupported operand type(s) for +: 'Polynomial' and 'Polynomial'
```

Once again, this is not so surprising since we haven’t
defined what addition of polynomials should mean. The special
method which defines addition is `__add__()`

. It takes the
object itself and another object and returns their sum. That is,
when you write `a + b`

in Python, then what actually happens is
`a.__add__(b)`

.

Before we define our addition method, we first need to consider what other objects it might make sense to add to a polynomial. Obviously, we should be able to add two polynomials, but it also makes sense to add a number to a polynomial. In either case, the result will be a new polynomial, with coefficients equal to the sum of those of the summands.

We also need to do something in the case where a user attempts to add to a
polynomial a value for which the operation makes no sense. For example, a user
might accidentally attempt to add a string to a polynomial. In this case, the
Python language specification requires that we return the special value
`NotImplemented`

. Differentiating between the types of operands requires
two more Python features we have not yet met. One of these is the built in
function `isinstance()`

, which tests whether an object is an instance of a
class. The other is the class `Number`

, which we import from
the built-in `numbers`

module. All Python numbers are instances of
`Number`

so this provides a mechanism for checking whether the
other operand is a number. We will consider `isinstance()`

and
`Number`

in more detail when we look at inheritance and abstract base classes.

Putting all this together, Listing 3.2 defines polynomial addition.

```
1def __add__(self, other):
2 if isinstance(other, Number):
3 return Polynomial((self.coefficients[0] + other,)
4 + self.coefficients[1:])
5
6 elif isinstance(other, Polynomial):
7 # Work out how many coefficient places the two polynomials have in
8 # common.
9 common = min(self.degree(), other.degree()) + 1
10 # Sum the common coefficient positions.
11 coefs = tuple(a + b for a, b in zip(self.coefficients[:common],
12 other.coefficients[:common]))
13
14 # Append the high degree coefficients from the higher degree
15 # summand.
16 coefs += self.coefficients[common:] + other.coefficients[common:]
17
18 return Polynomial(coefs)
19
20 else:
21 return NotImplemented
```

Notice that we create a new `Polynomial`

object for the result
each time: the sum of two polynomials is a different polynomial, it
doesn’t modify either polynomial in place.

Let’s try our new addition functionality in action:

```
In [2]: a = Polynomial((1, 2, 0, 1))
In [3]: print(a)
x^3 + 2x + 1
In [4]: b = Polynomial((0, 1))
In [5]: print(b)
x
In [6]: print(a + b)
x^3 + 3x + 1
In [7]: print(a + 1)
x^3 + 2x + 2
In [8]: print(1 + a)
--------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [8], line 1
----> 1 print(1 + a)
TypeError: unsupported operand type(s) for +: 'int' and 'Polynomial'
```

So, everything proceeds as expected until we try to add a
`Polynomial`

to an integer. What happened? Remember that
`1 + a`

causes Python to call `int.__add__(1, a)`

. What does that do?:

```
In [9]: int.__add__(1, a)
Out[9]: NotImplemented
```

Naturally, Python’s inbuilt `int`

type knows nothing about our
new `Polynomial`

class, so when we ask it to do the addition,
it returns `NotImplemented`

. We could, however, tell
`Polynomial`

how to be added to an `int`

, and Python
provides a mechanism for this. If the `__add__()`

of the left hand
operand of `+`

returns `NotImplemented`

, then Python tries the
reverse addition method, called `__radd__()`

, on the right hand
operand. Because we know that polynomial addition is commutative,
we can define this very easily:

```
def __radd__(self, other):
return self + other
```

With our newly enhanced `Polynomial`

class, we can revisit the
previously problematic operation:

```
In [2]: a = Polynomial((1, 2, 0, 1))
In [3]: print(1 + a)
x^3 + 2x + 2
```

Of course, addition is not the only arithmetic operator one might wish to overload. A fully featured polynomial class will, at the very minimum, need subtraction, multiplication (by a scalar or another polynomial) and exponentiation by an integer power. The combination of these, and particularly exponentiation, would allow the user to define new polynomials in a particularly natural way, using Python’s intrinsic operators:

```
In [1]: x = Polynomial((0, 1))
In [2]: print(x)
x
In [3]: p = x**3 + 2*x + 2
In [4]: p
Polynomial((2, 2, 0, 1))
```

The special method names for further arithmetic operators are
given in the Python documentation. The
implementation of multiplication, exponentiation, and subtraction for
the `Polynomial`

class is left as an exercise.

### 3.3.7. Creating objects that act like functions¶

From a mathematical perspective, a real polynomial is a function. That is to say, if:

then for any real \(x\), \(f(x)\) is defined and is a real
number. We already know from the example of `abs()`

, above, that
Python functions are objects. However, our challenge is the converse of
this: we have `Polynomial`

objects which we would like to be
able to call like functions. The solution to our challenge is that
calling a function is an operation on an object similar to addition,
and Python provides another special method name for
this. `f(x)`

is mapped to `f.__call__(x)`

, so any Python object with a
`__call__()`

method behaves like a function, and any class
defining a `__call__()`

method in effect defines a new type of
function.

## 3.4. Encapsulation¶

The property that objects have of bundling up data and methods in a more-or-less opaque object with which other code can interact without concerning itself with the internal details of the object is called encapsulation. Encapsulation is one of the core concepts in object-oriented programming. In particular, encapsulation is key to creating single objects representing high level mathematical abstractions whose concrete realisation in code may require many pieces of data and a large number of complex functions.

## 3.5. Assignment and instantiation¶

One common confusion among new Python programmers concerns the distinction between making new objects, and assigning new names to existing objects. The key point to remember is that assignment in Python does not by itself create new objects, only new variable names. For example:

```
In [1]: a = set()
In [2]: b = a
```

The right hand side of the first line instantiates a new
set, and the assignment creates the name `a`

and associates it with the same
set. The second line is just an assignment: it associates the name `b`

with the
same set. We can see the effect of this if we add an item to `b`

and then look
at the contents of `a`

:

```
In [3]: b.add(1)
In [4]: print(a)
{1}
```

The same distinction between instantiating objects and making new references to them is the cause of a frequent mistake when trying to create a list of empty objects:

```
In [5]: c = [set()] * 5
In [6]: print(c)
[set(), set(), set(), set(), set()]
```

The programmer almost certainly intended to create a list containing five empty sets. Instead, they have created a list containing five references to the same set:

```
In [7]: c[0].add(2)
In [8]: print(c)
[{2}, {2}, {2}, {2}, {2}]
```

The right way to create a list of five empty sets is to use a list comprehension. This will instantiate a different set for each entry in the list:

```
In [9]: d = [set() for i in range(5)]
In [10]: d[0].add(2)
In [11]: print(d)
[{2}, set(), set(), set(), set()]
```

## 3.6. Glossary¶

- abstraction¶
A mathematical concept with a limited set of defined properties. For the purposes of the abstraction, any other properties that an object may have are disregarded.

- attribute¶
A value encapsulated in another object, such as a class. Attributes are accessed using dot syntax, so if

`b`

is an attribute of`a`

then its value is accessed using the syntax`a.b`

. Methods are a special case of attributes.- class¶
- type¶
An abstraction defined by a set of possible values, and a set of operators valid for objects of that type.

`Class`

and`type`

are essentially synonymous, though the two words have different roles in Python code.- concatenation¶
The combination of two sequences by creating a new sequence containing all of the items in the first sequence, followed by all of the items in the second sequence. For example

`(1, 2) + (3, 4)`

is`(1, 2, 3, 4)`

.- constructor¶
The

`__init__()`

method of a class. The constructor is passed the new object as its first argument (`self`

) and is responsible for setting up the object. The constructor modifies`self`

in place: constructors never return a value.- data attribute¶
An attribute which is not a method. As the name suggests, these are used to store data in an object.

- encapsulation¶
The bundling up of attributes and methods into an object which can be dealt with as a single unit.

- infix operator¶
A mathematical operator whose symbol is written between its operands. Examples include addition, subtraction, division and multiplication.

- instance¶
An object of a particular class.

`a`

is an instance of`MyClass`

means that`a`

has class`MyClass`

. We will return to this concept when we learn about inheritance.- instantiate¶
To create an instance of a class by calling its constructor.

- method¶
- instance method¶
A function defined within a class. If

`a`

is an instance of`MyClass`

, and`MyClass`

has a`foo()`

method then`a.foo()`

is equivalent to`MyClass.foo(a)`

. The first parameter of an instance method is always named`self`

.- operands¶
The input values to an operator. For example the operands to

`+`

are the numbers being added (the summands), while the operands to exponentiation are the base and exponent.- pseudocode¶
A description of an algorithm given in the form of a computer program but without conforming to the rules of a particular programming language, and employing mathematical notation or plain text to express the algorithm in a human-readable form.

- special method¶
- magic method¶
A method which has special meaning in the Python language. Special method names are used to define operations on a class such as arithmetic operators, indexing, or the class constructor. Special methods have names starting and ending with a double underscore (

`__`

). See the Python documentation for a technical description. Special methods are sometimes informally called “magic methods”.

## 3.7. Exercises¶

Using the information on the book website
obtain the skeleton code for these exercises. The skeleton code contains a
`polynomial`

package with a version of the `Polynomial`

class.

Implement the following operations on the `Polynomial`

class.

Subtraction (

`__sub__()`

and`__rsub__()`

).Multiplication by another polynomial, and by a scalar (

`__mul__()`

and`__rmul__()`

).Exponentiation by a positive integer power (

`__pow__()`

). It may be useful to know that all integers are instances of`numbers.Integral`

.Polynomial evaluation at a scalar value (

`__call__()`

).

Note

Don’t forget to commit and push your changes, and make sure that the tests pass on GitHub!

Define a `dx()`

method on the `Polynomial`

class which returns a
new `Polynomial`

which is the derivative of that
`Polynomial`

. Also define a function `derivative`

in
`polynomials.py`

which takes a `Polynomial`

and returns its
derivative. Rather than duplicating code, you should implement the function
by calling the method.

Inside the exercise repository, create a new `shape`

package containing a `circle`

module.

Create a

`Circle`

class whose constructor takes two user parameters,`centre`

and`radius`

.`centre`

should be a length 2 sequence containing the two-dimensional coordinates of the centre, while`radius`

is the radius of the circle.Add an import statement to

`shape/__init__.py`

so that the following code works:from shape import Circle

Implement the

`__contains__()`

special method on the`Circle`

class so that it returns`True`

if a point (represented by a length 2 sequence of coordinates) lies inside the circle. For example, the following code should print`True`

.from shape import Circle c = Circle((1., 0.), 2) print((0.5, 0.5) in c)

Make the `circle`

and `polynomial`

packages installable. As with
last chapter’s exercise, Pytest can’t test this so you’ll need to push to
GitHub and check that the autograding tests pass there.

Footnotes