import addutils.toc ; addutils.toc.js(ipy_notebook=True)
Object Oriented (OOP) is a programming paradigm that allows abstraction through the concept of interacting entities. This is usally opposed to a more conventional model, called procedural, in which programs are organized as a sequence of commands (statements) to perform. We can think an object as an entity that resides in memory, has a states and it's able to perform some actions.
More formally objects are entities that represent instances of a general abstract concept called class. In
Python the variables defining an object state are called
attributes and the possible actions are called
In Python everything is an object also classes and functions.
from addutils import css_notebook css_notebook()
Suppose we want to create a class, named Person, as a prototype, a sort of template for any number of 'Person' objects (instances).
The python syntax to define a class is the following:
class ClassName(base_classes): statements
Class names should always be uppercase (it's a naming convention).
Say we need to model a Person as:
class Person: pass john_doe = Person() john_doe.name = "Alec" john_doe.surname = "Baldwin" john_doe.year_of_birth = 1958 print(john_doe) print("%s %s was born in %d." % (john_doe.name, john_doe.surname, john_doe.year_of_birth))
<__main__.Person object at 0x7fb64bb540b8> Alec Baldwin was born in 1958.
The preceding example defines an empty class (i.e. the class doesn't have a state) called Person then creates a Person instance called _johndoe and adds three attributes to _johndoe. We see that we can access objects attributes using the . dot operator.
This isn't a raccomended style because classes should describe omogeneous entities. A way to do so is the following:
class Person: def __init__(self, name, surname, year_of_birth): self.name = name self.surname = surname self.year_of_birth = year_of_birth
Is a special Python method that is automatically called after an object construction, its purpose is to initialize every object state. The first argument (by convention) self is automatically passed either and refers to the object itself.
In the preceding example
__init__ adds three attributes to every object that is instantiated. So the class is actually describing each object's state.
A class isn't something which we can directly manipulate, we need to create an instance of the class:
alec = Person("Alec", "Baldwin", 1958) print(alec) print("%s %s was born in %d." % (alec.name, alec.surname, alec.year_of_birth))
<__main__.Person object at 0x7fb64bb54ef0> Alec Baldwin was born in 1958.
We have just created an instance of the Person class, bound to the variable
class Person: def __init__(self, name, surname, year_of_birth): self.name = name self.surname = surname self.year_of_birth = year_of_birth def age(self, current_year): return current_year - self.year_of_birth def __str__(self): return "%s %s was born in %d ." % (self.name, self.surname, self.year_of_birth) alec = Person("Alec", "Baldwin", 1958) print(alec) print(alec.age(2014))
Alec Baldwin was born in 1958 . 56
We defined two more methods
__str__. The latter is once again a special method that is called by Python when the object has to be represented as a string (e.g. when has to be printed). If the
__str__ method isn't defined the print command shows the type of object and its address in memory. We can see that in order to call a method we use the same syntax for attributes (instance_name.instance _method).
It is possible to create a class without the
__init__ method, but this isn't a raccomended style because classes should describe omogeneous entities.
class Person: def set_name(self, name): self.name = name def set_surname(self, surname): self.surname = surname def set_year_of_birth(self, year_of_birth): self.year_of_birth = year_of_birth def age(self, current_year): return current_year - self.year_of_birth def __str__(self): return "%s %s was born in %d ." \ % (self.name, self.surname, self.year_of_birth)
In this case an empty instance of the class Person is created, and no attributes have been initialized while instantiating:
president = Person()
# This code will raise an attribute error: print(president.name)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-10-004adac081fd> in <module>() 1 # This code will raise an attribute error: ----> 2 print(president.name) AttributeError: 'Person' object has no attribute 'name'
This raises an Attribute Error... We need to set the attributes:
president.set_name('John') president.set_surname('Doe') president.set_year_of_birth(1940)
print('Mr', president.name, president.surname, 'is the president, and he is very old. He is', president.age(2014))
Mr John Doe is the president, and he is very old. He is 74
Since objects are a powerfull mean of abstraction they shouldn't reveal internal implementation detail, hence instance attributes shouldn't be accessible by the end user of an object.
Python doesn't have a strict mechanism to protect objects attributes, but official guidelines suggest that a variable that has an underscore
_ prefix should be treated as private.
Moreover prepending two underscores to a variable name makes the intepreter mangle a little the variable name.
class Person: def __init__(self, name, surname, year_of_birth): self._name = name self._surname = surname self._year_of_birth = year_of_birth def age(self, current_year): return current_year - self._year_of_birth def __str__(self): return "%s %s and was born %d." \ % (self._name, self._surname, self._year_of_birth) alec = Person("Alec", "Baldwin", 1958) print(alec) print(alec.age(2014))
Alec Baldwin and was born 1958. 56
class Person: def __init__(self, name, surname, year_of_birth): self.__name = name self.__surname = surname self.__year_of_birth = year_of_birth def age(self, current_year): return current_year - self.__year_of_birth def __str__(self): return "%s %s and was born %d." \ % (self.__name, self.__surname, self.__year_of_birth) alec = Person("Alec", "Baldwin", 1958) print(alec.__dict__.keys())
dict_keys(['_Person__name', '_Person__surname', '_Person__year_of_birth'])
__dict__ is a special attribute that is a dictionary containing each attribute of an object. We can see that prepending two underscores every key has
Once a class is defined it models a concept. Often is usefull to extend a class behavior to model a less general concept. Say we need to model a Student, but we know that every student is also a Person so we shouldn't model the Person again but inherit from it instead.
class Student(Person): def __init__(self, student_id, *args, **kwargs): super(Student, self).__init__(*args, **kwargs) self._student_id = student_id charlie = Student(1, 'Charlie', 'Brown', 2006) print(charlie) print(type(charlie)) print(isinstance(charlie, Person)) print(isinstance(charlie, object))
Charlie Brown and was born 2006. <class '__main__.Student'> True True
Charlie now has the same behavior of a Person, but his state has also a student ID. Person is one of the base classes of Student and Student is one of the sub classes of Person. Be aware that a subclass knows about its superclasses but the converse isn't true.
A sub class doesn't only inherits from its base classes, but from its base classes base classes too, forming an inheritance tree that starts from object (every class base class).
is a function that returns a proxy-object that delegates method calls to a parent or sibling class of type.
So we used it to access Person's
Inheritance allows to add new methods to a subclass, but often is usefull to change the behavior of a method defined in the superclass. To override a method just define it again.
class Student(Person): def __init__(self, student_id, *args, **kwargs): super(Student, self).__init__(*args, **kwargs) self._student_id = student_id def __str__(self): return super(Student, self).__str__() + " And has ID: %d" % self._student_id charlie = Student(1, 'Charlie', 'Brown', 2006) print(charlie)
Charlie Brown and was born 2006. And has ID: 1
__str__ again overriding the one wrote in Person, but we wanted to extend it, so we used super to achieve our goal.
Another powerful way to extend a class is called encapsulation and consists on wrapping an object with a second one. There are two main reasons to use encapsulation:
The abstraction process relies on creating a simplified model that remove useless details from a concept. In order to be simplified a model should be described in terms of other simpler concepts. For example we can say that a car is composed by:
And break down each one of this elements in simpler parts until we reach primitive data.
class Tyres: def __init__(self, branch, belted_bias, opt_pressure): self.branch = branch self.belted_bias = belted_bias self.opt_pressure = opt_pressure def __str__(self): return ("Tyres: \n \tBranch: " + self.branch + "\n \tBelted-bias: " + str(self.belted_bias) + "\n \tOptimal pressure: " + str(self.opt_pressure)) class Engine: def __init__(self, fuel_type, noise_level): self.fuel_type = fuel_type self.noise_level = noise_level def __str__(self): return ("Engine: \n \tFuel type: " + self.fuel_type + "\n \tNoise level:" + str(self.noise_level)) class Body: def __init__(self, size): self.size = size def __str__(self): return "Body:\n \tSize: " + self.size class Car: def __init__(self, tyres, engine, body): self.tyres = tyres self.engine = engine self.body = body def __str__(self): return str(self.tyres) + "\n" + str(self.engine) + "\n" + str(self.body) t = Tyres('Pirelli', True, 2.0) e = Engine('Diesel', 3) b = Body('Medium') c = Car(t, e, b) print(c)
Tyres: Branch: Pirelli Belted-bias: True Optimal pressure: 2.0 Engine: Fuel type: Diesel Noise level:3 Body: Size: Medium
Sometimes it's necessary to model a concept that may be a subclass of another one, but it isn't possible to know which class should be its superclass until runtime.
Suppose we want to model a simple dog school that trains instructors too. It will be nice to re-use Person and Student but students can be dogs or peoples. So we can remodel it this way:
class Dog: def __init__(self, name, year_of_birth, breed): self._name = name self._year_of_birth = year_of_birth self._breed = breed def __str__(self): return "%s is a %s born in %d." % (self._name, self._breed, self._year_of_birth) kudrjavka = Dog("Kudrjavka", 1954, "Laika") print(kudrjavka)
Kudrjavka is a Laika born in 1954.
class Student: def __init__(self, anagraphic, student_id): self._anagraphic = anagraphic self._student_id = student_id def __str__(self): return str(self._anagraphic) + " Student ID: %d" % self._student_id alec_student = Student(alec, 1) kudrjavka_student = Student(kudrjavka, 2) print(alec_student) print(kudrjavka_student)
Alec Baldwin and was born 1958. Student ID: 1 Kudrjavka is a Laika born in 1954. Student ID: 2
Python uses dynamic typing (also called duck typing). If an object implements a method you can use it, no matter what the type is. This is different from statically typed languages, where the type of a construct need to be explicitly declared. Polymorphism is the ability to use the same syntax for objects of different types:
def summer(a, b): return a + b print(summer(1, 1)) print(summer(["a", "b", "c"], ["d", "e"])) print(summer("abra", "cadabra"))
2 ['a', 'b', 'c', 'd', 'e'] abracadabra
There is an Object Oriented Programming principle called SRP single responsability principle and it states: "A class should have one single responsability" or "A class should have only one reason to change". If you come up with a class that doesn't follow this principle you should consider to split it. You will be grateful to SRP during your software manteinance.