Lecture 14/16: Introduction to Classes

In today's lecture we will introduces user-defined types in Python called classes and formalize the notion of objects.

Everything in Python in an Object

Python supports many different kinds of data

  • 1234 (int), 3.14 (float), "Hello" (str)
  • [1, 5, 7, 11, 13] (list), {"CA": "California", "MA": "Massachusetts"} (dict)

Each of these is an object, and every object has:

  • a type
  • an internal data representation (primitive or composite)
  • a set of functions for interaction with the object

An object is an instance of a type

  • 1234 is an instance of an int
  • "hello" is an instance of a str

Different between Jupyter Notebook and interactive Python. The outputs to type queries appear different between Jupyter notebook and interactive python, which displays the word class. For example:
>>> type(1234)
<class 'int'>
>>>

In [1]:
type(1234)
Out[1]:
int
In [2]:
type("hello")
Out[2]:
str
In [3]:
type([1, 5, 7, 11, 13])
Out[3]:
list
In [4]:
type({"CA": "California", "MA": "Massachusetts"})
Out[4]:
dict
In [5]:
type(range(5))
Out[5]:
range
In [6]:
def greeting():
    print("Hello")
In [7]:
type(greeting)
Out[7]:
function

Defining Our Own Type: Classes

We can create our own type by defining our own class. Creating a class involves: defining the class name and defining its attributes. Using the class involves: creating new instances (objects) and doing operations on the instances.

Let us start by defining a simple class Book.

In [8]:
class Book:
    """This class represents a book"""
    pass

Creating an instance of the class. We can create an instance (or an object) of the class Book as follows.

In [9]:
b1 = Book()
In [10]:
b1
Out[10]:
<__main__.Book at 0x1130b0790>
In [11]:
type(b1)
Out[11]:
__main__.Book

Each instance is different. If we create a new instance b2 of class Book, it is a different object than b1.

In [12]:
b2 = Book()
In [13]:
b2 == b1
Out[13]:
False

Data Attributes or Instance Variables

Data attributes (a Pythonic term) or instance variables store data that "belong" to the instances of the class.

For example, an instance of a class Book must have associated information about it, such as name for the name of the book, author for name of the author, etc. Such information is associated with all instances of the class by defining them as the data attributes in the class definition.

Warm up: Improper Approach for Assigning Attributes

We can assign attributes directly to an instance of the class (outside the class definition) but this is not recommended and should not be done. Let us take an example of the not the proper approach and then show describe the approach that should be used instead.

In [14]:
b1.name = "Emma"
b1.author = "Jane Austen"
In [15]:
b1.name
Out[15]:
'Emma'
In [16]:
b2.name # will this work?
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-61fe72925b2a> in <module>
----> 1 b2.name # will this work?

AttributeError: 'Book' object has no attribute 'name'
In [17]:
b2.name = "Harry Potter"
In [18]:
b2.name
Out[18]:
'Harry Potter'
In [19]:
b2.author = "J.K. Rowling"
In [20]:
b2.author
Out[20]:
'J.K. Rowling'

Methods or Procedural Attributes

Methods or procedural attributes are functions that are defined as part of the class definition and describe how to interact with the class objects. For example, we now how we can modify a list object L by using methods like append, extend,etc.

How are these methods called? We call the append method on the list object L, by writing L.append(4). Methods of the classes we define are called in a similar way.

In [21]:
L = list()
In [22]:
L.extend([1,2,3])
In [23]:
L
Out[23]:
[1, 2, 3]
In [24]:
L.append(4)
In [25]:
L
Out[25]:
[1, 2, 3, 4]

First Method. Lets define a toy class with a simple method that prints "Hello" and see how we can call the method on an instance of the class.

In [26]:
class A:
    """Class to test the use of methods"""
    def greeting(self):
        print("Hello")
In [27]:
greeting()
Hello
In [28]:
a = A()
In [29]:
a.greeting()
Hello

Digging Deeper: Self

Lets try to understand what the purpose of the parameter self by using the python function id().

Recall that id(obj) displays the unique identifier to the object. You can think of this number as the address in memory this object is stored.

In [30]:
num = 42
In [31]:
id(num)
Out[31]:
4564567088
In [32]:
listA = listB = [1,2,3]
In [33]:
id(listA) == id(listB)
Out[33]:
True

Let us rewrite class A to print the id of self.

In [34]:
class A:
    """Class to test the use of methods"""
    def greeting(self):
        print("Hello, object with ID", id(self))
In [35]:
obj = A()
In [36]:
obj.greeting()
Hello, object with ID 4615231568
In [37]:
id(obj)
Out[37]:
4615231568
In [38]:
A.greeting(obj)
Hello, object with ID 4615231568
In [39]:
L = [1,2,3,4]
In [40]:
list.append(L, 5)
In [41]:
L
Out[41]:
[1, 2, 3, 4, 5]

Summary. Methods differs from a function in two aspects:

  • They are object-specific functions, defined within a class
  • The first parameter in the definition of a method has to be a reference to the calling instance. This parameter is called "self" by convention.

The following two calls are equivalent: A.greeting(obj) and obj.greeting().

Initializing a Class with Attributes

While Python allows you to assign attributes to instances of a class on the fly (and outside the class), it is not the proper way to do so.

The data attributes (instance variables) should be part of the class definition. We can achieve this by the Python's special method __init__.

__init__ Special method that lets us define how to create an instance of a class. In particular, lets us initialize some data attributes of the class.

In [42]:
class TestInit:
    """This class will test when __init__ is called"""
    def __init__(self):
        print("__init__ is called")
In [43]:
obj = TestInit()
__init__ is called

Thus, the special method __init__ is called automatically when we create a new instance of the class.

Let us use it now to properly initialize our class Book with attributes name and author.

In [44]:
class Book:
    """This class represents a book"""
    def __init__(self, name=None, author=None):
        self.name = name
        self.author = author
In [45]:
emma = Book('Emma', 'Jane Austen')
In [46]:
emma.name
Out[46]:
'Emma'
In [47]:
emma.author
Out[47]:
'Jane Austen'
In [48]:
hp = Book('Harry Potter')
In [49]:
hp.name
Out[49]:
'Harry Potter'
In [50]:
print(hp.author)
None

Avoid Dynamically Created Attributes

While Python allows you to assign attributes to instances of a class on the fly outside the class, this functionality is not ideal.

In [51]:
class Book:
    """This class represents a book"""
    def __init__(self, name=None, author=None):
        self.name = name
        self.author = author
In [52]:
b = Book('Emma', 'Jane Austen')
In [53]:
b.year = 1815
In [54]:
b.year
Out[54]:
1815
  • Attributes of objects are stored in a dictionary __dict__
  • Like any other dictionary, you can add items to __dict__ on the fly and there are no predetermined set of keys
  • This is why we can dynamically add attributes to objects (even though this is not recommended)
In [55]:
b.__dict__
Out[55]:
{'name': 'Emma', 'author': 'Jane Austen', 'year': 1815}
In [56]:
b.ref = 9999777
In [57]:
b.__dict__
Out[57]:
{'name': 'Emma', 'author': 'Jane Austen', 'year': 1815, 'ref': 9999777}

Fix Attributes via __slots__

  • Dynamic creation and assignment of attributes is not desirable
  • Special variable __slots__ provide a clean way to work around this space consumption problem: instead of having a dynamic dict, slots provide a static structure which prohibits addition of attributes
In [58]:
class Book:
    """This class represents a book"""
    __slots__ = ['name', 'author']
    def __init__(self, name=None, author=None):
        self.name = name
        self.author = author
In [59]:
b = Book('Emma', 'Jane Austen')
In [60]:
b.name
Out[60]:
'Emma'
In [61]:
b.author
Out[61]:
'Jane Austen'
In [62]:
b.__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-62-395029f23f31> in <module>
----> 1 b.__dict__

AttributeError: 'Book' object has no attribute '__dict__'
In [63]:
b.year = 1815
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-63-58a49885b6e1> in <module>
----> 1 b.year = 1815

AttributeError: 'Book' object has no attribute 'year'

Writing More Methods for our Book Class

We could define other methods as part of the class definition.

For example, we define the following methods:

  • numWordsName that returns the number of words in the name of the book
  • sameAuthorAs that takes another book object as parameter and checks if the two books have the same author or not.
  • yearSincePub that takes in the current year and returns the number of years since the book was published.
In [64]:
class Book:
    """This class represents a book with
       attributes name, author and year"""
    __slots__ = ['name', 'author','year']  #attributes
    def __init__(self, name=None, author=None, year=None):
        self.name = name
        self.author = author
        self.year = year
    def numWordsName(self):
        """Returns the number of words in name of book"""
        return len(self.name.split())
    def sameAuthorAs(self, other):
        """Check if self and other have same author"""
        if self.author == other.author:
            return True
        return False
    def yearsSincePub(self, currentYear):
        """Returns the number of years since book was published"""
        return currentYear - self.year
In [65]:
# creating book objects:
pp = Book('Pride and Prejudice', 'Jane Austen', 1813)
emma = Book('Emma', 'Jane Austen', 1815)
hp = Book("Harry Potter and the Sorcerer's Stone", "J.K. Rowling", 1997)
In [66]:
pp.sameAuthorAs(emma)
Out[66]:
True
In [67]:
pp.sameAuthorAs(hp)
Out[67]:
False
In [68]:
hp.numWordsName()
Out[68]:
6
In [69]:
emma.yearsSincePub(2020)
Out[69]:
205
In [70]:
hp.yearsSincePub(2020)
Out[70]:
23