Classes and Objects (2)

In today’s lecture, we will continue our discussion of object oriented programming in Python.

class Book:
    """This class represents a book with attributes title, author, and year"""
    # attributes
    # _ indicates that they are protected
    __slots__ = ['_title', '_author', '_year']  
    # __init__ is automatically called when we create new Book objects
    # we set the intial values of our attributes in __init__
    # (optional) default values can also be provided 
    def __init__(self, bookTitle="", bookAuthor="", bookYear=0):
        self._title = bookTitle
        self._author = bookAuthor
        self._year = int(bookYear)
    # accessor (getter) methods
    def getTitle(self):
        return self._title

    def getAuthor(self):
        return self._author
    def getYear(self):
        return self._year
    # mutator (setter) methods
    def setTitle(self, bookTitle):
        self._title = bookTitle
    def setAuthor(self, bookAuthor):
        self._author = bookAuthor
    def setYear(self, bookYear):
        self._year = int(bookYear)
    # methods for manipulating Books
    def numWordsInTitle(self):
        """Returns the number of words in title of book"""
        return len(self._title.split())
    def sameAuthorAs(self, otherBook):
        """Check if self and otherBook have same author"""
        return self._author == otherBook.getAuthor()
    def yearsSincePub(self, currentYear):
        """Returns the number of years since book was published"""
        return currentYear - self._year
    # __str__ is used to generate a meaningful string representation for Book objects
    # __str__ is automatically called when we ask to print() a Book object
    def __str__(self):
        return "'{}', by {}, in {}".format(self._title, self._author, self._year)
# creating book objects:
pp = Book('Pride and Prejudice', 'Jane Austen', 1813)
emma = Book('Emma', 'Jane Austen', 1815)
ps = Book("Parable of the Sower", "Octavia Butler", 1993)
# we can access (non-private) attributes directly using dot notation
# (but this is not the preferred way!)
'Parable of the Sower'
# invoke the accessor method (preferred way to access attributes)
'Pride and Prejudice'
'Jane Austen'
# invoke the mutator methods
# verify that our update to year attribute in the previous line worked
# invoke Book methods on specify books
# test our __str__ method bring printing a specific Book instance
'Parable of the Sower', by Octavia Butler, in 1991

Data Hiding via Attribute Types

When we create attributes of a class, we must decide what level of access “users” of the class should have. Some OOP languages strictly enforce these distinctions. Python uses the following special naming conventions to “signal the attribute type”:

  • Private (prefixed with __): these attributes should only be used inside of the class definition. These attributes are strictly private and essentially invisible from outside the class.

  • Protected (prefixed with _): these attributes can be used from outside the class but only in subclasses.

  • Public (no underscore prefix): these attributes can and should be freely used anywhere.

class TestingAttributes():
    __slots__ = ['__val', '_val', 'val']
    def __init__(self):
        self.__val = "I am strictly private."
        self._val = "I am private but accessible from outside."
        self.val = "I am public."
a = TestingAttributes()
'I am public.'
'I am private but accessible from outside.'
AttributeError                            Traceback (most recent call last)
Input In [19], in <cell line: 1>()
----> 1 a.__val

AttributeError: 'TestingAttributes' object has no attribute '__val'

String Representation of a Class

Printing objects is often useful for debugging. For built-in objects in Python, such as lists, dictionaries, etc, Python knows how to print the contents of these objects in a useful way. Unfortunately, this is not true for objects that we define in our own classes.

class TestingPrint():
    __slots__ = ['_attr']

    def __init__(self, value):
        self._attr = value
test = TestingPrint("testing")
<__main__.TestingPrint object at 0x105adea30>

Another Example: Name class

In this example, we create a Name class that represents names, including a first, middle, and last name. This scenario illustrates a good reason to specify default parameter values in __init__. Also, note that we do not define mutator methods in this case since a person’s name cannot change (usually). Finally, we can choose how these names are printed in __str__.

class Name:
    """Class to represent a person's name."""
    __slots__ = ['_first', '_mid', '_last']
    # since middle names are optional, we can define a default value
    def __init__(self, firstName, lastName, middleName=''):
        self._first = firstName
        self._mid = middleName
        self._last = lastName

    # accessor methods for attributes
    def getFirst(self):
        return self._first
    def getMiddle(self):
        return self._mid

    def getLast(self):
        return self._last

    def __str__(self):
        # if the person has a middle name
        if len(self.getMiddle()):  
            return '{}. {}. {}'.format(self.getFirst()[0], self.getMiddle()[0], self.getLast()) 
            return '{}. {}'.format(self.getFirst()[0], self.getLast())
n1 = Name('Iris', 'Howley')
n2 = Name('Jeannie', 'Albrecht', 'Raye')
I. Howley
J. R. Albrecht

Notice Even though we can print the object now, if you ask about the name object in interactive python, it still gives something that is not human readable. (It’s harder to see this in a script though.)

<__main__.Name at 0x1059b09c0>


Today we saw how Python supports data abstraction (separating the data and details of the implementation from the user) via :

  • Data hiding: via attribute naming conventions

  • Encapsulation: bundling together of data and methods that provide an interface to the data (accessor and mutator methods)