Everything is an Object, Python OOP primer

Posted by Bob on Tue 24 January 2017 in Concepts • 13 min read

I created this notebook on OOP (object oriented programming) in Python.

Basics: classes, instances, class vs instance variables

In [1]:
class Blog(object):
    """Good practice (py3) is to explicitly inherit from object"""

    # class variable = shared among instances
    num_blogs = 0

    def __init__(self, name, bio):
        """The constructor, gets called implicitly when instantiating a new object (next cell)"""
        self.name = name
        self.bio = bio
        Blog.num_blogs += 1  # we access a class variable like this (or use a class method)
    
    def get_articles(self, rss):
        """Get all articles from RSS"""
        print('Articles {}: ...'.format(rss))

    def post_article(self, title):
        """Add a new article"""
        print('Posted new article: {}'.format(title))
    
    def __str__(self):
        """Informal or nicely printable string representation of an object"""
        return '{}: {}'.format(self.name, self.bio)
In [2]:
name = 'PyBites'
bio = 'Python code challenges, tutorials and news, one bite a day'
blog = Blog(name, bio)
In [3]:
blog
Out[3]:
<__main__.Blog at 0x104400cf8>
In [4]:
print(blog)  # invokes __str__
PyBites: Python code challenges, tutorials and news, one bite a day
In [5]:
blog.get_articles('http://pybit.es/feeds/all.rss.xml')
Articles http://pybit.es/feeds/all.rss.xml: ...
In [6]:
blog.post_article('OOP fun with PyBites')
Posted new article: OOP fun with PyBites
In [7]:
print(blog.num_blogs)
1
In [8]:
blog2 = Blog('bobcodesit', 'my personal blog')
In [9]:
blog2
Out[9]:
<__main__.Blog at 0x10441b400>
In [10]:
blog == blog2
Out[10]:
False
In [11]:
print(blog.num_blogs)
2

Abstract base classes (ABC's)

Let's create some developers to add to a blog.

ABC's lets you force derived classes to implement certain behaviors / methods (tip from Dan Bader)

In [12]:
from abc import ABCMeta, abstractmethod

class Developer(metaclass=ABCMeta):
    
    @abstractmethod
    def get_post_days(self):
        """Classes that inherit from Developer need to implement this method"""
        pass
    
    def __str__(self):
        return self.__class__.__name__
In [14]:
class Julian(Developer):
    pass

# oops, did not implement get_post_days()
jul = Julian()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
      3 
      4 # oops, did not implement get_post_days()
----> 5 jul = Julian()

TypeError: Can't instantiate abstract class Julian with abstract methods get_post_days
In [15]:
class Julian(Developer):
    
    def get_post_days(self):
        return 'Tue Wed'.split()

# ok
julian = Julian()
In [16]:
class Bob(Developer):
    
    def get_post_days(self):
        return 'Wed Thurs'.split()
    
bob = Bob()
In [17]:
class PyBites(Developer):
    
    def get_post_days(self):
        return 'Mon Fri Sat'.split()

pybites = PyBites()

Inheritance and using objects inside objects

In [18]:
class PyBitesBlog(Blog):
    """PyBitesBlog inherits from parent class Blog"""
    
    def __init__(self, *args):
        """We get a variable-length argument list of developers"""
        self.name = self.__class__.__name__  # how to get name of class 'PyBitesBlog' as string
        self.bio = 'Python code challenges, tutorials and news, one bite a day'
        self.developers = args
        # pass name and bio to the parent class
        # py2 requires longer syntax: super(PyBitesBlog, self).__init__
        super().__init__(self.name, self.bio)
        
    def schedule(self):
        """Loop over all developer objects calling their get_post_days method"""
        for dev in self.developers:
            print('{}: {}'.format(dev, ', '.join(dev.get_post_days())))
            
    def __str__(self):
        """Use the parent __str__ adding something extra"""
        return super().__str__() + '\nAuthors: ' + ', '.join([str(dev) for dev in self.developers])
In [19]:
pyblog = PyBitesBlog(julian, bob, pybites)
In [20]:
print(pyblog)  # again call object's __str__
PyBitesBlog: Python code challenges, tutorials and news, one bite a day
Authors: Julian, Bob, PyBites
In [21]:
pyblog.get_articles('http://pybit.es/feeds/all.rss.xml')  # inherited method from parent
Articles http://pybit.es/feeds/all.rss.xml: ...
In [22]:
pyblog.schedule()
Julian: Tue, Wed
Bob: Wed, Thurs
PyBites: Mon, Fri, Sat

and this is ...

Polymorphism

We call get_post_days() on every developer (instance), each one giving a different result.

Good examples from stdlib are len (= __len__()) and + (= __add__()): they just do what you expect them to do for each type (as long as compatible):

In [23]:
s = 'a string'
t = (0, 1)
d = dict(zip([1,2,3], ['a','b','c']))
In [24]:
for var in (s, t, d): 
    print('len of {} (type {}) = {}'.format(var, type(var), len(var)))
len of a string (type ) = 8
len of (0, 1) (type ) = 2
len of {1: 'a', 2: 'b', 3: 'c'} (type ) = 3
In [25]:
s = ('python', '123')
i = (3, 6)
lst = [1, 2, 3], [4, 5]

for var in (s, i, lst):
    res = var[0] + var[1]
    print('calling + on {} yields {} (type {})'.format(var, res, type(res)))
calling + on ('python', '123') yields python123 (type )
calling + on (3, 6) yields 9 (type )
calling + on ([1, 2, 3], [4, 5]) yields [1, 2, 3, 4, 5] (type )
In [26]:
# ok cannot add str + int
'a' + 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
      1 # ok cannot add str + int
----> 2 'a' + 1

TypeError: Can't convert 'int' object to str implicitly
In [27]:
# but this works
'a' + str(1)
Out[27]:
'a1'

Encapsulation

http://stackoverflow.com/questions/26216563/how-to-do-encapsulation-in-python

Python has encapsulation - you are using it in your class.

What it doesn't have is access control such as private and protected attributes. However, in Python, there is an attribute naming convention to denote private attributes by prefixing the attribute with one or two underscores, e.g:

self._a self.__a A single underscore indicates to the user of a class that an attribute should be considered private to the class, and should not be accessed directly.

A double underscore indicates the same, however, Python will mangle the attribute name somewhat to attempt to hide it.

If you want getter / setter behavior look at the @property decorator:

http://stackabuse.com/python-properties/

It is often considered best practice to create getters and setters for a class's public properties. Many languages allow you to implement this in different ways, either by using a function (like person.getName()), or by using a language-specific get or set construct. In Python, it is done using @property.


See an error in this post? Please submit a pull request on Github.

Join our community and grab our Become a Better Python Developer cheat sheet. Learn Python. Receive bonus material. Challenge yourself!