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
# create an instance called my_object
# set the name of my_object to 'Tris' using give_me_a_name
# print my_object.name
# set the name through direct access, and print result
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.
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
Constructors may take arguments just like any other method or function. This gives us flexibility for the different instances.
class Person(object):
def __init__(self, name, title="Programmer"):
"""Create Person object with name and title"""
self.name = name
self.title = title
## create a new object called grace, setting the name to 'Grace Hopper'
## create a new object called 'Ada Lovelace' and change her title to 'Mother of Computing'
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 MakeList(object):
something = []
def append_input(self, input):
self.something.append(input)
firstlist = MakeList()
secondlist = MakeList()
## put 23 into the 'something' list of your first instance
#what will be in the something attribute for firstlist
#what about secondlist?
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()
## make a new class, create something in the __init__ method
## Test it!
test_MakeList()
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')
## Check comparison is on weight
## Now if we put these creatures in a list, we can sort them by weight
## sort list by weight
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?
#Is b an attribute of c_instance?
# create a new instance of A, what is the value of the 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
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?
## create a good_creature named 'bird_001', with weight 32.5, and gender 'Female'
## print result
Can you think of how to check that gender is Male or Female?
## create Creature Class using property to check gender input
## Check gender
good_gender = Creature('Bruce', 32.4, 'Male')
print good_gender
bad_gender = Creature('abby', 32.4, 'blue')