Object oriented programming

What is object oriented programming?

Fundamental for object (-orientated) programming (OOP) is the concept of object---a complete combination of data and operations on these data. Objects usually (not always) hide data and externally provide only a limited set of operations, ie. methods. Typically the person who accesses an object is not interested in the implementation of those methods (the object can silently delegate their execution to other objects), or the way the objects's data are arranged.

Object-oriented programming also includes several other concepts. Programming languages adopt and interpret them in different ways. Let us show which concepts are present in Python and how to use them.

Objects

In Python, anything is an object (unlike C ++ or Java). Objects are all built-in types (numbers, strings, ...), all containers, as well as functions, modules, and object types. Absolutely everything provides some public methods.

Object's type

Each object has a type; types can be divided into built-in types (list, tuple, int, ...) and classes (types defined using the class keyword ). The type determines what methods an object offers, it is a sort of a template (general characteristics), from which the individual object differs by its internal state (specific properties). We say that object is an instance of that type (class). To determine the type of an object Python has a built-in function type.

In [2]:
print(type("Babička"))
print(type(46878678676848648486))              # long in Python 2, int in Pythonu 3
print(type(list()))                            # an instance of the list type
print(type(list))                              # the list type itself

print(isinstance(list(), list))                # The "isinstance" checks the object type
<class 'str'>
<class 'int'>
<class 'list'>
<class 'type'>
True

Instantiation

An instance of a given type is created similarly to calling a function. If we have a data type (class), create an instance, just as we like to call it, ie. using parethesis. After all, we have already done so with built-in types like tuple, dict or list. Effectively, the instantiation process involves calling the class constructor (see below).

In [3]:
objekt = list()        # Creates a new instace of the list type
objekt2 = list         # This does not create a new instace! It just gives a new name to the list type

# Let's see what we've got
print("objekt = %s" % objekt)
print("objekt2 = %s" % objekt2)
objekt = []
objekt2 = <class 'list'>
In [4]:
# Now we can create a list using obejct2
objekt2()
Out[4]:
[]

Using methods

Method is a function that is tied to some object (and is basically meaningless without the object) and operates with its data. It can also change the internal state of the object, ie. the attributes' values.

In Python, methods are called using dot notation, object.method(arguments)

In [5]:
objekt = [45, 46, 47, 48]     # objekt is a list instance
objekt.append(49)             # we call its append method

objekt
Out[5]:
[45, 46, 47, 48, 49]

The append method has no meaning in itself, only in conjunction with a specific list; it adds a new element to the list.

Classes

Class is any user type. Like built-in types, it offers methods and data (attributes), which we can arbitrarily define.

The simplest definition of an empty class (pass is used for empty classes and methods to circumvent the indentation):

In [6]:
class MyClass(object):    # create a new class calles MyClass
    pass                  # the class is empty

Method definition

Methods are defined within the calss block. (NB. Methods can be added to the class later, but it is not the preferred method.)

Conventional methods (instance methods) are called on a particular object. Besides, there are also so-called class methods and static methods, which we are not going to discuss.

Quite unusual (unlike C ++, Java and other languages) is that the first argument of the method is the object on which the method is called. Without that, the method does not know with which object it is working! By convention (which is perhaps never violated), this first argument is called self. When the method is called, Python fill this argument automatically.

In [7]:
class Car(object):
    def roll(self, distance):     # Don't forget *self*
        print("Rolling {} kilometers.".format(distance))
        
car = Car()                        
car.roll(100)                     # self is omitted
Rolling 100 kilometers.

Error! Notice the number of arguments that Python complains about.

In [8]:
car.roll(car, 100)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-486a0f332ee2> in <module>()
----> 1 car.roll(car, 100)

TypeError: roll() takes 2 positional arguments but 3 were given

Constructor

Contructor is the method that initializes the object. It's called when we create a new instance. We can (and in most cases we do) define it but we do not have to, in which case the default constructor is used that simply does nothing (special). The constructor in Python is always named __init__ (two underscores before and after).

In [9]:
class MyClass2(object):
    def __init__(self):
        print("We are in the constructor")

print("Before instantiating MyClass2")
# The constructor will be called now
objekt = MyClass2()
print("After instantiating MyClass2")
Before instantiating MyClass2
We are in the constructor
After instantiating MyClass2

Attributes

Python does not distinguish between methods and data (such as in general the variables - everything is an object). Everything is an attribute of the object. Values are assigned similarly as variables but we have to add the object and the dot. The attributes may not even exist yet when it is assigned (it does not have to be declared). (NB. Internally attributes are stored in dictionaries and access to them is through the dictionary of the object itself, its class, its parent class, ...).

In [10]:
from __future__ import division      # division operator from Pythonu 3

class Car(object):
    def __init__(self, consupmtion):    # constructor with an argument
        self.consupmtion = consupmtion  # simply store as an attribute (of self)
    
    def roll(self, distance):
        # the consumption attribute is used
        gas = distance / 100 * self.consupmtion
        # gas is local, not an attribute
        print("Rolling {} kilometrs, using {} liters of gas.".format(distance, gas))
        
car = Car(15)
print("My car has a consumtions of {} l/100 km.".format(car.consupmtion))  
car.roll(150)
My car has a consumtions of 15 l/100 km.
Rolling 150 kilometrs, using 22.5 liters of gas.

The list of all attributes is returned by dir

In [11]:
# attributes with underscore are special, we'll filter them out
", ".join(item for item in dir(car) if not item.startswith("_"))  
Out[11]:
'consupmtion, roll'

Properties

Properties are "smarter" data. They allow you to enter into the process of reading or setting attributes. It is useful for example if an object has several interdependent parameters, and we do not want to store them independently; or if we want to check what value is stored; or if we want to do anything interesting with the values.

From the syntactic point of view, we must first define the method that bears the name of the property and that "reads" the property (returns its value). The line above must a property decorator (for details see e.g. Python decorators in 12 easy steps). If we want, we can then create methods for writing and deleting.

Once we have created the following properties, we approach them as common data attributes - call them without brackets and assign to them using the sign "equals".

Properties work like properties in C# or Java JavaBeans. However notice that for accessing properties exactly the same notation as for accessing data attributes is used. Hence if someone wants to change the behavior of a data attribute and make it a property, clients of the class will not recognize it and will not have to make any changes in the code. It is therefore not suitable to aggressively create trivial properties that encapsulate only access to attributes (like we would certainly do in Java).

We will show how properties work on a simple example of a Circle class, which can set both the radius and the area consistently.

In [12]:
import math
import numbers

class Circle(object):
    def __init__(self, r):
        self.radius = r
        
    @property                          # this will be our are "reader"
    def area(self):                    # this looks like any other method
        return math.pi * self.radius ** 2
    
    @area.setter                       # area "setter"
    def area(self, s):                 
        print("Changing the area to {}".format(s))
        if not isinstance(s, numbers.Number):   # is s a number?
            raise TypeError("Area must me a number")
        # the radius must be set consistently
        self.radius = math.sqrt(s / math.pi)
        
    @area.deleter
    def area(self):
        raise Exception("Deleting circle's does not make any sense")
    
# create a circle with unity radius
circle = Circle(1)
print("r = {}".format(circle.radius))    # usual attribute
print("S = {}".format(circle.area))      # a property

circle.area = 5                          # Changing radius using the area setter
print("r = {}".format(circle.radius))    # We've changed the radius accordingly
print("S = {}".format(circle.area))      # a property
r = 1
S = 3.141592653589793
Changing the area to 5
r = 1.2615662610100802
S = 5.000000000000001
In [13]:
# Let's see if the check in the "setter" works
circle.area = "Just like the biggest Czech pond, which is called Rožmberk."
Changing the area to Just like the biggest Czech pond, which is called Rožmberk.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-208c2926f400> in <module>()
      1 # Let's see if the check in the "setter" works
----> 2 circle.area = "Just like the biggest Czech pond, which is called Rožmberk."

<ipython-input-12-b40830c6a419> in area(self, s)
     14         print("Changing the area to {}".format(s))
     15         if not isinstance(s, numbers.Number):   # is s a number?
---> 16             raise TypeError("Area must me a number")
     17         # the radius must be set consistently
     18         self.radius = math.sqrt(s / math.pi)

TypeError: Area must me a number
In [14]:
# Another meaningless operation 
del circle.area
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-14-da91ede7e384> in <module>()
      1 # Another meaningless operation
----> 2 del circle.area

<ipython-input-12-b40830c6a419> in area(self)
     20     @area.deleter
     21     def area(self):
---> 22         raise Exception("Deleting circle's does not make any sense")
     23 
     24 # create a circle with unity radius

Exception: Deleting circle's does not make any sense

Encapsulation

Python does not adhere to this (fundamental) OOP concept very strongly. The principles of OOP claim that the data should not be accessible from outside. Other languages usually offer a way of hiding some methods (such as the keywords private or protected in C++, Java). Python does not try to resolve this issue and, by default, everything is accessible.

Instead, there exist the following conventions:

  • Object's data (unless the class is really primitive) are not modified from outside.
  • Methods whose name starts with an underscore will are not called from outside (because they are not part of the "public" interface).
  • To protect data, we can make them properties.
  • Any differences in general and the way in which the methods and data are handled should be included in the class documentation.
  • There are ways you can enforce encapsulation (redefining the access to attributes, ...) but those are rarely used (and rarely rally useful).

In return, Python offers a very high level of introspection, or the ability to learn information about objects (their type, attributes, etc.) at runtime.

The underscore convention

In Python conventions are generally very strongly entrenched. It is perhaps the most visible in the context of objects.

  1. "Private" attributes (attributes in Python often means both data and methods - everything is an object) are named with an underscore at the beginning, e.g _private_method.
  2. Two underscores at the beginning of the name of an attribute renames it so it's really hard to reference the attribute outside the context of the class.
  3. Attributes with two undescores at the beginning and at the end have a special meaning (see documentation). We have already seen init and will look at several others.
    • __repr__ and __str__ convert the object to a string.
    • __getattr__ and __setattr__ are used for reading and storing not found attributes.
    • __call__ will be called when we use the object as a function.
    • __doc__ contains documentation (docstring).
    • __dict__ contains the dictionary with the namespace of the object.
    • ... Furthermore, there are special features for logical operators, to emulate the functionality of containers (iteration, items, cuts), for arithmetic operations, etc.
In [15]:
# what an instance ob the object type contains?
dir(object())
Out[15]:
['__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']
In [16]:
# and a simple function?
def foo(x):
    """Toto je funkce foo"""
    return x
dir(foo)
Out[16]:
['__annotations__',
 '__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__get__',
 '__getattribute__',
 '__globals__',
 '__gt__',
 '__hash__',
 '__init__',
 '__kwdefaults__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']

Inheritance

Class can inherit (derive) its behavior (and data) from another class, thus saving a lot of work in the repetition of common features. In this case, we say that our new class (child or subclass) inherits from the original (parent) class.

  • In a subclass, we can change the definition of some of the methods of the superclass.
  • Constructors are inherited by default (unlike C++ or Java, in Python we have to explicitely call the superclass constructor only if we define a new constructor).
  • Subclasses can be used wherever the parent class(es) can be used. This applies even more generally in Python as we usually do not check the specific type. Instead, we look for particular attributes / methods. This is easily possible because Python is dynamically typed.

Syntax: The name of the parent class is given in paretheses after the name (instead of object from which the class usually inherits).

In [19]:
class Human(object):
    def __init__(self, name):          # The constructor sets the name
        self.name = name
    
    def say(self, what):               # The default say method
        print(type(self).__name__ + ": " + what)
    
    def introduce(self):             
        self.say("My name is %s." % self.name)
        
    def greet(self):                 
        self.say("Hello!")
        
    def goodbye(self):
        self.say("Good bye!")        
    
    
class Serviceman(Human):
    def repair_tv(self):         # A new method
        self.say("Give me 5 minutes.")
        print("---The serviceman is working.---")
        self.say("Done.")
        
    def introduce(self):            # introduce differently; self.name is used here
        self.say("I'm %s." % self.name)
                
class Patient(Human):
    def say(self, what):            # redefined method
        """Say something with a running nose."""
        trantab = "".maketrans("nmNM", "dbDB")
        
        Human.say(self, what.translate(trantab))   # call parent class' method
        self.sneeze()
        
    def sneeze(self):                 # A new method - other humans do not sneeze
        print("---Achoo---")
    
joe = Serviceman("Joe Smith")
bill = Patient("Bill Jones")

# A daily conversation
joe.greet()
bill.greet()
joe.introduce()
bill.introduce()
bill.say("Can you fix my TV, please?")
joe.repair_tv()
bill.say("Thank you very much.")
joe.goodbye()
bill.goodbye()
Serviceman: Hello!
Patient: Hello!
---Achoo---
Serviceman: I'm Joe Smith.
Patient: By dabe is Bill Jodes.
---Achoo---
Patient: Cad you fix by TV, please?
---Achoo---
Serviceman: Give me 5 minutes.
---The serviceman is working.---
Serviceman: Done.
Patient: Thadk you very buch.
---Achoo---
Serviceman: Good bye!
Patient: Good bye!
---Achoo---
In [20]:
bill.repair_tv()           # Patients do not repair TV's
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-20-d3e549105d18> in <module>()
----> 1 bill.repair_tv()           # Patients do not repair TV's

AttributeError: 'Patient' object has no attribute 'repair_tv'

A sick electrician could be created using multiple inheritance, in which case we would have to consider if parent methods are called properly. Even better, we could use so-called mix-ins and inject properties into objects dynamically, but this is really an advanced topic that we will not cover.

Inheriting from built-in types

Classes can also inherit from built-in types (and it is often useful, although our example does not prove it).

In [21]:
# A list that does not return its item unless pleaded
class PeevishList(list):
    def __getitem__(self, index):                     # redefining the method that handles getting items by [...]
        if isinstance(index, tuple) and index[1].lower()[:6] in ("please"):
            return list.__getitem__(self, index[0])   # the parent's method
        else:
            print("What about pleading?")
            return None
        
s = PeevishList((1, 2, 3, 4))
print(s[1])
What about pleading?
None
In [22]:
print(s[1, "please"])
2

Advanced topics

Following topics are very interesting and terribly useful but we do not have so much time to devote to them. However, we recommend reading about them, if you have a little time.

  • Multiple inheritance
  • Class methods
  • Static methods
  • Abstract classes
  • Polymorphism
  • Metatclasses
  • Design Patterns

Comments

Comments powered by Disqus