One of the main reasons for working with classes is that objects can be grouped together and bound to a common object. We saw this already when looking at rational numbers; denominator and numerator are two objects which we bound to an instance of the RationalNumber
class. They are called attributes of the instance. The fact that an object is an attribute of a class instance becomes apparent from the way they are referenced, which we have used tacitly before:
<object>.attribute
Here are some examples of instantiation and attribute reference:
q = RationalNumber(3, 5) # instantiation q.numerator # attribute access q.denominator a = array([1, 2]) # instantiation a.shape z = 5 + 4j # instantiation z.imag
Once an instance is defined we can set, change or delete attributes of that particular instance. The syntax is the same as for regular variables:
q = RationalNumber(3, 5) r = RationalNumber(7, 3) q.numerator = 17 del r.denominator
Changing or deleting an attribute may have undesired side effects, which might even render the object useless. We will be learning more on this in the section Attributes that depend on each other. As functions are objects too, we can also use functions as attributes; they are called methods of the instance:
<object>.method(<arguments...>)
For example, let us add a method to the class RationalNumber
that converts the number to a float:
class RationalNumber: ... def convert2float(self): return float(self.numerator) / float(self.denominator)
Again, this method takes as its first (and only) argument, self
, the reference to the object itself. We use this method with a regular function call:
q = RationalNumber(10, 20) # Defines a new object q.convert2float() # returns 0.5
This is equivalent to the following call:
RationalNumber.convert2float(q)
Note again that the object instance is inserted as the first argument of the function. This use of the first argument explains the error message that would occur if this particular method were used with additional arguments:
The q.convert2float(15)
call provokes this error message:
TypeError: convert2float() takes exactly 1 argument (2 given)
The reason this does not work is that q.convert2float(15)
is precisely equivalent to RationalNumber.convert2float(q,15)
, which fails because RationalNumber.convert2float
takes only one argument.
The special method __repr__
gives us the ability to define the way the object is represented in a Python interpreter. For rational numbers, a possible definition of this method could be as follows:
class RationalNumber: ... def __repr__(self): return '{} / {}'.format(self.numerator,self.denominator)
With this method defined, just typing q
returns 10 / 20.
We would like to have a method that performs addition of two rational numbers. A first attempt could result in a method like this:
class RationalNumber: ... def add(self, other): p1, q1 = self.numerator, self.denominator if isinstance(other, int): p2, q2 = other, 1 else: p2, q2 = other.numerator, other.denominator return RationalNumber(p1 * q2 + p2 * q1, q1 * q2)
A call to this method takes the following form:
q = RationalNumber(1, 2) p = RationalNumber(1, 3) q.add(p) # returns the RationalNumber for 5/6
It would be much nicer if we could write q + p
instead. But so far, the plus sign is not defined for the RationalNumber
type. This is done by using the __add__
special method. So, just renaming add
to __add__
allows for using the plus sign for rational numbers:
q = RationalNumber(1, 2) p = RationalNumber(1, 3) q + p # RationalNumber(5, 6)
The expression q + p
is in fact an alias for the expression q.__add__(p)
. In the table (Table 8.1), you will find the special methods for binary operators, such as +
, -
, or *
.
Operator
|
Method |
Operator |
Method |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Table 8.1: Some Python operators & corresponding class methods, you can find the complete list [31]
The implementation of those operators for a new class is called operator overloading. Another example of operator overloading is a method for examining whether two rational numbers are the same:
class RationalNumber: ... def __eq__(self, other): return self.denominator * other.numerator == self.numerator * other.denominator
It is used like this:
p = RationalNumber(1, 2) # instantiation q = RationalNumber(2, 4) # instantiation p == q # True
Operations between objects belonging to different classes need special care:
p = RationalNumber(1, 2) # instantiation p + 5 # corresponds to p.__add__(5) 5 + p # returns an error
By default, the +
operator invokes the left operand’s method, __add__
. We programmed it so that it allows both, objects of type int
and objects of type RationalNumber
. In the statement 5 + p
, the operands are commuted and the __add__
method of the build-in int
type is invoked. This method returns an error as it does not know how to handle rational numbers. This case can be handled by the method __radd__
, with which we will equip the RationalNumber
class now. The method __radd__
is called reverse addition.
If operations like +
are applied to two operands of different types, the corresponding method (in this case, __add__
) of the left operand is invoked first. If this raises an exception, the reverse method (here, __radd__
) of the right operand is called. If this method does not exist, a TypeError
exception is raised.
Consider an example of reverse operation. In order to enable the operation 5+p where p is an instance of RationalNumber
, we define this:
class RationalNumber: .... def __radd__(self, other): return self + other
Note that __radd__
interchanges the order of the arguments; self
is the object of type RationalNumber
while other is the object that has to be converted.
Using a class instance together with brackets, ( , ) or [ , ] invokes a call to one of the special methods __call__
or __getitem__
, giving the instance the behavior of a function or of an iterable (refer to the Table 8.1 for these and other special methods):
class Polynomial: ... def __call__(self, x): return self.eval(x)
Which now may be used as follows:
p = Polynomial(...) p(3.) # value of p at 3.
The __getitem__
special method makes sense if the class provides an iterator (It is recommended to refer section Iterators in Chapter 9, Iterating before you consider the following example).
The recursion ui+1 = a1ui+ a0ui-1is called a three-term recursion. It plays an important role in applied mathematics, in particular in the construction of orthogonal polynomials. We can set up a three-term recursion as a class in the following way:
import itertools class Recursion3Term: def __init__(self, a0, a1, u0, u1): self.coeff = [a1, a0] self.initial = [u1, u0] def __iter__(self): u1, u0 = self.initial yield u0 # (see also Iterators section in Chapter 9) yield u1 a1, a0 = self.coeff while True : u1, u0 = a1 * u1 + a0 * u0, u1 yield u1 def __getitem__(self, k): return list(itertools.islice(self, k, k + 1))[0]
Here, the __iter__
method defines a generator object, which allows us to use an instance of the class as an iterator:
r3 = Recursion3Term(-0.35, 1.2, 1, 1) for i, r in enumerate(r3): if i == 7: print(r) # returns 0.194167 break
The __getitem__
method enables us to directly access the iterates as if r3
were a list:
r3[7] # returns 0.194167
Note that we used itertools.islice
when coding the __getitem__
method (refer to section Iterators of Chapter 9, Iterating, for more information). An example of the use of __getitem__
together with slices and the function ogrid
is given in the section Function with two variables in Chapter 5, Advance Array Concepts.
3.133.141.219