10.3 Designing and Implementing a Planet Class

We now turn our attention to solving the problem of building a model of the solar system. To do so will require that we consider the specific data used in the model. Even with the rich set of built-in classes provided by Python, it is often preferable to describe our problem and solution in terms of classes that are specifically designed to represent the objects in the problem. We begin by building a simple representation of a planet; we will then design and implement a Planet class.

To design a class to represent the idea of a planet, we need to consider the data the planet objects will need to know about themselves—that is, their instance data. The values of the instance data will help to differentiate the individual planet objects. To begin, we assume that each planet has a name. Each planet also has size information, such as its radius and its mass. We also want each planet to know how far it is from the sun.

In addition to data, the class will provide methods that a planet can perform. Some of these might be simple, such as returning the name of the planet. Other methods may require more computation. In Python, the general format for defining a class begins with the keyword class followed by the name of the class and a colon. The methods for the class are indented under the class heading and look just like function definitions.

Image

10.3.1 Constructor Method

The first method that all classes should provide is a constructor, which creates the data objects. To create a Planet object, we will need the previously listed four pieces of data as parameters: (1) name, (2) radius, (3) mass, and (4) distance. The constructor will then create instance variables to hold these values. Each instance variable holds a reference to an object.

In Python, the constructor is always called _ _init_ _ . (Note the two underscores that appear before and after init.) Methods in class definitions are written in the same way as the other functions we have seen thus far. For the constructor, we provide the name of the method and then a collection of parameters that describe the initial state of the new object. These parameters will be filled in with values when the method is invoked.

LISTING 10.1 shows the constructor for the Planet class. Notice that even though we stated that four pieces of information would be necessary to construct a planet, this method has five formal parameters. The extra parameter, self, is a special parameter that will always refer to the object being constructed. It must always be the first parameter in the list. Python automatically adds an actual parameter corresponding to self when you call the constructor, so you should not explicitly pass a parameter corresponding to self.

Image

LISTING 10.1 Planet class with a constructor

Line 3 of Listing 10.1 defines an instance variable called name, which is attached to the special parameter self by a dot. The notation self.name, when it appears on the left side of an assignment statement in the constructor, defines an instance variable. Because self refers to the object being constructed, self.name refers to a variable in the object. We call these variables instance variables. Lines 4–6 introduce and assign values to the other three instance variables.

We use the constructor to create an instance of the class by using the name of the class (we do not call _ _init_ _ directly). Just as _ _add_ _ is called automatically when the + operator is used, _ _init_ _ is called automatically when the class name is followed by the() function call operator.

Assuming that we have saved the definition of the Planet class in the file planetclass.py, SESSION 10.3 shows the creation of a Planet object named myPlanet. Note that the statement is similar to creating an instance of the Turtle class. As stated earlier, the call to the Planet constructor requires only four parameters, even though it is defined to have five. The first parameter, self, never receives an explicit value, as it always refers implicitly back to the object being constructed. Evaluating the reference myPlanet shows that it is an instance of the Planet class and the value 0x000001C7EAA97DA0 is actually the value of self, which is the address in memory where this object is stored. When we create our own classes, we are creating a new type. This becomes apparent when we call the type function on the myPlanet object.

Image

SESSION 10.3 Creating a Planet object

Also note that the instance variables are accessible through the object name—which is not a good thing. As Session 10.3 shows, the instance variable values can not only be accessed, but also be changed. This violates a basic design principle of classes, known as encapsulation: Only the class methods should be able to change the value of an object’s instance variables.

To fix this exposure of the instance variables, we can preface each instance variable name with two underscores (_ _). Instance variables whose names begin with _ _ are hidden from access outside the class. The new constructor for the Planet class is shown in LISTING 10.2.

Image

LISTING 10.2 The revised Planet class with hidden instance variables

Now when we try to access an instance variable, Python reports an error, as shown in SESSION 10.4. Another advantage of prefacing instance variables’ names with two underscores is that instance variables are easily distinguished from other local or global variables.

Image

SESSION 10.4 Instance variables are now hidden

The newly created object myPlanet is an instance of the Planet class with a name of "X25", a radius of 45, a mass of 198, and a distance of 1000. FIGURE 10.1 shows a logical view of this object. Notice that we have separated the object into two distinct layers. The inner layer, or object state, contains the instance variable names. The outer layer contains the names of the methods. In both cases, the names are simply references to the actual objects. We draw objects in this fashion to suggest the existence of a strong relationship between the methods of an object and its instance data.

A figure represents a planet object “my planet” whose name is X25, mass is 198, radius is 45, and the distance is 1000. The method shows init representing init_def.

FIGURE 10.1 A Planet object.

10.3.2 Accessor Methods

If no code outside the class can access the data of an object, then you might wonder how the data of an object can ever be known. The answer is that classes typically provide accessor methods, which allow code outside the class to access the values of the instance variables. These methods are also referred to as “getter” methods because the word “get” often appears in the name. Each instance variable usually has an associated accessor method. Another advantage of using accessor methods to retrieve the object data is that in a large software project, it is common for the internal representation of an instance variable to change. An accessor method hides those internal changes from the user—a practice known as information hiding.

LISTING 10.3 shows the four accessor methods associated with the Planet instance variables. For example, the getName method returns the string referenced by the _ _name instance variable. In the getName method, we refer to the _ _name instance variable as self._ _name, where self is a synonym for myPlanet (self and myPlanet both reference the same object).

Image

LISTING 10.3 Accessor methods in the Planet class

Listing 10.3 also shows accessor methods that return computed results based on values of instance variables. For example, we assume that a planet is a sphere, so we can write an accessor method that returns the volume of the planet—after all, the radius is already an instance variable. We also include methods that return the surface area as well as the density of the planet. Each of these values (volume, surface area, and density) can be calculated using the instance data, but volume, surface area, and density are not instance data. SESSION 10.5 shows these methods in action.

Image

SESSION 10.5 Calling accessor methods

We should look into two additional details before moving on. In the methods to compute volume and surface area, we used the value of pi from the math module. Notice that these methods import math. Also, the computation for density, which requires dividing the mass by the volume, calls the getVolume method. To call the getVolume method, we must use self.getVolume because self is a reference to the Planet object. Note that self is a reference to the object invoking the getDensity method, which in turn can be asked to invoke the getVolume method. If we had simply tried to call getVolume directly, we would get an error stating that getVolume was not found.

10.3.3 Mutator Methods

Mutator methods are procedures that mutate or change an object in some way. Changes to the object, in turn, involve changes to one or more of the instance variables. Recall that each object from a particular class has the same instance variables, but the values of those variables are different, which allows an object to “behave” differently when asked to perform methods. To change those variables’ values, we provide methods.

A mutator method will allow us to change the name of a planet in our example. Instead of using a cryptic name, such as “X25,” we can change the name to something more meaningful. To do so, we need a method that takes the new name as a parameter and modifies the value of the _ _name instance variable. LISTING 10.4 shows the setName method. As before, the first parameter is self. In addition, we define a second parameter that will pass the new name (newName). In the body of the method, the value of newName is assigned to the instance variable _ _name. Notice that this method does not return anything—a pattern that you will often encounter. Mutator methods modify the state of an object but do not return any value to the caller. Mutator methods for the other instance variables follow this same pattern.

Image

LISTING 10.4 Mutator methods in the Planet class

When we use the setName method, as shown in SESSION 10.6, we provide one parameter value. Although the method definition has two parameters, remember that the first parameter, self, will receive a reference to the object implicitly. As the state of the object has now changed, it responds with a differenct value when asked to perform the getName method.

Image

SESSION 10.6 Using a mutator method

10.3.4 Special Methods

As mentioned earlier, Python provides some special class methods such as integer addition and item selection in a list. We can also define our own special methods within a class. TABLE 10.2 shows some of the special methods that can be defined for a class. These special methods are sometimes referred to as magic methods, because they magically perform the work of an operator. Also, because these methods start and end with double underscores, they are often referred to as dunder methods (“dunder” stands for “double under”).

TABLE 10.2 Some of Python’s Special Methods

Image

One of these dunder methods, _ _str_ _, is used to provide a string representation for an object.

SESSION 10.7 shows the result of printing a Planet object. The print function automatically tries to convert the object into a string representation, but the Planet object does not know how to respond to this request. The result is the default representation, which is certainly not very helpful.

Image

SESSION 10.7 Printing a Planet object

We can provide better printing capability for the Planet class by defining our own version of the special method _ _str_ _. We have already seen that the default implementation for _ _str_ _ returns a string containing the class name and object address. What we need to do is provide an alternative implementation for the _ _str_ _ method that provides more user-friendly information.

To do this, we define a method with the name _ _str_ _ and give a new implementation, as shown in LISTING 10.5. The modified method does not require any information other than the self parameter. It returns the string that we have chosen to represent the object—in this case, the planet’s name. Note that the designer of a class decides which instance variables will be returned by the _ _str_ _ method. The only restriction is that the return value should be a string. SESSION 10.8 shows the result of printing the object. It also demonstrates that when you call either the print function or the str function on an object, that object’s _ _str_ _ method is automatically invoked.

Image

LISTING 10.5 The _ _str_ _ method

Image

SESSION 10.8 Using the _ _str_ _ method

FIGURE 10.2 shows the complete reference diagram for the myHome planet object.

A figure represents a logical view of a planet object “my home.”

FIGURE 10.2 Logical view of a Planet object.

Whether your class should implement versions of the other special methods depends on the class and its data. Like the return value for the _ _str_ _ method, the special methods that you implement are a design decision. For example, for the Planet class, we do not need the _ _add_ _ method because adding a planet to another planet doesn’t make much sense. For the comparison methods, usually we choose an instance variable as the value to be compared between objects. To illustrate this concept, we could decide that a planet is “less than” another planet if it is closer to the sun. Then we could define the _ _gt_ _ and _ _lt_ _ special methods as shown in LISTING 10.6. SESSION 10.9 shows these methods in use.

Image

LISTING 10.6 The _ _lt_ _ and _ _gt_ _ methods

Image

SESSION 10.9 Using the _ _lt_ _ and _ _gt_ _ methods

10.3.5 Methods and self

When methods are invoked, they create local namespaces in the namespace where they are defined. The difference between functions and methods is that each local namespace also has a reference to the object that invoked the method. The name we have been using for this reference is self.

Consider again the Planet class described earlier. When we create a new instance (see SESSION 10.10), we add the variable myPlanet to the main namespace with a reference to the object being constructed. Now when we invoke a method such as getName, a local namespace is created and placed in the main namespace. The formal parameter, self, implicitly receives a reference to the object that made the invocation (in this case myPlanet). The result can be seen in FIGURE 10.3. Although we do not have to call the first formal parameter self, that is a common convention that we will continue to use.

Image

SESSION 10.10 Showing scope with methods

A figure represents the local name space for a method.

FIGURE 10.3 A local namespace for a method.

When we look at the getName method, the only statement is return self._ _name. When Python evaluates self._ _name, it first evaluates self, which returns a reference to the object that invoked the method. Once we have the reference to the object, we continue to search for _ _name in the instance variables of the object. Since self refers to the myPlanet object, the value of the _ _name instance variable in myPlanet is returned. This process explains why each object, when invoking the getName method, will behave differently: These objects will likely have a different value for the _ _name instance variable.

10.3.6 Details of Method Storage and Lookup

Before moving on, it is important to understand the difference between the descriptive view of objects that we are using and the storage and lookup mechanism that Python uses. We have been representing objects as containing both instance variables and methods. This is an accurate representation for the instance variables, as each object needs its own copies. However, because methods are shared by all instances of a class, methods are stored in a slightly different fashion.

To see this, consider FIGURE 10.4. When a class is defined, the name of the class is added to the current namespace with a reference to a class definition object. In this case, we have added the name Planet to the main namespace with a reference to a corresponding class definition object. Class definition objects store method names that refer to method definitions that are implemented by the class. Figure 10.4 shows only two of the many methods that have been implemented by Planet.

A figure shows actual storage of methods.

FIGURE 10.4 Actual storage of methods.

When an instance of the Planet class is created, as was done using

Image

the Planet reference is followed to gain access to the _ _init_ _ method definition. This constructor is executed, and an object is created with the given instance variable values. This object is referenced from the main namespace using the name myPlanet.

Figure 10.4 shows one additional variable. The _ _class_ _ instance variable is used to provide each object with a reference to the class definition object to which it belongs. Now, when a method is called as

Image

the lookup sequence can proceed as follows. First, myPlanet is dereferenced to gain access to the object. Next, the _ _class_ _ reference is followed to find the class definition object. Then, the method name, getName, is used to access the method definition. When this method is invoked, a namespace is created and placed as usual.

With this architecture, only one copy of the method definition is needed—no matter how many objects exist for a particular class. Because each object will have a _ _class_ _ reference, the method definitions can always be located. Furthermore, because each method has a formal parameter, self, that references the object making the invocation, no confusion will occur when different objects call the same method. Instead, each object will get its own local namespace with an appropriate value for self.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.38.43