Table of Contents
Introduction
Arrays in C have a huge disadvantage.
- All values stored in an
array
need to be of the same data type
int main (void)
{
int a[] = {1, 2, 3, 5, 6};
char word[10] = {'c', 'o', 'd', 'e', '\0'};
}
Lists in Python are more powerful then arrays in C.
- A
list
in Python allows us to store multiple variables of different data types together in one list - A
dictionary
in Python allows us to store multiple key-value pairs in one data structure
# gift = ["<content>", <width>, <height>, <color>]
gift_1 = ["CS50 Rubber Duck", 20.0, 40.0, "green"]
gift_2 = ["CS50 Stress Ball", 30.0, 20.0, "pink"]
gift_3 = ["CS50 Sunglasses", 15.0, 10.0, "yellow"]
Data Structures in C allow us to store information in different layouts.
- A
struct
is a collection of variables (can be of different types) under a single name
struct Gift
{
char content[50];
float width;
float height;
char color[20];
} gift_1, gift_2, gift_3;
int main (void)
{
gift_1.content = "CS50 Rubber Duck";
gift_1.width = 20.0;
gift_1.height = 40.0;
gift_1.color = "green";
gift_2.content = "CS50 Stress Ball";
gift_2.width = 30.0;
gift_2.height = 20.0;
gift_2.color = "pink";
...
return 0;
}
Object-oriented programming
Everything in Python is an object.
- Every object
- has a type
- can manipulate objects
- can destroy objects
a = [1, 2, 3, 4, 5]
b = 5
c = "Hello"
print(type(a))
print(type(b))
print(type(c))
Output:
<type 'list'>
<type 'int'>
<type 'str'>
Objects = attributes + methods.
- Python built-in function
dir
allows us to access all attributes and methods of a specific object - Objects are a data abstraction that captures
- an internal representation through data attributes
- an interface for interacting with the object through methods
a = [1, 2, 3, 4, 5]
print(dir(a))
Output:
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
In OOP classes are the blueprints for objects.
class <name>
starts a class definition- Code inside
class
is indented - Use
pass
to create an “empty” class
class Gift():
# define attributes here
pass
Use ClassName()
to create an object of class ClassName
>>> gift_1 = Gift()
>>> gift_2 = Gift()
Attributes are used specify classes like data structures allowed us to do.
- Let’s quickly re-visit the concept of data structures (
struct
) in C
struct Gift
{
char content[50];
float width;
float height;
char color[20];
};
- Having this in mind, attributes allow us to do the same with classes:
- Use constructor
__init__
to initialize data attributes
- Use constructor
class Gift():
def __init__(self, content, width, height, color):
self.content = content
self.width = width
self.height = height
self.color = color
Methods can be easily assigned to Classes and you can work with them as you are used to with functions.
- method definition = function definition within class
- Use
self
as the 1st argument in method definition
class Gift():
def __init__(self, content, width, height, color):
self.content = content
self.width = width
self.height = height
self.color = color
def pack(self, packer):
print("This gift is packed by " + packer)
Before we use a certain method we need to create an instance of a class (object).
- Data atrributes of an instance are called instance variables
- Ignore
self
when calling method on an object (Python takes care of that for you) self
is a placeholder for particular object used in class definition and is the first argument of any method
# create a new object of type Gift and pass in content, width, height and color to __init__
>>> gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
# use dot operator to access any attribute of gift_1
>>> print(gift_1.color)
# use dot operator to access any method of gift_1
>>> gift_1.pack("Nele")
Output:
This gift is packed by Nele
gift_1.pack("Nele")
is interpreted asGift.pack(gift_1, "Nele")
# create a new object of type Gift and pass in content, width, height and color to __init__
>>> gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
# use dot operator to access any method of gift_1
>>> gift_1.pack("Nele")
This gift is packed by Nele
Now it’s time to bring it all together.
class Gift():
def __init__(self, content, width, height, color):
self.content = content
self.width = width
self.height = height
self.color = color
def pack(self, packer):
print("This gift is packed by " + packer)
def gift_to(self, gifted):
print(f"This gift contains a {self.content} and is gifted to {gifted}.")
def main():
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
gift_1.gift_to("David")
if __name__ == "__main__":
main()
Output:
This gift contains a CS50 Rubber Duck and is gifted to David.
Let’s re-visit the just learned by having a look at the anatomy of classes.
- Methods are function definitions within a class
- Define data attributes by assignment
- Refer to attributes via
self._____
- Refer to attributes via
class MyClass():
# method definition in class
# first argument is self
def my_method1(self, other_arguments...):
# do things here
def my_method2(self, my_attr):
# attribute created by assignment
self.my_attr = my_attr
...
- Constructor
__init__
is called every time an object is created
class Gift:
def __init__(self, content):
self.content = content # <--- Create the .name attribute and set it to name parameter
print("The __init__ method was called")
gift_1 = Gift("CS50 Rubber Duck") # <--- __init__ is implicitly called
Output:
The __init__ method was called
Through encapsulation, we can easily work with objects within objects.
- Encapsulation means bundling together data attributes and methods to operate on them
- This allows us to effectively combine objects
class Gift():
def __init__(self, content, width, height, color):
self.content = content
self.width = width
self.height = height
self.color = color
class ChristmasTree():
def __init__(self, max_gifts):
self.max_gifts = max_gifts
self.gifts = []
def place_gift(self, gift):
self.gifts.append(gift)
def remove_gift(self, gift):
self.gifts.remove(gift)
def get_contents(self):
for gift in self.gifts:
print(gift.content)
def main():
# Initialize gifts
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
gift_2 = Gift("CS50 Stress Ball", 30.0, 20.0, "pink")
# Initalize tree
tree = ChristmasTree(5)
tree.place_gift(gift_1)
tree.place_gift(gift_2)
tree.get_contents()
tree.remove_gift(gift_2)
print("After removal:")
tree.get_contents()
if __name__ == "__main__":
main()
In procedural programming, we used print-statements to interact with a program. Objects cannot be printed that easily.
Default print representation
- By default, the
print
representation of objects is very uninformative
class Gift():
...
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
print(gift_1)
Output:
<__main__.Gift object at 0x10337ccd0>
Define custom method to print objects
- Using a custom method, allows us to make the
print
representation more informative - define
show
to print object of classGift
class Gift():
...
def show(self):
print(self.content, self.width, self.height, self.color)
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
gift_1.show()
Output:
CS50 Rubber Duck 20.0 40.0 green
Instead of accessing data attributes by using the “.
” notation, it’s best practice to write getter and setter methods.
- Here we define
get_width
andset_width
to access and change the width of an object of classGift
-
class Gift(): def __init__(self, content, width, height, color): self.content = content self.width = width ... def get_width(self): return self.width def set_width(self, width): self.width = width
By nature Python is not great in information hiding. Though in OOP we have the possibility to do so.
- Private: indicated by a double underscore
self.__attribute
- Private attributes cannot be accessed from outside a class.
- Protected: Indicated by a single underscore
self._attribute
- Protected attributes should not be accessed from outside a class, other than subclasses
- Note that Python only sets this as convention, so it’s more an indicator
- Public: Indicated by the absence of an underscore:
self.attribute
- Public attributes are always accessible
class Gift():
def __init__(self, content, width, height):
self.__content = content
self._width = width
self.height = height
def __get_height(self):
return self.height
def _get_width(self):
return self._width
def get_content(self):
return self.__content
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0)
### Data attributes
## AttributeError: 'Gift' object has no attribute '__content
#print(gift_1.__content)
print(gift_1._width)
print(gift_1.height)
### Methods
## AttributeError: 'Gift' object has no attribute '__getHeight'
#print(gift_1.__get_height())
print(gift_1._get_width())
print(gift_1.get_content())
Output:
20.0
40.0
20.0
CS50 Rubber Duck
Core Concepts of OOP
Inheritance provides a convenient mechanism for building groups of related abstractions.
- Remember that classes are used to implement data abstractions
- Inheritance allows you to create a type hierarchy in which each type inherits types from above it in the hierarchy
- The class
object
is at the top of hierarchy This makes sense, since in Python everything that exists at runtime is an object Because Animal inherits all the properties of objects, programs can bind a variable to anAnimal
, append anAnimal
to alist
, etc.
Parent class
- Everything is an
object
in Python, soAnimal
inherits all the properties of objects - class
object
implements basic operations in Python, like binding variables, etc.
class Animal():
def __init__(self, age, name):
self.age = age
self.name = name
def make_noise(self):
print("I don't know, which noise I make")
Children classes
- Parent class is
Animal
- Call Animal constructor
- Call Animal’s
set_name
method - Add new data attribute
color
to Cat which is a string containing the cat’s color
- Override Animal’s
make_noise
method
class Cat(Animal):
def __init__(self, age, name, color):
super().__init__(age, name)
self.color = color
def make_noise(self):
print("Meow")
class Dog(Animal):
def __init__(self, age, name, color):
super().__init__(age, name)
def make_noise(self):
print("Wuff")
class Fox(Animal):
def __init__(self, age, name, color):
super().__init__(age, name)
You might have noticed that we called the constructor of our superclass by using super().__init__()
instead of Animal.__init__()
.
- In a class hierarchy with single inheritance, super can be used to refer to parent class without naming it explicitly
- This makes the code more maintainable
self
is not needed when working withsuper()
super().__init__(age, name)
equals toAnimal.__init__(self, age, name)
class Cat(Animal):
def __init__(self, age, name, color):
super().__init__(age, name)
self.color = color
def make_noise(self):
print("Meow")
In addition to what subclasses inherit they can add new attributes and override attributes of superclasses.
Add new attributes
- Cat added the instance variables
color
andcatID
- The instance variable
self.catID
is initialized using a class variabletag
, that belongs to the classCat
rather than to instances of the class
Override attributes of superclass
- For example,
Cat
has overridden__init__
andmake_noise
class Cat(Animal):
tag = 0
def __init__(self, age, name, color):
super().__init__(age, name)
self.color = color
self.catID = Cat.tag
Cat.tag += 1
def make_noise(self):
print("Meow")
Inheritance allows us to modify methods in children classes, which is one of the most common forms of polymorphism.
- Use of a single type entity (method, operator or object) to represent different types in different scenarios
Dynamic typing
- No need to declare variable during runtime
## variable is assigned to a string
a = "hello"
print(type(a))
## variable is assigned to an integer
a = 5
print(type(a))
Operator overloading
- Python objects allow us to extend the meaning of default operators, e.g. ‘
+
’ or ‘*
’
# Python program to show use of
# + operator for different purposes.
print(1 + 2)
# concatenate two strings
print("Intro" + "CS")
# Product two numbers
print(3 * 4)
# Repeat the String
print("IntroCS" * 4)
Output:
str
int
Method overloading
- In Python, Method overloading does not work as in other languages like Java or C++/#
- However, we can set parameters to default values:
def product(a, b, c=1):
return a * b * c
# without defining c=1 as default parameter, this line would throw an error
print(product(5, 10))
Method overriding
- Method overriding is an ability of every OOP programming language that allows subclasses to override methods of the according superclasses (Inheritance)
class Animal(object):
def __init__(self):
self.value = "Inside parent"
def show(self):
print(self.value)
class Cat(Animal):
def __init__(self):
self.value = "Inside children"
def show(self):
print(self.value)
Output:
Inside parent
Inside children
Instead of overriding the show method, we can use the str method to override Python’s default print-statement.
str(self.width)
:__str__
must return a string
class Gift():
...
def __str__(self):
return 'This gift contains ' + self.content + ' and has a width of ' + str(self.width)
gift_1 = Gift("CS50 Rubber Duck", 20.0, 40.0, "green")
print(gift_1.__str__())
print(gift_1)
Output:
This gift contains CS50 Rubber Duck and has a width of 20.0
This gift contains CS50 Rubber Duck and has a width of 20.0
Just like __str__
there are more of those special operators in Python.
- Special methods in OOP allow us to override common methods, which we know already.
__add__(self, other)
__sub__(self, other)
__eq__(self, other)
__lt__(self, other)
__len__(self)
__str__(self)
This maps to:
self + other
self - other
self == other
self < other
len(self)
print(self)