Lecture 17

Review

  1. What is self? Why use it?
  2. What is __init__? Why use it?

Classes

"Private" Members and Set/Get Methods

Often, we can't trust users to do the right thing. Thus, with classes, we often engage in data hiding. Data hiding is when we hide fields from the user and force them to use methods to access and modify fields. These methods can then validate the values coming in from the user. They also help to ensure data abstraction and encapsulation. By default, all members are publicly-accessible, that is they are accessible both in the class and outside of the class. Private members are those that are only directly-accessible inside the class (i.e. the members are hidden).

To hide a field (or method) from the user, start the member's name with two underscores. This causes Python to secretly mangle the name so that it isn't easily accessible outside of the class. Inside the class, you can still access the member using its name (including the two underscores).

For example, we want an age field in our Person class, but it only makes sense for age to be a positive number (maybe even just a positive int). So, we don't want the age field to be publicly accessible (why not?). To make age private, we name it __age. See the example below:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

The user of the Person class can still access name directly but they can't access age:

>>> joel = Person('Joel', 65)
>>> print('name =', joel.name)
name = 'Joel'
>>>
>>> print('age =', joel.__age)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute '__age'
>>>
>>> print('age =', joel.age)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'age'

If you want to allow the user to access your private fields, we need to write set and get methods (also called setters and getters or mutators and accessors). A get method gets the value of a field. A set method sets, modifies, or mutates the value of a field. Traditionally, accessors follow the naming convention "get_field_name" and mutators follow the naming convention "set_field_name". For example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.set_age(age)
    
    def set_age(self, age):
        if not isinstance(age, int):
            raise TypeError('age must be a positive int')
        elif age < 0:
            raise ValueError('age must be a positive int')
        
        self.__age = age
    
    def get_age(self):
        return self.__age

Now, if the user wants to access age, they can use the set_age and get_age methods. Notice that the set_age method also validates the age, ensuring that it is a positive int. Notice also that the __init__ method now calls the set_age method instead of directly setting __age. Why do you think this change was made?

Your setter should do validation of the value before doing the assignment. In many cases, your setter and getter methods should save and return copies of the fields and not the originals so that the field's value can only be modified through the setter. We'll talk more about this when we cover data aggregatioin.

In many other programming languages, it is strongly recommended to make all fields private. Python is more lax about that, but it is still a good idea to make your fields private.

Property

With some fields public (e.g. name in the Person class) and other fields private (e.g. age in the Person class), accessing fields becomes inconsistent. The programmer will need to memorize which fields they can access directly and which fields they must use methods for.

One solution to this problem is to provide set/get methods for all fields. A lot of programming languages take this approach (and also say that almost all fields should be private). This is an option for Python as well. In our Person class, we could do:

class Person:
    def __init__(self, name, age):
        self.set_name(name)
        self.set_age(age)
    
    def set_name(self, name):
        self.__name = name
    
    def get_name(self):
        return self.__name
    
    def set_age(self, age):
        if not isinstance(age, int):
            raise TypeError('age must be a positive int')
        elif age < 0:
            raise ValueError('age must be a positive int')
        
        self.__age = age
    
    def get_age(self):
        return self.__age

In the code above, notice that name was changed to __name, set/get methods were written, and set_name is used in the __init__ method.

While this solves our problem of inconsistent access to fields, it has the unfortunately effect of causing the user to type more to access a field. For example, now the user has to type:

joel.set_name('Joel')

instead of the shorter option of:

joel.name = 'Joel'

Python offers an alternative to this first solution: create properties. Properties are basically pseudo-fields. The user can treat them like a field, but secretly the user is interacting with methods. To create a property, first write set and get methods. Then, use the property function to create the property by passing in the get and set methods (do not call these methods when passing them in). The property function returns the pseudo-field, so store it into a field with the name you want to pseudo-field to have. For example:

class Person:
    def __init__(self, name, age):
        self.set_name(name)
        self.set_age(age)
    
    def set_name(self, name):
        self.__name = name
    
    def get_name(self):
        return self.__name
    
    name = property(get_name, set_name)
    
    def set_age(self, age):
        if not isinstance(age, int):
            raise TypeError('age must be a positive int')
        elif age < 0:
            raise ValueError('age must be a positive int')
        
        self.__age = age
    
    def get_age(self):
        return self.__age
    
    age = property(get_age, set_age)

To now use these properties, treat them just like a field:

>>> joel = Person('Joel', 65)
>>> print('name =', joel.name)
name = Joel
>>> print('age =', joel.age)
age = 65
>>> joel.age = 66
>>> print('age =', joel.age)
age = 66
>>> joel.age = -1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: age must be a positive int

There are other things you can do with properties, such as provide a del method and provide documentation. We won't cover these features. However, the last thing to cover with properties is that you can use them to make constant fields. Constant fields are fields that do not change their values. To create a constant field, just create a get method, then just pass in the get method to the property function. In the example below, two constant fields are created: birthday and home_planet and some other changes are made to support those fields (all new code is bolded and red):

class Person:
    def __init__(self, name, age, birthday):
        self.set_name(name)
        self.set_age(age)
        self.__set_birthday(birthday)
    
    def set_name(self, name):
        self.__name = name
    
    def get_name(self):
        return self.__name
    
    name = property(get_name, set_name)
    
    def set_age(self, age):
        if not isinstance(age, int):
            raise TypeError('age must be a positive int')
        elif age < 0:
            raise ValueError('age must be a positive int')
        
        self.__age = age
    
    def get_age(self):
        return self.__age
    
    age = property(get_age, set_age)
    
    def __set_birthday(self, birthday):
        #some validation code to ensure birthday is a valid date should be here
        self.__birthday = birthday
    
    def get_birthday(self):
        return self.__birthday
    
    birthday = property(get_birthday)
    
    def get_homeplanet(self):
        return 'Earth'
    
    home_planet = property(get_homeplanet)

Notice that:

Special Methods

Python provides a long list of special methods for classes. These special methods make working with classes a lot easier. One example is the __init__ method we saw above. There are many others. Some allow us to use common operators (e.g. ==, >, [index]), others perform helpful actions (e.g. when the object is created or destroyed), others help other actions work (such as using an object in a dictionary or set), or just letting us use standard Python syntax to accomplish what we want.

For now, we will look at just one other: __eq__. Lab 8 will give you practice with another special method (the __str__ method). The __eq__ is used to compare two objects for equality. It is called when the equality operator (==) is used.

How two objects are "equal" depends on the problems you're trying to solve and the situation you're working on. How do you determine whether two people are the same? Are two people the same if they have the same name? Are they the same if they have the same name and same birthdate? For our example, we will assume two people are the same if they have the same name and birthdate.

In the code below, the additions are in bold red.

class Person:
    def __init__(self, name, age, birthday):
        self.set_name(name)
        self.set_age(age)
        self.__set_birthday(birthday)
    
    def set_name(self, name):
        self.__name = name
    
    def get_name(self):
        return self.__name
    
    name = property(get_name, set_name)
    
    def set_age(self, age):
        if not isinstance(age, int):
            raise TypeError('age must be a positive int')
        elif age < 0:
            raise ValueError('age must be a positive int')
        
        self.__age = age
    
    def get_age(self):
        return self.__age
    
    age = property(get_age, set_age)
    
    def __set_birthday(self, birthday):
        #some validation code to ensure birthday is a valid date should be here
        self.__birthday = birthday
    
    def get_birthday(self):
        return self.__birthday
    
    birthday = property(get_birthday)
    
    def get_homeplanet(self):
        return 'Earth'
    
    home_planet = property(get_homeplanet)
    
    def __eq__(self, other):
        if self.name == other.name and self.birthday == other.birthday and self.age == other.age:
            return True
        else:
            return False

The __eq__ method must return a True or False value.

Python will automatically call this method when a programmer tries to compare a Person object with something:

>>> alice = Person('Alice', 32, 'November 4')
>>> bob = Person('Bob', 29, 'July 11')
>>> if alice == bob:
        print('The two people are the same.')
<< Previous Notes Daily Schedule Next Notes >>