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 ofPolynomial
is called. The newPolynomial
object is passed as the first parameter (self
), and thetuple
(0, 1, 2)
is passed as the second parameter (coefs
).The name
f
in the surrounding scope is associated with thePolynomial
.
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 ofa
then its value is accessed using the syntaxa.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
andtype
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 modifiesself
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 ofMyClass
means thata
has classMyClass
. 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 ofMyClass
, andMyClass
has afoo()
method thena.foo()
is equivalent toMyClass.foo(a)
. The first parameter of an instance method is always namedself
.- 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.
Turn the exercises repository into an installable Pip package. 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.
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 ofnumbers.Integral
.Polynomial evaluation at a scalar value (
__call__()
).
Hint
A limitation of the provided implementation of Polynomial
is
that it doesn’t strip leading zeroes. This means that it doesn’t
correctly identify that, for example, Polynomial((1, 0)) ==
Polynomial((1,))
. You may find it convenient to remove this limitation
by removing any leading zeroes in Polynomial.__init__()
. If you do
this, take care that printing the zero polynomial still works.
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
andradius
.centre
should be a length 2 sequence containing the two-dimensional coordinates of the centre, whileradius
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 theCircle
class so that it returnsTrue
if a point (represented by a length 2 sequence of coordinates) lies inside the circle. For example, the following code should printTrue
.from shape import Circle c = Circle((1., 0.), 2) print((0.5, 0.5) in c)
Footnotes