Object-oriented Programming (OOP) is a programming paradigm which provides a means of structuring programs so that properties and behaviours are bundled into individual objects.
For example, an object would represent an employee with given name, last name, salary.
OOP is different to, for example, procedural programming which structures program like a recipe in that it provides a set of steps in the form of functions and code blocks that flow sequentially in order to complete a task. In OOP, objects are at the center of programming as it does not only represent the data (like PP) but also the overall structure and blueprint of the program.
Python is a multi-paradigm programming language. OOP is one of the paradigms that best suits the problem at hand. In a program, multiple paradigms can be used and mixed between each others.
class User:
pass
user = User()
user.first_name = 'Bruce'
user.last_name = 'Lee'
print(user.first_name, user.last_name)
: Bruce Lee
#+Name: PEP8 style guide for class object naming
#+begin_quote
"..lowercase with words seperated by underscores as necessary to improve readability.."
class User:
pass
user2 = User()
user2.first_name = 'Bruce'
user2.last_name = 'Lee'
first_name = 'Hello'
last_name = 'World'
print(user2.first_name, user2.last_name)
print(first_name, last_name)
user1 = User()
user1.first_name = 'Clark'
user1.last_name = 'Kent'
print(user1.first_name, user1.last_name)
: Bruce Lee
: Hello World
: Clark Kent
Classes allow us to logically group our data and functions in a way that’s easily to reuse and to build upon (if need be).
A Class is like a blueprint of objects of a similar type.
An instance of a Class is an object with the blueprint that is the Class. Any instance of the class is a unique instance with unique attributes.
init
methodA method is a function inside a class.
The init method is called everytime a new instance of the class is initiated. In some other languages, the init method is called the constructor.
self
should always be the first argument, which is reference to the object being created.
class User:
def __init__(self, full_name, birthday):
# Assign the init arguments to the initiating object.
self.name = full_name
self.birthday = birthday
# extract first and lastname
name_pieces = full_name.split(' ')
self.gvn = name_pieces[0]
self.sn = name_pieces[-1]
user = User('Bruce Lee', '31031983')
print(user.name)
print(user.sn, user.gvn)
: Bruce Lee
: Lee Bruce
Note: In the above example, the only valid attributes of the class User upon init are name, birthday, gvn, sn
. These are the variables that have been assigned to self
. Calling any other attributes that have not been assigned to self
would result in an AttributeError
.
class User:
""" This is a docstring, They are pretty useful.
We should always write this."""
pass
help(User)
The docstring is displayed when running help(class
) command.
class Employee:
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def fullname(self):
return '{} {}'.format(self.gvn, self.sn)
employee1 = Employee('Bruce', 'Lee', 50000)
print(employee1.gvn)
print(employee1.fullname())
: Bruce
: Bruce Lee
class Employee:
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def fullname(self):
return '{} {}'.format(self.gvn, self.sn)
employee1 = Employee('Bruce', 'Lee', 50000)
print(employee1.fullname())
# These are the same
print(Employee.fullname(employee1))
: Bruce Lee
: Bruce Lee
Calling a method on the class instead of on the instance provides the same result, however calling the Class requires us to type the name of the instance into the argument.
This explains the self
argument, which is automatically placed as an argument whenever you invoke a method from an instance of a class.
self
argumentDon’t forget to use the self
argument. Without this, althougth the class creation would have no problem, when we call the actual method, there would be an error message. Although it looks like no argument is passed, the instance argument is passed automatically when the method is called.
class Employee:
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def fullname():
return '{} {}'.format(self.gvn, self.sn)
employee1 = Employee('Bruce', 'Lee', 50000)
print(employee1.fullname())
: Traceback (most recent call last):
: File "<stdin>", line 1, in <module>
: File "/tmp/babel-10144YHR/python-10144Osv", line 13, in <module>
: print(employee1.fullname())
: TypeError: fullname() takes 0 positional arguments but 1 was given
Regular methods in a class automatically takes the instance as the first argument in the method call. (See [[self][self]])
@classmethod
decorator.self
, we can specify the class variable by using cls
.Class methods are similar to other instance methods (e.g __init__
) in a class. However instead of having the self
instance passed as the first argument to it, class methods’ first argument is the actual uninstantiated class itself (cls
).
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def set_new_raise_amt(cls, amount):
cls.pay_raise_amt = amount
employee1 = Employee('Bruce', 'Lee', 50000)
employee2 = Employee('Manny', 'Pacquiao', 500000)
print(Employee.pay_raise_amt)
Employee.set_new_raise_amt(1.05)
print(employee1.pay_raise_amt)
print(employee2.pay_raise_amt)
: 1.04
: 1.05
: 1.05
This is an example of alternative constructor.
The aim is to be able to instantiate an object from a simple string, with the properties seperated by a dash.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def from_string(cls, emp_str):
first, last, pay = emp_str.split('-')
return cls(first, last, pay)
emp_str = 'Bruce-Lee-50000'
employee1 = Employee.from_string(emp_str)
print(employee1.email)
: Bruce.Lee@company.com
Instead of passing the instance (self
) or the class (cls
) as an argument to the method, a static method does not pass anything on default.
They behave just like regular function however they are organised in the class because they have a logical connection to the class. They essentially know nothing about the class.
Static methods definition is preceded by the @staticmethod
decorator:
import datetime
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@staticmethod
def is_workday(day):
if day.weekday() > 4:
return False
return True
print(Employee.is_workday(datetime.date(2019, 9, 3)))
: True
@classmethod
and @staticmethod
Similarities:
Differences:
Class methods work with the class and has access to class variables. Its first parameter must always be the (uninstantiated) class.
Magic methods in Python are special methods that are not invoked by us howver the invocation happens internally from the class on a certain action.
Built-in classes in Python define many magic methods. This can be retrieved by dir()
function.
print(dir(int))
print()
x = 10
print((x + 10))
print((x.__add__(10)))
print(repr(x))
: ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
:
: 20
: 20
: 10
From the above, when we does +
, the operator actually calls the __add__
method. Variable assignment invokes the __new__
method.
This is why the same operator, i.e +
, perform different operations for strings and integers.
Special methods are always surrounded by double underscores __
. These are called Dunder. For example, __init__
is called “Dunder init”.
The three most common methods are __repr__
, __str__
and __init__
methods.
According to the official Python documentation, _repr__
is a built-in function used to compute the “official” string reputation of an object, while __str__
is a built-in function that computes the “informal” string representations of an object.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
emp1 = Employee('Bruce', 'Lee', 50000)
print(emp1)
: <__main__.Employee object at 0x7f8204872f28>
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def __repr__(self):
return "Employee('{}', '{}', {})".format(self.gvn, self.sn, self.pay)
emp1 = Employee('Bruce', 'Lee', 50000)
print(emp1)
#+Name: With the Dunder repr and str method:
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def __repr__(self):
return "Employee('{}', '{}', {})".format(self.gvn, self.sn, self.pay)
def __str__(self):
return '{} - {}'.format((self.gvn + ' ' + self.sn), self.email)
emp1 = Employee('Bruce', 'Lee', 50000)
print(emp1)
print()
print(repr(emp1))
print(str(emp1))
: Bruce Lee - Bruce.Lee@company.com
:
: Employee('Bruce', 'Lee', 50000)
: Bruce Lee - Bruce.Lee@company.com
The repr
method takes a single parameter Obj
whose printable representation is to be returned.
The return value is a printable representational string of the given object.
The returned string would yield an object with the same value when passed to eval()
.
eval(repr(obj)) == object
What is the difference between repr()
and str()?
(More on this topic: https://stackoverflow.com/questions/1436703/difference-betw``` een-str-and-repr)
print(1 + 2)
print(int.__add__(1, 2))
print((1 + 2) == int.__add__(1, 2))
: 3
: 3
: True
We can customize how the operator +
works by the use of Dunder add method.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def __add__(self, other):
return self.pay + other.pay
emp1 = Employee('Bruce', 'Lee', 50000)
emp2 = Employee('Jet', 'Li', 55000)
print(emp1 + emp2)
: 105000
More on this: https://docs.python.org/3.7/reference/datamodel.html?highlight=emulating%20numeric%20types
Class variables are variables that are shared amongst all instances of the same class.
For example, with the class Employee, we’d like to share variables such as given name, last name, pay ..
class Employee:
pay_raise_amt = 1.04 ## This is a Class Variable
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def fullname(self):
return '{} {}'.format(self.gvn, self.sn)
def pay_raise(self):
self.pay = int(self.pay * self.pay_raise_amt)
employee1 = Employee('Bruce', 'Lee', 100000)
print(employee1.__dict__)
employee1.pay_raise()
print(employee1.__dict__)
print('Accessing var from Instance:', employee1.pay_raise_amt)
print('Accessing var from Class:', Employee.pay_raise_amt)
Employee.pay_raise_amt = 1.10 ## Change Class Variable
print('New Class Variable is applied across all instances:', employee1.pay_raise_amt)
employee1.pay_raise_amt = 1.20
print('New Instance Variable:', employee1.pay_raise_amt)
print('Changing Instance Variable does nothing to the Class Variable', Employee.pay_raise_amt)
print(employee1.__dict__)
: {'gvn': 'Bruce', 'sn': 'Lee', 'pay': 100000, 'email': 'Bruce.Lee@company.com'}
: {'gvn': 'Bruce', 'sn': 'Lee', 'pay': 104000, 'email': 'Bruce.Lee@company.com'}
: Accessing var from Instance: 1.04
: Accessing var from Class: 1.04
: New Class Variable is applied across all instances: 1.1
: New Instance Variable: 1.2
: Changing Instance Variable does nothing to the Class Variable 1.1
: {'gvn': 'Bruce', 'sn': 'Lee', 'pay': 104000, 'email': 'Bruce.Lee@company.com', 'pay_raise_amt': 1.2}
Class variables can be accessed via both the Class (`Employee.pay_raise_amt`) or the Instance (`employee1.pay_raise_amt`).
Note how there is no `pay_raise_amt` in the `__dict()__` method call of the instance before the change to the variable. This is because after the change, `pay_raise_amt` was turned into an Instance Variable that exists in `employee1` which is seperate from the default Class Variable.
### Class Variable vs Instance Variable in method definition
Note in the above snippet, there was this part:
```python
def pay_raise(self):
self.pay = int(self.pay * self.pay_raise_amt)
What it does is apply the pay_raise_amt
of the Instance, not the Class variable. Unless a change to the instance variable was applied, the method pay_raise
would use the fallback/ default Class variable value.
That can also be forced by typing this instead:
def pay_raise(self):
self.pay = int(self.pay * Employee.pay_raise_amt)
Inheritance allows us to inherit class attributes from parent class. It is useful so that we can create subclasses that inherits from parent classes then we can also modify the subclasses without touching the parent classes.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def set_new_raise_amt(cls, amount):
cls.pay_raise_amt = amount
class Developer(Employee):
# inherit from Employee Class
pass
dev1 = Developer('Bruce', 'Lee', 50000)
print(dev1.email)
: Bruce.Lee@company.com
Aim: Change the pay raise amount of Developer to 1.05 instead of 1.04.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def set_new_raise_amt(cls, amount):
cls.pay_raise_amt = amount
class Developer(Employee):
# inherit from Employee Class
pass
Developer.set_new_raise_amt(1.05) ## Changes Pay Raise amount for only Developer Class.
print(Employee.pay_raise_amt) ## Unchanged
print(Developer.pay_raise_amt)
: 1.04
: 1.05
Aim: Developer to have an init value for Programming Language.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def set_new_raise_amt(cls, amount):
cls.pay_raise_amt = amount
class Developer(Employee):
# inherit from Employee Class
def __init__(self, first_name, last_name, pay, p_lang):
# inherit the parent class init
super().__init__(first_name, last_name, pay)
# Doing this allows us to modify existing methods with minimal codechange i.e more maintainable
# This also works:
# Employee.__init__(self, first_name, last_name, pay)
self.p_lang = p_lang
dev1 = Developer('Bruce', 'Lee', 50000, 'C++')
print(dev1.email)
print(dev1.p_lang)
: Bruce.Lee@company.com
: C++
Aim: Create a class for Manager that also contains information about employees that he supervises.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
def fullname(self):
return '{} {}'.format(self.gvn, self.sn)
class Manager(Employee):
# Pass arguments for employees which the manager supervises
def __init__(self, first_name, last_name, pay, employees=None):
super().__init__(first_name, last_name, pay)
if employees is None:
self.employees = []
else:
self.employees = employees
print ('Created new Manager', super().fullname())
def add_emp(self, emp):
if emp not in self.employees:
def remove_emp(self, emp):
if emp in self.employees:
self.employees.remove(emp)
def print_emps(self):
for emp in self.employees:
print('-->', emp.fullname())
mgr1 = Manager('Daniel', 'Smith', 100000)
mgr1.print_emps()
emp1 = Employee('Bruce', 'Lee', 50000)
emp2 = Employee('Jackie', 'Chan', 50000)
mgr2 = Manager('Jonathan', 'Pickle', 100000, [emp1, emp2])
mgr2.print_emps()
print()
mgr2.add_emp(Employee('Jet', 'Li', '55000'))
mgr2.print_emps()
: Created new Manager Daniel Smith
: Created new Manager Jonathan Pickle
: --> Bruce Lee
: --> Jackie Chan
:
: --> Bruce Lee
: --> Jackie Chan
: --> Jet Li
Python has 2 built-in functions for this.
class Employee:
pay_raise_amt = 1.04
def __init__(self, first_name, last_name, pay):
self.gvn = first_name
self.sn = last_name
self.pay = pay
self.email = first_name + '.' + last_name + '@company.com'
@classmethod
def set_new_raise_amt(cls, amount):
cls.pay_raise_amt = amount
class Developer(Employee):
# inherit from Employee Class
pass
dev1 = Developer('test', 'test', 100000)
print(isinstance(dev1, Developer))
print(issubclass(Developer, Employee))
: True
: True
dir
dir
is a powerful in-built function of Python 3. It returns all properties and attributes of a class. These includes class attributes, methods, .etc.
dir(class)
dir(instance)
__dict__
Every class has a __dict__
method. It returns all variables of an instance.
Note that it does not return the default class variables.
help()
Useful for reading docstring and properties. This can also be used to see inherited attributes.
print(help(class))
print(help(instance))
super()
super()
alone returns a temporary object of the superclass. A super class is also known as a parent class. This allows us to invoke methods of the parent class, when working with a subclass.