GEOG 489
Advanced Python Programming for GIS

4.6.2 Constructors with parameters and defining the == and < operators

PrintPrint

It can be a bit cumbersome to use methods or assignments to set all the instance variables to the desired initial values after a new object has been created. Instead, one would rather like to pass initial values to the constructor and get back an object with these values for the instance variables. It is possible to do so in Python by adding additional parameters to the constructor. Go ahead and change the definition of the constructor in class Car to the following version:

def __init__(self, owner = 'UNKNOWN', color = 'UNKNOWN', currentSpeed = 0, lightsOn = False): 
	self.owner = owner 
	self.color = color 
	self.currentSpeed = currentSpeed 
	self.lightsOn = lightsOn
    

Please note that we here used identical names for the instance variables and corresponding parameters of the constructor used for providing the initial values. However, these are still distinguishable because instance variables always have the prefix “self.”. In this new version of the constructor we are using keyword arguments for each of the properties to provide maximal flexibility to the user of the class. The user can now use any combination of providing their own initial values or using the default values for these properties. Here is how to re-create Sue’s car by providing values for all the properties:

carOfSue = Car(owner='Sue', color='white', currentSpeed = 41, lightsOn = True) 
carOfSue.printInfo()
Output: 

Car with owner = Sue, color = white, currentSpeed = 41, lightsOn = True

Here is a version in which we only specify the owner and the speed. Surely you can guess what the output will look like.

carOfSue = Car(owner='Sue', currentSpeed = 41) 
carOfSue.printInfo() 

In addition to __init__(…) for the constructor, there is another special method called __str__(). This method is called by Python when you either explicitly convert an object from that class to a string using the Python str(…) function or implicitly, e.g. when printing out the object with print(…). Try out the following two commands for Sue’s car and see what output you get:

print(str(carOfSue)) 
print(carOfSue)

Now add the following method to the definition of class Car:

	def __str__(self): 
            return 'Car with owner = {0}, color = {1}, currentSpeed = {2}, lightsOn = {3}'.format(self.owner, self.color, self.currentSpeed, self.lightsOn)

Now repeat the two commands from above and look at the difference. The output should now be the following line repeated twice:

Car with owner = Sue, color = UNKNOWN, currentSpeed = 41, lightsOn = False

For implementing the method, we simply used the same string that we were printing out from the printInfo() method. In principal, this method is not really needed anymore now and could be removed from the class definition.

Objects can be used like any other value in Python code. Actually, everything in Python is an object, even primitive data types like numbers and Boolean values. That means we can …

  • use objects as parameters of functions and methods (you will see an example of this with the function stopCar(…) defined below),
  • return objects as the return value of a function or method,
  • store objects inside sequences or containers, for instance in lists like this: carList = [ carOfTom, carOfSue, Car(owner = 'Mike'],
  • store objects in instance variables of other objects.

To illustrate this last point, we can add another class to our car example, one for representing car manufacturers:

class Manufacturer(): 
     def __init__(self, name): 
          self.name = name

Usually such a class would be much more complex, containing additional properties for describing a concrete car manufacturer. But we keep things very simple here and say that the only property is the name of the manufacturer. We now modify the beginning of the definition of class Car so that another instance variable is created called self.manufacturer. This is used for storing an object of class Manufacturer inside each Car object for representing the manufacturer of that particular car. For parameters that are objects of classes, it is common to use the special value None as the default value when the parameter is not provided.

class Car(): 

     def __init__(self, manufacturer = None, owner = 'UNKNOWN', color = 'UNKNOWN', currentSpeed = 0, lightsOn = False): 
        self.manufacturer = manufacturer 
        self.owner = owner 
        self.color =  color 
        self.currentSpeed = currentSpeed 
        self.lightsOn = lightsOn

The rest of the class definition can stay the same although we would typically change the __str__(...) method to include this new instance variable. The following code shows how to create a new Car object by first creating a Manufacturer object with name 'Chrysler'. This object could also come from a predefined list or dictionary of car manufacturer objects if we want to be able to use the same Manufacturer object for several cars. Then we use this object for the manufacturer keyword argument of the Car constructor. As a result, this object gets assigned to the manufacturer instance variable of the car as reflected by the output from the final print statement.

m = Manufacturer('Chrysler') 
carOfFrank = Car(manufacturer = m, owner = 'Frank', currentSpeed = 70) 
print(carOfFrank.manufacturer.name)
Output: 
Chrysler

Note how in the last line of the example above, we chain things together via dots starting from the variable containing the car object (carOfFrank), followed by the name of an instance variable (manufacturer) of class Car, followed by the name of an instance variable of class Manufacturer (name): carOfFrank.manufacturer.name . This is also something you have probably seen before, for instance as “describeObject.SpatialReference.Name” when accessing the name of the spatial reference object that is stored inside an arcpy Describe object.

We briefly discussed in Section 4.2 when talking about collections that when defining our own classes we may have to provide definitions of comparison operators like == and < for them to work as we wish when placed into a collection. So a question for instance would be, when should two car objects be considered to be equal? We could take the standpoint that they are equal if the values of all instance variables are equal. Or it could make sense for a particular application to define that two Car objects are equal if the name of the owner and the manufacturer are equal. If our instance variables would include the license plate number that would obviously make for a much better criterion. Similarly, let us say we want to keep our Car objects in a priority queue sorted by their current speed values. In that case, we need to define the < comparison operator so that car A < car B holds if the value of the currentSpeed variable of A is smaller than that of B.

The meaning of the == operator is defined via a special method called __eq__(…) for “equal”, while that of the < operator is defined in a special method called __lt__(…) for “less than”. The following code example extends the most recent version of our class Car with a definition of the __eq__(…) method based on the idea that cars should be treated as equal if owner and manufacturer are equal. It then uses a Python list with a single car object and another car object with the same owner and manufacturer but different speed to illustrate that the new definition works as intended for the list operations “in” and index(…).

class Car(): 

    … # just add the method below to the previous definition of the class 

    def __eq__(self, otherCar): 
        return self.owner == otherCar.owner and self.manufacturer == otherCar.manufacturer 

m = 'Chrysler' 
carList = [ Car(owner='Sue', currentSpeed = 41, manufacturer = m) ] 
car = Car(owner='Sue', currentSpeed = 0, manufacturer = m) 

if car in carList: 
    print('Already contained in the list') 
print(carList.index(car))
Output: 
Already contained in the list 
0

Note that __eq__(…) takes another Car object as parameter and then simply compares the values of the owner and manufacturer instance variables of the Car object the method was called for with the corresponding values of that other Car object. The output shows that Python considers the car to be already located in the list as the first element, even though these are actually two different car objects with different speed values. This is because these operations use the new definition of the == operator for objects of our class Car that we provided with the method __eq__(...).

You now know the basics of writing own classes in Python and how to instantiate them and use the created objects. To wrap up this section, let’s come back to a topic that we already discussed in Section 1.4 of Lesson 1. Do you remember the difference between mutable and immutable objects when given as a parameter to functions? Mutable objects like lists used as parameters can be changed within the function. All objects that we create from classes are also mutable, so you can in principle write code like this:

def stopCar(car): 
	car.currentSpeed = 0 

stopCar(carOfFrank) 
print(carOfFrank)

When stopCar(...) is called, the parameter car will refer to the same car object that variable carOfFrank is referring to. Therefore, all changes made to that object inside the function referring to variable car will be reflected by the final print statement for carOfFrank showing a speed of 0. What we have not discussed so far is that there is a second situation where this is important, namely when making an assignment. You may think that when you write something like

anotherCar = carOfFrank 

a new variable will be created and a copy of the car object in variable carOfFrank will be assigned to that variable so that you can make changes to the instance variables of that object without changing the object in carOfFrank. However, that is only how it works for immutable values. Instead, after the assignment, both variables will refer to the same Car object in memory. Therefore, when you add the following commands

anotherCar.color = 'green' 
anotherCar.changeCurrentSpeed(12) 
print(carOfFrank)

The output will be:

Car with owner = Frank, color = green, currentSpeed = 12, lightsOn = False

It works in the same way for all mutable objects, so also for lists for example. If you want to create an independent copy of a mutable object, the module copy from the Python standard library contains the functions copy(…) and deepcopy(…) to explicitly create copies. The difference between the two functions is explained in the documentation and only plays a role when the object to be copied contains other objects, e.g. if you want to make a copy of a list of Car objects.