Example The Length Class

Now that we have seen a lot of Python's general and numerical special methods, we are in a position to create a complete custom data type. We will create the Length class to hold physical lengths. We want to be able to create lengths using syntax like this: distance = Length("22 miles"). And we want to be able to retrieve lengths in the units we prefer—for example, km = distance.to("km"). The class must not support the multiplication of lengths by lengths (since that would produce an area), but should support multiplication by amounts; for example distance * 2.

As usual, although the source code, in chap03/length.py, has docstrings, we will not show them in the following snippets, both to save space and to avoid distracting us from the code itself.

from __future__ import division

Truncating division

class Length(object):

convert = dict(mi=621.371e-6, miles=621.371e-6, mile=621.371e-6, yd=1.094, yards=1.094, yard=1.094, ft=3.281, feet=3.281, foot=3.281, inches=39.37, inch=39.37, mm=1000, millimeter=1000, millimeters=1000, millimetre=1000, millimetres=1000, cm=100, centimeter=100, centimeters=100, centimetre=100, centimetres=100, m=1.0, meter=1.0, meters=1.0, metre=1.0, metres=1.0, km=0.001, kilometer=0.001, kilometers=0.001, kilometre=0.001, kilometres=0.001) convert["in"] = 39.37 numbers = frozenset("0123456789.eE")

We begin with a class statement to give our class a name, and to provide a context in which we can create static data and methods. We have inherited

The first statement in the file is rather intriguing. The from __future__ import syntax is used to switch on Python features that will be on by default in a later version. Such statements must always come first. In this case, we are saying that we want to switch on Python's future division behavior, which is for / to do "true", or floating-point division, rather than what it does normally, that is, truncating division. (The // operator does truncating division, if that is what we really need.)

from object, so our class is new-style. Then we create some static data. First we create a dictionary that maps names to conversion factors. We can't use "in" as an argument name because it is a Python keyword, so we insert it into the dictionary separately using the [] operator. We also create a set of the characters that are valid in floating-point numbers.

def_init_(self, length=None):

if length is None:

else:

for i, char in enumerate(length): if char in Length.numbers:

digits += char else:

self._amount = float(digits)

raise ValueError, "need an amount and a unit" self._amount /= Length.convert[unit]

Inside the initializer, the local variables length, digits, i, char, and unit all go out of scope at the end of the method. We refer only to one instance variable, self._amount. This variable always holds the given length in meters, no matter what units were used in the initializer, and is accessible from any method. We also refer to two static variables, Length.numbers and Length.convert.

When a Length object is created, Python will call the __init__() method. We give the user two options: Pass no arguments, in which case the length will be 0 meters, or pass a string that specifies an amount and a unit with optional whitespace separating the two.

If a string is given, we want to iterate over the characters that are valid in numbers, and then take the remainder to be the units. Python's enumerate() function returns an iterator that returns a tuple of two values on each iteration, an index number starting from 0, and the corresponding item from the sequence. So if the string in length was "7 mi", the tuples returned would be (0, "7"), (1, " "), (2, "m"), and (3, "i"). We can unpack a tuple in a for loop simply by providing enough variables.

As long as we retrieve characters that are in the numbers set we add them to our digits string. Once we reach a character that isn't in the set, we attempt to convert the digits string to a float, and take the rest of the length string to be the units. We strip off any leading and trailing whitespace from the units string, and lowercase the string. Finally, we calculate how many meters the given length is by using the conversion factor from the static convert dictionary.

We called our data attribute_amount, rather than, say, amount, because we want this data to be private. Python will name-mangle any name in a class that begins with two underscores (and which does not end in two underscores) to be preceded by an underscore and the class name to make the attribute's name unique. In this case,_amount will be mangled to be _Length_amount. When we look at some of the special methods, we will see a practical reason why this is beneficial.

Clearly many things could go wrong. The floating-point conversion could fail, there may be no units given (in which case we raise an exception, along with a "reason" string), or the units may not match any in the convert dictionary. In this method, we have chosen to let the possible exceptions be raised, documenting them in the method's docstring so that users of the class know what to expect.

def set(self, length): self._init_(length)

We want our lengths to be mutable, so we have provided a set() method. It takes the same argument as_init_(), and because_init_() is an initializer rather than a constructor, we can safely pass the work on to it.

return self._amount * Length.convert[unit]

We store lengths inside the class as meters. This means that we need to maintain only a single floating-point value, rather than, say, a value and a unit. But just as we can specify our preferred units when we create a length, we also want to be able to retrieve a length as a value in the units of our choice. This is what the to() method achieves. It uses the convert dictionary to convert the meters value to the units specified.

def copy(self):

other._amount = self._amount return other

As we know, if we use the = operator, we will simply bind (or rebind) a name, so if we want a genuine copy of a length we need some means of doing it. Here we have chosen to provide a copy() method. But we did not have to: Instead, we could have simply relied on the copy module. For example:

import copy import length x = length.Length("3 km") y = copy.copy(x)

We have imported both the standard copy module and our own length module, (assuming that chap03 is in sys.path, and that the module is called length.py).

Then we created two independent lengths. If, instead, we had done y = x and then changed x using the set() method, y would have changed too. Of course, since we have implemented our own copy() method, we could also have copied by writing y = x.copy().

We could have implemented the copy() method differently. For example:

def copy(self): # Alternative #1 import copy return copy.copy(self)

def copy(self): # Alternative #2 return eval(repr(self))

The first of these uses Python's standard copy module to implement the copy() function. The second uses the repr() method to provide an eval()-able string version of the length—for example, Length('3000.000000m')—and then uses eval() to evaluate this code; in this case, it constructs a new length of the same size as the original.

@staticmethod def units():

return Length.convert.keys()

We have provided this static method to give users of our class access to the names of the units we support. By using keys(), we ensure that a list of unit names is returned, rather than an object reference to our static dictionary.

With the exception of the_init_() initialization method, none of the methods we have looked at so far has been a special method. But we want our Length class to work like a standard Python class, so that it can be used with operators like * and *=, compared, and converted to suitable compatible types. All these things are achievable by implementing special methods. We will begin with comparisons.

return cmp(self._amount, other._amount)

This method is easy to implement since we can just compare how long each length is.

The other object could be an object of any type. Thanks to Python's name mangling, the actual comparison is made between self._Length_amount and other._Length_amount. If the other object does not have a _Length_amount attribute, that is, if it is not a length, Python will raise an AttributeError which is what we want. This is true of all the other methods that take a length argument in addition to self.

Without the name mangling, there is a small risk of the other object not being a length, yet happening to have an __amount attribute. To prevent this risk we might have used type testing, even though this is often poor practice in object-oriented programming.

return "Length('%.6fm')" % self.__amount def _str_(self):

Python's floating-point accuracy depends on the compiler it was built with, but it is very likely to be accurate to much more than the six decimal places we have chosen to use for our "representation" method.

For the string representation, we don't need to be as accurate, nor do we need to return a string that can be eval()'d, so we just return the raw length and the meters unit. If users of our Length class want a string representation with a different unit, they can use to()—for example, "%s miles" % length.Length("200 ft").to("miles").

return Length("%fm" % (self._amount + other._amount))

self._amount += other._amount return self

We have used two special methods to support addition. The first supports binary + with a length operand on either side. It constructs and returns a new Length object. The second supports += for incrementing a length by another length.

They allow us to write code like this:

x = length.Length("30ft") y = length.Length("250cm")

It is also possible to implement_radd_() for mixed-type arithmetic, but we have not done so because it does not make sense for the Length class.

We will omit the code that provides support for subtraction since it is almost identical to the code for addition (and is in the source file).

if isinstance(other, Length): raise ValueError, \

"Length * Length produces an area not a Length" return Length("%fm" % (self._amount * other))

return Length("%fm" % (other * self._amount))

self._amount *= other return self

For the multiplication methods, we provide support for multiplying a length by a number. If we assume that x is a length,_mul_() supports uses like x *

5, and_rmul_() supports uses like 5 * x. We must explicitly disallow multiplying lengths together in mul () since the result would be an area and not a length. We do not need to do this in_rmul_() because_mul_() is always tried first, and if it raises an exception, Python does not try_rmul_(). The

_imul_() method supports in-place (augmented) multiplication—for example, x *= 5.

def_truediv_(self, other):

return Length("%fm" % (self._amount / other))

def_itruediv_(self, other):

self._amount /= other return self

The implementation of the division special methods has a similar structure to the other arithmetic methods. One reason for showing them is to remind ourselves that the reason the / and /= operators perform floating-point division is because of the from future import division directive at the beginning of the length.py file. It is also possible to reimplement truncating division, but that isn't appropriate for the Length class.

Another reason for showing them is that they are subtly different from the addition methods we have just seen. Although addition and subtraction operate only on lengths, multiplication and division operate on a length and a number.

def float (self): return self._amount def _int_(self):

return int(round(self._amount))

We have chosen to support two type conversions, both of which are easy to write and understand. The_str_() method implemented earlier is also a type conversion (to type str).

Now that we have seen how to implement a custom data type, we will turn our attention to implementing a custom collection class.

Was this article helpful?

0 0
Tube Traffic Ninja

Tube Traffic Ninja

Discover How You Can Quickly And Easily Dominate Google and YouTube... With Simple Cash Generating Videos. Did you know that YouTube is the second largest search website on the entire Internet? YouTube gets more daily searches than Bing and Yahoo. In fact, there is only one search engine that gets more action.

Get My Free Ebook


Post a comment