Python Fundamentals Tutorial: Object-Oriented Programming

16. Object-Oriented Programming

16.1. Classes

A class is defined in Python using the class statement. The syntax of this statement is class <ClassName>(superclass). In the absence of anything else, the superclass should always be object, the root of all classes in Python.

[Note]Note

object is technically the root of "new-style" classes in Python, but new-style classes today are as good as being the only style of classes.

Here is a basic class definition for a class with one method. There are a few things to note about this method:

  1. The single argument of the method is self, which is a reference to the object instance upon which the method is called, is explicitly listed as the first argument of the method. In the example, that instance is a. This object is commonly referred to as the "bound instance."
  2. However, when the method is called, the self argument is inserted implicitly by the interpreter — it does not have to be passed by the caller.
  3. The attribute __class__ of a is a reference to the class object A
  4. The attribute __name__ of the class object is a string representing the name, as given in the class definition.

Also notice that "calling" the class object (A) produces a newly instantiated object of that type (assigned to a in this example). You can think of the class object as a factory that creates objects and gives them the behavior described by the class definition.

>>> class A(object):
...     def whoami(self):
...         return self.__class__.__name__
...

>>> a = A()

>>> a
<__main__.A object at 0x100425d90>

>>> a.whoami()
'A'

The most commonly used special method of classes is the __init__() method, which is an initializer for the object. The arguments to this method are passed in the call to the class object.

Notice also that the arguments are stored as object attributes, but those attributes are not defined anywhere before the initializer.

Attempting to instantiate an object of this class without those arguments will fail.

>>> class Song(object):
...     def __init__(self, title, artist):
...         self.title = title
...         self.artist = artist
...     def get_title(self):
...         return self.title
...     def get_artist(self):
...         return self.artist
...

>>> unknown = Song()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 3 arguments (1 given)

Notice again that one argument was actually provided (self) and only title and artist are considered missing.

So, calling Song properly gives an instance of Song with an artist and title. Calling the get_title() method returns the title, but so does just referencing the title attribute. It is also possible to directly write the instance attribute. Using boilerplate getter / setter methods is generally considered unnecessary. There are ways to create encapsulation that will be covered later.

>>> leave = Song('Leave', 'Glen Hansard')

>>> leave
<__main__.Song object at 0x100431050>

>>> leave.get_title()
'Leave'

>>> leave.title
'Leave'

>>> leave.title = 'Please Leave'

>>> leave.title
'Please Leave'

One mechanism that can be utilized to create some data privacy is a preceding double-underscore on attribute names. However, it is possible to find and manipulate these variables if desired, because this approach simply mangles the attribute name with the class name. The goal in this mangling is to prevent clashing between "private" attributes of classes and "private" attributes of their superclasses.

>>> class Song(object):
...     def __init__(self, title, artist):
...         self.__title = title
...         self.__artist = artist
...     def get_title(self):
...         return self.__title
...     def get_artist(self):
...         return self.__artist
...
>>> leave = Song('Leave', 'Glen Hansard')

>>> leave.get_title()
'Leave'

>>> leave.__title
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Song' object has no attribute '__title'

>>> leave._Song__title
'Leave'

16.2. Emulation

Python provides many special methods on classes that can be used to emulate other types, such as functions, iterators, containers and more.

Functions. In order to emulate a function object, a class must define the method __call__(). If the call operator () is used on an instance of the class, this method will be called behind the scenes. Here is an example that performs the same task as the adding closure in the functional programming section.

>>> class Adder(object):
...     def __init__(self, extra):
...         self.extra = extra
...     def __call__(self, base):
...         return self.extra + base
...
>>> add2 = Adder(2)
>>> add2(3)
5
>>> add5 = Adder(5)
>>> add5(3)
8
>>> add2(1)
3

Iterators. When an object is used in a for ... in statement, the object’s __iter__() method is called and the returned value should be an iterator. At that point, the interpreter iterates over the result, assigning each object returned from the iterator to the loop variable in the for ... in statement.

This example class implements the __iter__() method and returns a generator expression based on whatever arguments were passed to the initializer.

>>> class Lister(object):
...     def __init__(self, *args):
...         self.items = tuple(args)
...     def __iter__(self):
...         return (i for i in self.items)
...

>>> l = Lister('a', 'b', 'c')

>>> for letter in l:
...     print letter,
...
a b c

Here is the same example using a generator function instead of a generator expression.

>>> class Lister(object):
...     def __init__(self, *args):
...         self.items = tuple(args)
...     def __iter__(self):
...         for i in self.items:
...              yield i
...

>>> l = Lister('a', 'b', 'c')

>>> for letter in l:
...     print letter,
...
a b c

16.3. classmethod and staticmethod

A class method in Python is defined by creating a method on a class in the standard way, but applying the classmethod decorator to the method.

Notice in the following example that instead of self, the class method’s first argument is named cls. This convention is used to clearly denote the fact that in a class method, the first argument received is not a bound instance of the class, but the class object itself.

As a result, class methods are useful when there may not be an existing object of the class type, but the type of the class is important. This example shows a "factory" method, that creates Song objects based on a list of tuples.

Also notice the use of the __str__() special method in this example. This method returns a string representation of the object when the object is passed to print or the str() builtin.

>>> class Song(object):
...     def __init__(self, title, artist):
...         self.title = title
...         self.artist = artist

...     def __str__(self):
...         return ('"%(title)s" by %(artist)s' %
...                 self.__dict__)

...     @classmethod
...     def create_songs(cls, songlist):
...         for artist, title in songlist:
...             yield cls(title, artist)
...
>>> songs = (('Glen Hansard', 'Leave'),
...         ('Stevie Ray Vaughan', 'Lenny'))

>>> for song in Song.create_songs(songs):
...     print song
...
"Leave" by Glen Hansard
"Lenny" by Stevie Ray Vaughan

Static methods are very similar to class methods and defined using a similar decorator. The important difference is that static methods receive neither an instance object nor a class object as the first argument. They only receive the passed arguments.

As a result, the only real value in defining static methods is code organization. But in many cases a module-level function would do the same job with fewer dots in each call.

>>> class Song(object):
...     def __init__(self, title, artist):
...         self.title = title
...         self.artist = artist

...     def __str__(self):
...         return ('"%(title)s" by %(artist)s' %
...                 self.__dict__)

...     @staticmethod
...     def create_songs(songlist):
...         for artist, title in songlist:
...             yield Song(title, artist)
...
>>> songs = (('Glen Hansard', 'Leave'),
...         ('Stevie Ray Vaughan', 'Lenny'))

>>> for song in Song.create_songs(songs):
...     print song
...
"Leave" by Glen Hansard
"Lenny" by Stevie Ray Vaughan

16.4. Lab

oop-1-parking.py. 

'''
>>> # Create a parking lot with 2 parking spaces
>>> lot = ParkingLot(2)
'''


'''
>>> # Create a car and park it in the lot
>>> car = Car('Audi','R8', '2010')
>>> lot.park(car)
>>> car = Car('VW', 'Vanagon', '1981')
>>> lot.park(car)
>>> car = Car('Buick','Regal', '1988')
>>> lot.park(car)
'Lot Full'
>>> lot.spaces = 3
>>> lot.park(car)
>>> car.make
'Buick'
>>> car.model
'Regal'
>>> car.year
'1988'
>>> for c in lot:
...     print c
2010 Audi R8
1981 VW Vanagon
1988 Buick Regal
>>> for c in lot.cars_by_age():
...     print c
1981 VW Vanagon
1988 Buick Regal
2010 Audi R8
>>> for c in lot:
...     print c
2010 Audi R8
1981 VW Vanagon
1988 Buick Regal
'''



if __name__ == '__main__':
    import doctest
    doctest.testmod()

16.5. Inheritance

As noted in the first class definition example above, a class defines a superclass using the parentheses list in the class definition. The model for overloading methods is very similar to most other languages: define a method in the child class with the same name as that in the parent class and it will be used instead.

oop-1-inheritance.py. 

class Instrument(object):
    def __init__(self, name):
        self.name = name
    def has_strings(self):
        return True

class PercussionInstrument(Instrument):
    def has_strings(self):
        return False

guitar = Instrument('guitar')
drums = PercussionInstrument('drums')

print 'Guitar has strings: {0}'.format(guitar.has_strings())
print 'Guitar name: {0}'.format(guitar.name)
print 'Drums have strings: {0}'.format(drums.has_strings())
print 'Drums name: {0}'.format(drums.name)

$ python oop-1-inheritance.py
Guitar has strings: True
Guitar name: guitar
Drums have strings: False
Drums name: drums

Calling Superclass Methods. Python has a super() builtin function instead of a keyword and it makes for slightly clunky syntax. The result, however, is as desired, which is the ability to execute a method on a parent or superclass in the body of the overloading method on the child or subclass.

In this example, an overloaded __init__() is used to hard-code the known values for every guitar, saving typing on every instance.

oop-2-super.py. 

class Instrument(object):
    def __init__(self, name):
        self.name = name
    def has_strings(self):
        return True

class StringInstrument(Instrument):
    def __init__(self, name, count):
        super(StringInstrument, self).__init__(name)
        self.count = count

class Guitar(StringInstrument):
    def __init__(self):
        super(Guitar, self).__init__('guitar', 6)

guitar = Guitar()

print 'Guitar name: {0}'.format(guitar.name)
print 'Guitar count: {0}'.format(guitar.count)

python oop-2-super.py
Guitar name: guitar
Guitar count: 6

There is an alternate form for calling methods of the superclass by calling them against the unbound class method and explicitly passing the object as the first parameter. Here is the same example using the direct calling method.

oop-3-super-alt.py. 

class Instrument(object):
    def __init__(self, name):
        self.name = name
    def has_strings(self):
        return True

class StringInstrument(Instrument):
    def __init__(self, name, count):
        Instrument.__init__(self, name)
        self.count = count

class Guitar(StringInstrument):
    def __init__(self):
        StringInstrument.__init__(self, 'guitar', 6)

guitar = Guitar()

print 'Guitar name: {0}'.format(guitar.name)
print 'Guitar count: {0}'.format(guitar.count)

python oop-3-super-alt.py
Guitar name: guitar
Guitar count: 6

Multiple Inheritance. Python supports multiple inheritance using the same definition format as single inheritance. Just provide an ordered list of superclasses to the class definition. The order of superclasses provided can affect method resolution in the case of conflicts, so don’t treat it lightly.

The next example shows the use of multiple inheritance to add some functionality to a class that might be useful in many different kinds of classes.

oop-4-multiple.py. 

class Instrument(object):
    def __init__(self, name):
        self.name = name
    def has_strings(self):
        return True


class Analyzable(object):
    def analyze(self):
        print 'I am a {0}'.format(self.__class__.__name__)


class Flute(Instrument, Analyzable):
    def has_strings(self):
        return False


flute = Flute('flute')
flute.analyze()

$ python oop-4-multiple.py
I am a Flute

Abstract Base Classes. Python recently added support for abstract base classes. Because it is a more recent addition, its implementation is based on existing capabilities in the language rather than a new set of keywords. To create an abstract base class, override the metaclass in your class definition (metaclasses in general are beyond the scope of this course, but they define how a class is created). Then, apply the abstractmethod decorator to each abstract method. Note that both ABCMeta and abstractmethod need to be imported.

Here is a simple example. Notice that the base class cannot be instantiated, because it is incomplete.

oop-5-abc.py. 

from abc import ABCMeta, abstractmethod
import sys
import traceback

class Instrument(object):
    __metaclass__ = ABCMeta
    def __init__(self, name):
        self.name = name
    @abstractmethod
    def has_strings(self):
        pass


class StringInstrument(Instrument):
    def has_strings(self):
        return True


guitar = StringInstrument('guitar')
print 'Guitar has strings: {0}'.format(guitar.has_strings())
try:
    guitar = Instrument('guitar')
except:
    traceback.print_exc(file=sys.stdout)

$ python oop-5-abc.py
Guitar has strings: True
Traceback (most recent call last):
  File "samples/oop-5-abc.py", line 22, in <module>
    guitar = Instrument('guitar')
TypeError: Can't instantiate abstract class Instrument with abstract methods has_strings

One feature of abstract methods in Python that differs from some other languages is the ability to create a method body for an abstract method. This feature allows common, if incomplete, functionality to be shared between multiple subclasses. The abstract method body is executed using the super() method in the subclass.

oop-6-abcbody.py. 

from abc import ABCMeta, abstractmethod

class Instrument(object):
    __metaclass__ = ABCMeta

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

    @abstractmethod
    def has_strings(self):
        print 'checking for strings in %s' % \
            self.name


class StringInstrument(Instrument):

    def has_strings(self):
        super(StringInstrument,
                self).has_strings()
        return True


guitar = StringInstrument('guitar')
print 'Guitar has strings: {0}'.format(guitar.has_strings())

$ python oop-6-abcbody.py
checking for strings in guitar
Guitar has strings: True

16.6. Lab

oop-2-pets.py. 

'''
>>> cat = Cat('Spike')
>>> cat.speak()
'Spike says "Meow"'
>>> dog = Dog('Bowzer')
>>> cat.can_swim()
False
>>> dog.can_swim()
True
>>> dog.speak()
'Bowzer says "Woof"'
>>> fish = Fish('Goldie')
>>> fish.speak()
"Goldie can't speak"
>>> fish.can_swim()
True
>>> generic = Pet('Bob')
Traceback (most recent call last):
    ...
TypeError: Can't instantiate abstract class Pet with abstract methods can_swim
'''

if __name__ == '__main__':
    import doctest
    doctest.testmod()

16.7. Encapsulation

As mentioned previously, while Python does not support declarations of attribute visibility (public, private, etc), it does provide mechanisms for encapsulation of object attributes. There are three approaches with different levels of capability that can be used for this purpose.

16.7.1. Intercepting Attribute Access

When an attribute of an object is accessed using dot-notation, there are three special methods of the object that may get called along the way.

For lookups, two separate methods are called: __getattribute__(self, name) is called first, passing the name of the attribute that is being requested. Overriding this method allows for the interception of requests for any attribute. By contrast, __getattr__() is only called when __getattribute__() fails to return a value. So this method is useful if only handling undefined cases.

For setting attributes only one method, __setattr__(self, name, value) is called. Note that inside this method body, calling self.name = value will lead to infinite recursion. Use the superclass object.__setattr__(self, name, value) instead.

The following example shows a course with two attributes capacity and enrolled. A third attribute open is calculated based on the other two. However, setting it is also allowed and forces the enrolled attribute to be modified.

oop-7-intercept.py. 

import traceback
import sys

class Course(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.enrolled = 0

    def enroll(self):
        self.enrolled += 1

    def __getattr__(self, name):
        if name == 'open':
            return self.capacity - self.enrolled
        else:
            raise AttributeError('%s not found', name)

    def __setattr__(self, name, value):
        if name == 'open':
            self.enrolled = self.capacity - value
        else:
            object.__setattr__(self, name, value)

    def __str__(self):
        return 'Enrolled: \t{0}\nCapacity:\t{1}\nOpen:\t{2}'.format(
                self.enrolled, self.capacity, self.open)

course = Course(12)
course.enroll()
course.enroll()

print course

course.open = 8

print course

$ python oop-7-properties.py
Enrolled: 	2
Capacity:	12
Open:	10
Enrolled: 	4
Capacity:	12
Open:	8

16.7.2. Properties

For the simple case of defining a calculated property as shown in the above example, the more appropriate (and simpler) model is to use the property decorator to define a method as a propery of the class.

Here is the same example again using the property decorator. Note that this approach does not handle setting this property. Also notice that while open() is initially defined as a method, it cannot be accessed as a method.

oop-8-properties.py. 

import traceback
import sys

class Course(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.enrolled = 0

    def enroll(self):
        self.enrolled += 1

    @property
    def open(self):
        return self.capacity - self.enrolled

course = Course(12)
course.enroll()
course.enroll()

print 'Enrolled: \t{0}\nCapacity:\t{1}\nOpen:\t{2}'.format(course.enrolled,
        course.capacity, course.open)

print
try:
    course.open()
except:
    traceback.print_exc(file=sys.stdout)

print
try:
    course.open = 9
except:
    traceback.print_exc(file=sys.stdout)

$ python oop-8-properties.py
Enrolled: 	2
Capacity:	12
Open:	10

Traceback (most recent call last):
  File "samples/oop-8-properties.py", line 25, in <module>
    course.open()
TypeError: 'int' object is not callable

Traceback (most recent call last):
  File "samples/oop-8-properties.py", line 31, in <module>
    course.open = 9
AttributeError: can't set attribute

Using the property mechanism, setters can also be defined. A second decorator is dynamically created as <attribute name>.setter which must be applied to a method with the exact same name but an additional argument (the value to be set).

In this example, we use this additional functionality to encapsulate the speed of a car and enforce a cap based on the type of car being manipulated.

oop-9-propertysetters.py. 

class Car(object):
    def __init__(self, name, maxspeed):
        self.name = name
        self.maxspeed = maxspeed
        self.__speed = 0

    @property
    def speed(self):
        return self.__speed

    @speed.setter
    def speed(self, value):
        s = int(value)
        s = max(0, s)
        self.__speed = min(self.maxspeed, s)

car = Car('Lada', 32)
car.speed = 100
print 'My {name} is going {speed} mph!'.format(name=car.name, speed=car.speed)

car.speed = 24
print 'My {name} is going {speed} mph!'.format(name=car.name, speed=car.speed)

$ python oop-9-propertysetters.py
My Lada is going 32 mph!
My Lada is going 24 mph!

16.7.3. Descriptors

The final mechanism for encapsulating the attributes of a class uses descriptors. A descriptor is itself a class and it defines the type of an attribute. When using a descriptor, the attribute is actually declared at the class level (not in the initializer) because it is adopting some type information that must be preserved.

The descriptor class has a simple protocol with the methods __get__(), __set__() and __delete__() being called on attribute access and manipulation. Notice that the descriptor does not store instance attributes on itself, but rather on the instance. The descriptor is only instantiated once at class definition time, so any values stored on the descriptor object will be common to all instances.

The following example tackles the speed-limit problem using descriptors.

oop-10-descriptors.py. 

class BoundsCheckingSpeed(object):
    def __init__(self, maxspeed):
        self.maxspeed = maxspeed

    def __get__(self, instance, cls):
        return instance._speed

    def __set__(self, instance, value):
        s = int(value)
        s = max(0, s)
        instance._speed = min(self.maxspeed, s)


class Animal(object):
    speed = BoundsCheckingSpeed(0)

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

    @property
    def speed_description(self):
        return '{name} the {type} is going {speed} mph!'.format(name=self.name,
            type=self.__class__.__name__.lower(), speed=self.speed)


class Squirrel(Animal):
    speed = BoundsCheckingSpeed(12)


class Cheetah(Animal):
    speed = BoundsCheckingSpeed(70)


squirrel = Squirrel('Jimmy')
squirrel.speed = 20
print squirrel.speed_description

squirrel.speed = 10
print squirrel.speed_description

cheetah = Cheetah('Fred')
cheetah.speed = 100
print cheetah.speed_description

$ python oop-10-descriptors.py
Jimmy the squirrel is going 12 mph!
Jimmy the squirrel is going 10 mph!
Fred the cheetah is going 70 mph!
[Tip]Tip

Notice that values of the descriptor can be set for all instances of a certain class, while being different in different uses. The descriptor allows for the creation of a generic field "type" that can be shared in a configurable fashion across unrelated classes. It is more complex to use than properties, but can provide more flexibility in a complex object hierarchy.

16.8. Lab

oop-3-portfolio.py. 

'''
>>> p = Portfolio()
>>> stocks = (('APPL', 1000, 251.80, 252.73),
...           ('CSCO', 5000, 23.09, 23.74),
...           ('GOOG', 500, 489.23, 491.34),
...           ('MSFT', 2000, 24.63, 25.44))
...
>>> for stock in stocks:
...     p.add(Investment(*stock))
>>> print p['APPL']
1000 shares of APPL worth 252730.00
>>> p['GOOG'].quantity
500
>>> p['GOOG'].close
491.33999999999997
>>> p['GOOG'].open
489.23000000000002
>>> for stock in p:
...     print stock
1000 shares of APPL worth 252730.00
5000 shares of CSCO worth 118700.00
500 shares of GOOG worth 245670.00
2000 shares of MSFT worth 50880.00
>>> for stock in p.sorted('open'):
...     print stock.name
CSCO
MSFT
APPL
GOOG
>>> p['MSFT'].gain
0.81000000000000227
>>> p['CSCO'].total_gain
3249.9999999999927
>>> 'GOOG' in p
True
>>> 'YHOO' in p
False
'''

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Copyright © 2019 ProTech. All Rights Reserved.

Sign In Create Account

Navigation

Social Media