Object oriented programming is a way of thinking about and defining how different pieces of software and ideas work together. In object-oriented programming, there are two main interfaces: classes and objects.
Unlike functional or procedural paradigms, there are three main features that classes provide.
Encapsulation: Classes are container which may have any kind of other programming element living on them: variables, functions, and even other classes. In Python, members of a class are known as attributes for normal variables and methods for functions.
Inheritence: A class may automatically gain all of the attributes and methods from another class it is related to. The new class is called a subclass or sometimes a subtype. Multiple levels of inheritance sets up a class heirarchy. For example:
Polymorphism: Subclasses may override methods and attributes of their parents in a way that suitable to them. For example:.
If this seems more complicated than writing functions and calling them in sequence that is because it is! However, obeject orientation enables authors to cleanly separate out ideas into independent classes. It is also good to know because in many languages - Python included - it is the way that you modify the type system.
Object oriented programming revolves around the creation and manipulation of objects that have attributes and can do things.
They can be as simple as a coordinate with x and y values or as complicated as a dynamic webpage framework.
Here is the code for making a very simple class that sets an attribute.
class MyClass(object):
def give_me_a_name(self, name):
self.name = name
my_object = MyClass()
type(my_object)
__main__.MyClass
my_object.name
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-4-6b7f39812b30> in <module>() ----> 1 my_object.name AttributeError: 'MyClass' object has no attribute 'name'
my_object.give_me_a_name('Tris')
print my_object.name
Tris
# direct access
my_object.name = 'Violet'
print my_object.name
Violet
newobj = MyClass()
newobj.name = 'What?'
newobj.name
'What?'
In the object oriented terminology above:
object
- a special class which should be the parent of all classes.You write a class and you create an instance of an object.
newclass = MyClass()
print newclass.name
Usually you want to create an object with a set of initial or default values for things. Perhaps an object needs certain information to be created.
For this you write a constructor. In python, constructors are just methods
with the special name __init__()
:
class Person(object):
"""creates a person with the name Ada"""
def __init__(self):
self.name = "Ada"
# create an instance of Person called person
# print person.name
person = Person()
print person.name
Ada
Constructors may take arguments just like any other method or function. This gives us flexibility for the different instances.
def person():
print 'Ada'
person()
Ada
class Person(object):
def __init__(self, name, title="Programmer", awesomeness = 100):
"""Create Person object with name and title"""
self.name = name
self.title = title
self.awesome = awesomeness
ada = Person()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-19-1dec48bcfcfa> in <module>() ----> 1 ada = Person() TypeError: __init__() takes at least 2 arguments (1 given)
## create a new object called grace, setting the name to 'Grace Hopper'
grace = Person("Grace Hopper")
print grace.name, grace.title, grace.awesome
Grace Hopper Programmer 100
## create a new object called 'Ada Lovelace' and change her title to 'Mother of Computing'
ada = Person("Ada Lovelace", awesomeness=1000)
print ada.name, ada.title, ada.awesome
Ada Lovelace Programmer 1000
If you specify an attribute outside fo the init (and you will see examples of this), you need to be aware of behavior if the attribute is a mutable attribute (such as an array).
Consider the following class
class silly(object):
name = 'hello'
mysilly = silly()
print mysilly.name
hello
mystr = [1,]
mystr.
File "<ipython-input-39-004b932c288f>", line 1 mystr. ^ SyntaxError: invalid syntax
class MakeList(object):
something = []
def append_input(self, stuff):
self.something.append(stuff)
firstlist = MakeList()
secondlist = MakeList()
## put 23 into the 'something' list of your first instance
firstlist.append_input(23)
#what will be in the something attribute for firstlist
#what about secondlist?
print firstlist.something
print secondlist.something
[23, 23, 42, 23] [23, 23, 42, 23]
This is an example of an attribute that is shared across instances of a class. And is not the behavior we would gernerally want.
So How do we fix this?
Fix the broken behavior in MakeList by moving the attribute something to __init__
# Lets make a quick test so you can see if your solution works
def test_MakeList():
first = MakeList()
second = MakeList()
first.append_input(42)
#print first.something, second.something
assert (first.something == second.something) is False
#this should raise an Assertion Error
test_MakeList()
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-50-85c52f0cd302> in <module>() 8 9 #this should raise an Assertion Error ---> 10 test_MakeList() <ipython-input-50-85c52f0cd302> in test_MakeList() 5 first.append_input(42) 6 print first.something, second.something ----> 7 assert (first.something == second.something) is False 8 9 #this should raise an Assertion Error AssertionError:
[23, 23, 42, 23, 42] [23, 23, 42, 23, 42]
## make a new class, create something in the __init__ method
class MakeList(object):
def __init__(self):
self.something = []
fruit = 'blueberry'
def append_input(self, input):
self.something.append(input)
## Test it
test_MakeList()
tmp = MakeList()
list.__
If you want a to create a class that behaves mostly like another class, you should not have to copy code. What you do is subclass and change the things that need changing. When we created classes we were already subclassing the built in python class "object."
Consider the following data model.
You are a biologist collecting data on a group of creatures (dogs, cats birds, choose your favorite).
you could create a nested dictionary
mycreatures = {
'id001': {'weight': 32.4, 'gender': 'Female'},
'id002': {'weight': 36.7, 'gender': 'Male'},
}
But it makes it hard to easily access and compare the different creatures. So lets make a subclass of the basic object class in python and over-ride a couple methods (functions) to get the behavior we want.
To do this we have to implement part of the Python Data Model. Python has a list of special - or sometimes known as magic - method names that you can override to implement support for many language operations. All of these method names start and end with a double underscore __
. This is because no regular method would ever use such an obtuse name. It also lets the user and other developers know that something special is happening in those methods and that they aren't meant to be called directly. Many of these has a predefined interface they must follow.
We have already seen an example of this with the __init__()
constructor method. Now let's try to make comparisons work for Creature, specifically by comparing weights. From the documentation, there is:
object.__lt__(self, other) **less than**
object.__gt__(self, other) **greater than**
object.__eq__(self, other) **equal**
object.__ne__(self, other) **not equal**
Another useful special method is the __str__()
method, which allows you to provide a string representation of the object.
In our case this lets us easily print the name, weight and gender of our creature.
# make a generic creature class
class Creature(object):
def __init__(self, name, weight, gender):
"""Create instance of creature with a name, weight, and gender"""
self.name = name
self.weight = weight
self.gender = gender
def __lt__(self, other):
"""check if weight is less than other"""
return self.weight < other
def __gt__(self, other):
"""check if weight is greater than other"""
return self.weight > other
def __et__(self, other):
"""check of weight is equal to other"""
return self.weight == other
def __ne__(self, other):
"""check if weight is not equal to other"""
return not self.weight == other
def __str__(self):
"""name, weight and gender of specific creature instance"""
return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight)
## Create a few animals
animal_a = Creature('001', 34.2,'Female')
animal_b = Creature('002', 36.7, 'Male')
animal_c = Creature('003', 32.4, 'Male')
mylist = [1,2]
import os
os.__
'/Users/cindeem/anaconda/python.app/Contents/lib/python2.7/os.pyc'
print animal_a
Name: 001 is a Female with weight 34.20
animal_a > 1
True
## Check comparison is on weight
print animal_a < animal_b
print animal_a == animal_b
True False
## Now if we put these creatures in a list, we can sort them by weight
myanimals = [animal_a, animal_b, animal_c]
print [x.name for x in myanimals]
## sort list by weight
myanimals.sort()
print [(x.name, x.weight) for x in myanimals]
['001', '002', '003'] [('003', 32.4), ('001', 34.2), ('002', 36.7)]
Inheritance can be tricky...So lets use a simple set of classes and try to figure out how they will behave. We define three classes below, with simple attributes.
if c_instance is an instance of C:
class A(object):
a = 'a is a'
class B(A):
b = 'b is b'
class C(B):
b = 'b is 42'
c = 'c is c'
c_instance = C()
# Is a an attribute of c_instance?
print c_instance.a
#Is b an attribute of c_instance?
print c_instance.b
a is a b is 42
# create a new instance of A, what is the value of the attribute c?
a_instance = A()
print a_instance.c
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-68-166874c30d6e> in <module>() 1 # create a new instance of A, what is the value of the attribute c? 2 a_instance = A() ----> 3 print a_instance.c AttributeError: 'A' object has no attribute 'c'
Normally, when you get or set attributes on an object the value that you are setting simply gets a new name. However, sometimes you run into the case where you want to do something extra depending on the actual value you are reciveing. For example, maybe you need to confirm that the value is actually correct or desired.
Python provides a mechanism called properties to do this. Properties are methods which either get, set, or delete a given attribute. To implement this, use the built-in property()
decorator:
## example where our class fails
bad_animal = Creature(1,'blue', 43)
print bad_animal
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-73-4726600d26e6> in <module>() 1 ## example where our class fails 2 bad_animal = Creature(1,'blue', 43) ----> 3 print bad_animal <ipython-input-70-2e2cff1c9c4d> in __str__(self) 23 def __str__(self): 24 """name, weight and gender of specific creature instance""" ---> 25 return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight) 26 ValueError: Unknown format code 'f' for object of type 'str'
class Creature(object):
def __init__(self, name, weight, gender):
"""Create instance of creature with a name, weight, and gender"""
self.name = name
self._weight = 0
self.weight = weight
self.gender = gender
@property
def weight(self):
# getter
return self._weight
@weight.setter
def weight(self, val):
# setter check val is float
if isinstance(val, float):
self._weight = val
else:
raise ValueError('Weight must be a float, not {}:{}'.format(type(val),val))
def __lt__(self, other):
"""check if weight is less than other"""
return self.weight < other
def __gt__(self, other):
"""check if weight is greater than other"""
return self.weight > other
def __et__(self, other):
"""check of weight is equal to other"""
return self.weight == other
def __ne__(self, other):
"""check if weight is not equal to other"""
return not self.weight == other
def __str__(self):
"""name, weight and gender of specific creature instance"""
return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight)
## Now what happens when we pass a bad weight value?
bad_creature = Creature('bird_001', '32','Female')
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-75-41fdc6617759> in <module>() 1 ## Now what happens when we pass a bad weight value? ----> 2 bad_creature = Creature('bird_001', '32','Female') <ipython-input-74-e99c4a1e9184> in __init__(self, name, weight, gender) 4 self.name = name 5 self._weight = 0 ----> 6 self.weight = weight 7 self.gender = gender 8 <ipython-input-74-e99c4a1e9184> in weight(self, val) 18 self._weight = val 19 else: ---> 20 raise ValueError('Weight must be a float, not {}:{}'.format(type(val),val)) 21 22 ValueError: Weight must be a float, not <type 'str'>:32
## create a good_creature named 'bird_001', with weight 32.5, and gender 'Female'
## print result
good_creature = Creature('bird_001', 32.5,'Female')
print good_creature
Name: bird_001 is a Female with weight 32.50
# what about direct access?
good_creature.weight = 'hello'
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-77-ce4e2cb3d88f> in <module>() 1 # what about direct access? ----> 2 good_creature.weight = 'hello' <ipython-input-74-e99c4a1e9184> in weight(self, val) 18 self._weight = val 19 else: ---> 20 raise ValueError('Weight must be a float, not {}:{}'.format(type(val),val)) 21 22 ValueError: Weight must be a float, not <type 'str'>:hello
Can you think of how to check that gender is Male or Female?
## create Creature Class using property to check gender input
class Creature(object):
def __init__(self, name, weight, gender):
"""Create instance of creature with a name, weight, and gender"""
self.name = name
self._weight = 0
self.weight = weight
self._gender = gender
self.gender = gender
@property
def weight(self):
# getter
return self._weight
@weight.setter
def weight(self, val):
# setter check val is float
if isinstance(val, float):
self._weight = val
else:
raise ValueError('Weight must be a float, not {}:{}'.format(type(val),val))
@property
def gender(self):
# getter
return self._gender
@gender.setter
def gender(self, val):
# setter check val is 'Male' or 'Female'
if val in ('Male', 'Female'):
self._gender = val
else:
raise ValueError('Gender must be a Male or Female, not {}'.format(val))
def __lt__(self, other):
"""check if weight is less than other"""
return self.weight < other
def __gt__(self, other):
"""check if weight is greater than other"""
return self.weight > other
def __et__(self, other):
"""check of weight is equal to other"""
return self.weight == other
def __ne__(self, other):
"""check if weight is not equal to other"""
return not self.weight == other
def __str__(self):
"""name, weight and gender of specific creature instance"""
return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight)
## Check gender
good_gender = Creature('Bruce', 32.4, 'Male')
print good_gender
bad_gender = Creature('abby', 32.4, 'blue')