class SupervisedEstimator(...):

  def __init__(self, hyperparameter_1, ...):
    self.hyperparameter_1
    ...
    
  def fit(self, X, y):
    ...
    self.fit_attributes
    return self
    
  def predict(self, X):
    ...
    return y_pred
    
  def score(self, X, y):
    ...
    return score

OOP Concepts

  • Classes
  • Instances
  • Instance Attributes
  • Class Attributes
  • Methods
  • The ‘self’ parameter
  • Inheritance
  • Docstrings
  • Special Methods
  • Projects - Build a Dice Game & a Tic-Tac-Toe Game using Python OOP!

OOP Projects

»» Play Tic-Tac-Toe Game

»» Play Dice Game

1. INTRO

Object-Oriented Programming : Programming paradigm (an approach / style of programming) that organizes software design around objects

Other paradigms:

  • Imperative
  • Functional
  • Declarative
  • Logic

OOP is based on messages sent to objects (objects have their state and behavior)

Class is a blueprint for creating objects

Objects are like legos which can be combines to create more complex structures

Advantages:

  • Modularity - Classes work as blueprints / modules isolated from other part of programs

  • Extensability - Objects can be extended to include new attributes and behavior

  • Reusability - Objects can be resused within the program and in future projects. Faster development and lower cost of development

  • Easier to Maintain - Make changes to module without large scale changes. Higher quality software

2. CLASSES

A blueprint to define the attributes and behavior of an object

Process:

  • Identify an object
  • Analyze its attributes and behavior
  • Create a class as blueprint of the object

EXAMPLE PROBLEMS -

  • Our stores serve fast food - pizza, burger, hot dogs, soda. Clients choose to buy one or more items. More than 1 item purchase gets 20% discount. The store has 5 employees.
  • Nouns - Clients, Employees, pizza, burger, hot dogs, soda

CLASSES STRUCTURE

Header (first line in class definition)

class <ClassName>:
class Backpack:
  pass

Body - defines the attributes and behavior of objects

Elements of a class:

  • Class Attribues
  • init()
  • Methods

Notes:

  • Class is singular
  • First letter in upper case
  • Indent the elements
class Backpack:
  # Class Attribues
  # __init__()
  # Methods

3. OBJECTS

Objects - are instance of a class

Difference:

  • Class - Abstract
  • Object - Concrete instances of a class

Notes:

  • Instance attributes are independent
  • Attributes can have unique value for a instance
  • init() - special method used to define the initial state of an object. Called automatically when an instance is created.
  • init() always has to take self as the first parameter

Example 1:

class House:
  # Class Attribues
  # __init__()  
  def __init__(self, price):
    # self.price is attribute
    # price is parameter
    self.price = price
  # Methods

Example 2:

class Backpack:
  # Class Attribues
  # __init__()  
  def __init__(self):
    # self.items is attribute - initiall all backpacks are empty demonstrated by an empty list
    self.items = []
    
  # Methods

4. SELF & DEFINE INSTANCE ATTRIBUTES

Notes:

  • Self - is a generic way of referring to the current instance of the class
  • Go to style guide for Python for reference

Example 1: Backpack

class Backpack:
  # Class Attribues
  # __init__()  
  def __init__(self, color, size):
    # self.items is attribute - initiall all backpacks are empty demonstrated by an empty list
    self.items = []
    # self.color is attribute
    # color is parameter    
    self.color = color
    self.size = size
    
  # Methods

Example 2: Circle

class Circle:
  # Class Attribues
  # __init__()  
  def __init__(self, radius):
    # self.radius is attribute
    # radius is parameter    
    self.radius = radius
    self.color = "Blue" # assign all circle instance color blue
    
  # Methods

Example 2: Rectangle

class Rectangle:
  # Class Attribues
  # __init__()  
  def __init__(self, length, width):
    # self.length is attribute
    # length is parameter    
    self.length = length
    self.width = width
    
  # Methods

Example 3: Movie

class Movie:
  # Class Attribues
  # __init__()  
  def __init__(self, title, year, language, rating):
    # self.title is attribute
    # title is parameter    
    self.title = title
    self.year = year
    self.language = language
    self.rating = rating
    
  # Methods

5. HOW TO CREATE AN INSTANCE

Notes:

  • self is skipped in the argument list when we create an instance and you don’t have to pass a value for that parameter

Example 1: Backpack

class Backpack:

  def __init__(self):
    self.items = []
    
  
my_backpack = Backpack()  

print(my_backpack)

Example 2: Circle

class Circle:

  def __init__(self, radius):   
    self.radius = radius
    

my_circle = Circle(5)

print(my_circle)

Example 3: Rectangle

class Rectangle:

  def __init__(self, length, width):  
    self.length = length
    self.width = width


my_rectangle = Rectangle(3, 6)

print(my_rectangle)

6. HOW TO ACCESS INSTANCE ATTRIBUTES (VARIABLES & METHODS) OF AN OBJECT

Example 1: Backpack

# Define Class
class Backpack:

  def __init__(self):
    self.items = ["Water Bottle", "Pencils"] # Initially has two items
    print(self.items)
    
# Main Program (outside of class)  
my_backpack = Backpack()  

print(my_backpack.items)

7. HOW TO ACCESS INSTANCE ATTRIBUTES (VARIABLES & METHODS) OF AN OBJECT

Example 1: Movie

class Movie:

  def __init__(self, title, year, language, rating):  
    self.title = title
    self.year = year
    self.language = language
    self.rating = rating
    

# First instance of the class Movie
my_favorite_movie = Movie("Pride and Prejudice", 2005, "English", 4.8)
print(my_favorite_movie.title)
print(my_favorite_movie.year)
print(my_favorite_movie.language)
print(my_favorite_movie.rating)

# Second instance of the class Movie
your_favorite_movie = Movie("Titanic", 1997, "English", 4.6)
print(your_favorite_movie.title)
print(your_favorite_movie.year)
print(your_favorite_movie.language)
print(your_favorite_movie.rating)

8. DEFAULT ARGUMENTS

Example 1: Circle

class Circle:

  def __init__(self, radius=5):   
    self.radius = radius
    

my_circle = Circle()
print(my_circle.radius)

my_circle = Circle(8)
print(my_circle.radius)

Notes:

  • Default parameter should always follow after Non-Default parameter

Example 2: Circle

class Circle:

  def __init__(self, color, radius=5):   
    self.color = color
    self.radius = radius
    

my_circle = Circle("Blue", 7)
print(my_circle.color)
print(my_circle.radius)

9. HOW TO UPDATE INSTANCE ATTRIBUTES

Example 1: Backpack

# Define Class
class Backpack:

  def __init__(self, color):
    self.items = []
    self.color = color


# Main Program (outside of class)  
my_backpack = Backpack("Blue")  
print(my_backpack.color)

my_backpack.color = "Green"
print(my_backpack.color)

Example 2: Circle

class Circle:

  def __init__(self, color, radius=5):   
    self.color = color
    self.radius = radius
    

my_circle = Circle("Yellow", 6)
print(my_circle.color)
print(my_circle.radius)

my_circle.color = "Black"
my_circle.radius = 15

print(my_circle.color)
print(my_circle.radius)

10. INTRO TO CLASS ATTRIBUTES

Notes:

  • Defined before init() attributes
class Backpack:
  # Class Attribues
  # __init__()
  # Methods

11. DEFINE CLASS ATTRIBUTES

Notes:

  • Defined before init() attributes
  • 4 spaces preferred over tab

Example 1: Dog

class Dog:

  # Class Attribues
  species = "Canis lupus"
  
  # __init__() or Instance Attributes
  def __init()__(self, name, age, breed)
    self.name = name
    self.age = age
    self.breed = breed
  
  # Methods
 

Example 2: Backpack

class Backpack:

  max_num_items = 10

  def __init__(self):
    self.items = ["Water Bottle", "Pencils"] # Initially has two items
    print(self.items)
    
# Main Program (outside of class)  
my_backpack = Backpack()  

print(my_backpack.items)

12. HOW TO ACCESS CLASS ATTRIBUTES

Example 1: Dog

class Dog:

  # Class Attribues
  species = "Canis lupus"
  
  # __init__() or Instance Attributes
  def __init()__(self, name, age, breed)
    self.name = name
    self.age = age
    self.breed = breed
  
  # Methods

print(Dog.species)

Example 2: Movie

class Movie:

  # Class attributes shared across all instances
  id_counter = 1

  def __init__(self, title, rating):  
    self.id = Movie.id_counter
    self.title = title
    self.rating = rating
    
    # Increment the class attribute or counter
    Movie.id_counter += 1
    
    
 my_movie = Movie("Sense and Sensibility", 4.5)
 your_movie = Movie("Lengends of the Fall", 4.7)
 
 print(my_movie.id)
 print(your_movie.id)
 

Example 3: Backpack

class Backpack:

  max_num_items = 10

  def __init__(self):
    self.items = []
    
# Main Program (outside of class)  
my_backpack = Backpack()  
your_backpack = Backpack() 

# Access the attribute through class
print(Backpack.max_num_items)

# Access the attribute through instance
print(my_backpack.max_num_items)
print(your_backpack.max_num_items)

13. HOW TO MODIFY CLASS ATTRIBUTES

Changing the value of class attribute affects all instance

Example 1: Circle

class Circle:

  radius = 5

  def __init__(self, color):   
    self.color = color
    

print(Circle.radius)

my_circle = Circle("Blue")
your_circle = Circle("Green")

print(my_circle.radius)
print(your_circle.radius)

Circle.radius = 10

print(my_circle.color)
print(my_circle.radius)


# values will get updated for class as well as instances

print(Circle.radius)
print(my_circle.radius)
print(your_circle.radius)

Example 2: Pizza

class Pizza:

  price = 12.99

  def __init__(self, description, toppings, crust):   
    self.color = color
    self.description = description
    self.crust = crust
    

print(Pizza.price)

my_pizza = Circle("Margherita", ["Basil", "Mushrooms"], "New York Style")
print(my_pizza.price)

Pizza.price = 13.99

print(Pizza.price)
print(my_pizza.price)

Example 3: Backpack

class Backpack:

  max_num_items = 10

  def __init__(self):
    self.items = []
    
# Main Program (outside of class)  
print(Backpack.max_num_items)

my_backpack = Backpack()  
print(my_backpack.max_num_items)

Backpack.max_num_items = 15

print(Backpack.max_num_items)
print(my_backpack.max_num_items)

14. ENCAPSULATION

Two basic pillars of OOPS - Encapsulation & Abstraction

Encapsulation - Bundling of data and methods that act on the data into a single class Class - Shields direct access to the attributes in order to avoid making potentially problematic changes to the state

Making members of the class public or non-public

Getters

Setters

15. ABSTRACTION

  • Show only the essential members and hide the complexity

  • Example - GUI of a phone abstracts away the complexity. Turning on the key of car without knowing complexity of engine

Two part of class

  1. Interface
  2. Implementation

Abstract out common part of the code

16. PUBLIC & NON-PUBLIC ATTRIBUTES

Public attributes - An attribute that can be accessed and modified directly without access restrictions

Example 1: Car

class Car:

  def __init__(self, brand, model, year):
    self.brand = brand
    self.model = model
    self.year = year
    
# Main Program (outside of class)  
my_car = Car("Porsche", "911 Carrera", 2020)  
print(my_car.year)

# Can set a public-attribute invalid value
my_car.year = 5600

print(my_car.year)

Non-public attributes - An attribute that shouldn’t be accessed or modified outside of the class

Private - No attribute is ever private in Python (Python doesnt have access modifiers like Java)

Notes:

  • Two ways to make an attribute Non-Public
    • By naming convention : _variable (self._variable)
    • Changing name (name mangling) : __variable (self.__variable)

Example 2: Car

class Car:

  def __init__(self, brand, model, year):
    self.brand = brand
    self.model = model
    self._year = year
    
# Main Program (outside of class)  
my_car = Car("Porsche", "911 Carrera", 2020)  

# Will display a warning if try to access non-public attribute
my_car._year = 5600

print(my_car._year)

Example 3: Student

class Student:

  def __init__(self, student_id, name, age, gpa):
    self.student_id = student_id
    self.name = name
    self._age = age
    self.gpa = gpa
    
# Main Program (outside of class)  
student_nora = Car("245AFS", "Nora Nav", 15, 3.96)  

# Will display a warning if try to access non-public attribute | Student object has no attribute age
print(student_nora.age)

# In theory you shouldn't access non-public attribute but technically you can as attribute is never private in Python
print(student_nora._age)

Example 4: Backpack

class Backpack:

  max_num_items = 10

  def __init__(self):
    self._items = []
    
# Main Program (outside of class)  
my_backpack = Backpack() 

# Error - Backpack object has no attribute items
print(my_backpack.items)

# Warning - Will display a warning if try to access non-public attribute
print(my_backpack._items)

Example 5: Movie

class Movie:

  # Class attributes shared across all instances
  id_counter = 1

  def __init__(self, title, year, language, rating):
    # Non-public instance attribute
    self._id = Movie.id_counter
    
    # Public instance attribute
    self.title = title
    self.year = year
    self.language = language    
    self.rating = rating
    
    # Increment the class attribute or counter
    Movie.id_counter += 1
    
    
my_movie = Movie("Sense and Sensibility", 2005, "English", 4.5)
your_movie = Movie("Lengends of the Fall", 1995, "English", 4.7)
 
 
# Error - Movie object has no attribute id
print(my_movie.id)
print(your_movie.id)
 
# Warning - Will display a warning if try to access non-public attribute
print(my_movie._id)
print(your_movie._id)

17. Name Mangling

Notes:

  • Since there is a valid use-case for class-private members (namely to avoid name clashes of names with names defined by subclasses), there is limited support for such a mechanism, called name mangling. Any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped.
# __attribute is transformed to _class__attribute
# __engine_serial_num : _Car__engine_serial_num

# But you can still access _Car__engine_serial_num as no variable is private in Python

Example 1: Backpack

class Backpack:

  max_num_items = 10

  def __init__(self):
    self.__items = ["Water Bottle", "First Aid Kit"]
    
# Main Program (outside of class)  
my_backpack = Backpack() 

# Error - Backpack object has no attribute items
print(my_backpack.items)

# Error - Backpack object has no attribute items
print(my_backpack._items)

# Error - Backpack object has no attribute items
print(my_backpack.__items)

# Warning - Will display a warning if try to access non-public attribute
print(my_backpack._Backpack__items)

18. GETTERS & SETTERS

Getters & Setters - Members of a class, particularly they’re methods

Methods - Are like functions associated to a specific object or a class

  • Acts as intermediary to access attributes Getters - Get the value of an attribute Setters - Set the value of an attribute
# get_ + attribute

# Examples:

get_name
get_address
get_color
get_age
get_id

Example 1: Movie

class Movie:

  # Class attributes shared across all instances
  id_counter = 1

  def __init__(self, title, rating):
    # Non-public instance attribute
    self._id = Movie.id_counter
    
    # Public instance attribute
    self._title = title 
    self.rating = rating
    
    # Increment the class attribute or counter
    Movie.id_counter += 1
  
  # Getters
  def get_title(self):
    return self._title
    
    
my_movie = Movie("The Godfather", 4.8)
 
# Error - Movie object has no attribute title
print(my_movie.title)
 
# Correct Way
print("My favorite movie is: ", my_movie.get_title())

Setters - Methods that we can call to set the value of an instance attribute

Notes: Advantage - With setters we can validate the new value before assigning it to the attribute

set_ +

Example 2: Dog

class Dog:
  
  def __init()__(self, name, age)
    self._name = name
    self.age = age
  
  def get_name(self):
    return self._name
  
  def set_name(self, new_name):
    # Setters allow validation of values allowed
    if isinstance(new_name, str) and new_name.isalpha():
      self._name = new_name
    else:
      print("Please enter a valid name.")

  
my_dog = Dog("Nora", 8)
  
print("My dog is:", my_dog.get_name())
# My dog is: Nora

my_dog.set_name("Norita")
  
print("Her new name is:", my_dog.get_name())
# Her new name is: Norita 
  
my_dog.set_name("4567")
  
print("Her new name is:", my_dog.get_name())
# Please enter a valid name
# Her new name is: Norita 
  

Example 3: Backpack

class Backpack:

  def __init__(self):
    self.__items = []
  
  def get_items(self):
    return self._items
  
  def set_items(self, new_items):
    # Setters allow validation of values allowed
    if isinstance(new_name, list):
      self._items = new_items
    else:
      print("Please enter a valid list of items.")  
    
# Main Program (outside of class)  
my_backpack = Backpack() 

# Get initial list of items
print(my_backpack.get_items())

# Message - Please enter a valid list of items.
my_backpack.set_items("Hello, world!"])

Example 4: Circle

class Circle:

  def __init__(self, radius):
    self.__radius = radius
  
  def get_radius(self):
    return self._radius
  
  def set_radius(self, new_radius):
    # Setters allow validation of values allowed
    if isinstance(new_radius, float) and new_radius > 0:
      self._radius = new_radius
    else:
      print("Please enter a valid value for the radius.")  
    
# Main Program (outside of class)  
my_circle = Circle(5.0) 

# 5.0
print(my_circle.get_radius())

my_circle.set_radius(10.5)
  
# 10.5
print(my_circle.get_radius())
  
# Message - Please enter a valid value for the radius.
my_circle.set_radius(0)

19. PROPERTIES

Getters + Setters

Example 1: Dog

class Dog:
  
  def __init()__(self, age)
    self._age = age
  
  def get_age(self):
    return self._age
  
  def set_age(self, new_age):
    # Setters allow validation of values allowed
    if isinstance(new_age, int) and 0 < new_age < 30:
      self._age = new_age
    else:
      print("Please enter a valid age.")
  
  # Connecting age property to the getter and setter methods
  # Use the same name as attribute  
  age = property(get_age, set_age)

  
my_dog = Dog(8)
print(f"My dog age is {my_dog.age} years old.")
print("One year later...")  

# We avoided an error when directly setting age because we defined the propert age and connected it to getter & setter methods
# More concise than my_dog.set_age = my_dog.get_age + 1
my_dog.age += 1  
  
print(f"My dog age is {my_dog.age} years old.")  
class Circle:
  
  # Class Contant
  # Write in caps to indicate other developers that it should be treated as constant
  # Define using tuple as tuples are immutable
  VALID_COLORS = ()

  def __init__(self, radius, color):
    self.__radius = radius
  
  def get_radius(self):
    return self._radius
  
  def set_radius(self, new_radius):
    # Setters allow validation of values allowed
    if isinstance(new_radius, int) and new_radius > 0:
      self._radius = new_radius
    else:
      print("Please enter a valid radius.")  
  
  # Connecting age property to the getter and setter methods
  # Use the same name as attribute  
  radius = property(get_radius, set_radius)  
  
  def get_color(self):
    return self._color
  
  def set_color(self, new_color):
    # Setters allow validation of values allowed
    if new_color in Circle.VALID_COLORS:
      self._color = new_color
    else:
      print("Please enter a valid color.")    
  
  # Connecting age property to the getter and setter methods
  # Use the same name as attribute  
  color = property(get_color, new_color)    
    
# Main Program (outside of class)  
my_circle = Circle(10, "Blue") 

# Radius
print(my_circle.radius)
my_circle.radius = 0
# Attribute is protected by setter
print(my_circle.radius)
  
  
# Color
print(my_circle.color)
my_circle.radius = "Red"
# Attribute is protected by setter
print(my_circle.color)
  
my_circle.radius = "White"
# Attribute is protected by setter
# Color not in the list of valid colors
# Message - Please enter a valid value for the color.
print(my_circle.color)

@property DECORATOR

Decorator - A function that takes a function and extends its behavior without explicitly modifying it

@property syntax is more concise and readable

Example 2: Movie

class Movie:

  def __init__(self, title, rating):    
    # Public instance attribute
    self._title = title 
    self.rating = rating
  
  # PROPERTY  
  ## First define getter
  @property
  def rating(self):
    print("Calling the getter...")
    return self._rating

  ## Then define setter
  @rating.setter
  def rating(self, new_rating):
    print("Calling the setter...")
    if isinstance(new_rating, float) and 1.0 <= new_rating <= 5.0:
      self._rating = new_rating
    else:
      print(favorite_movie.rating)
    
    
my_movie = Movie("Titanic", 4.3)
print(my_movie.rating)
 
# Message - rating is not valid
my_movie.rating = -5.6

20. Methods

Class defined the state + behavior of the object

Methods determine the behavior of the objects created from a class and how they can interact with their state and other objects Method is a function which belongs to an object

3 main types of methods:

    1. Instance Methods - Methods that belong to a particular object because they’ve access to the state of the object
    1. Class Methods
    1. Static Methods

Instance Methods

Notes:

  • Self

Structure

class MyClass:
  
  # Class Attributes
  
  # __init()__
  
  def method_name(self, param1, param2, param3=value):
    # code

Attribute names usually include nouns
Method names usually include verbs since they represent actions

Example 1: Circle

class Circle:
  
  def __init()__(self, radius):
    self.radius = radius
  
  def find_diameter(self):
    print(f"Diameter: {self.radius * 2}")
    return self.radius * 2
  

Example 2: Backpack

class Backpack:

  def __init__(self):
    self.items = []
  
  def add_item(self, item):
    if isinstance(item, str):
      self._items.append(item)
    else:
      print("Please provide a valid item.")
  
  def remove_item(self, item)
    if item in self._items:
      self._items.remove(item)
      return 1
    else:
      print("This item is not in the backpack.")
      return 0
  
  def has_item(self, item):
    return item in self._items
  

my_backpack = Backpack()
print(my_backpack.items)
# []

# Before camping trip add items to the backpack
my_backpack.add_item("Water Bottle")
print(my_backpack.items)
# ["Water Bottle"]
  
my_backpack.add_item("Sleeping Bag")
print(my_backpack.items)
# ["Water Bottle", "Sleeping Bag"]
  
has_water = my_backpack.has_item("Water Bottle")
print(has_water)
# True

# After camping trip remove items from the backpack
my_backpack.remove_item("Sleeping Bag")
print(my_backpack.items)
# ["Water Bottle"]
  
my_backpack.remove_item("Water Bottle")
print(my_backpack.items)
# []

Example 3: Method of a List

# append, sort, pop and extend are methods of a list  
my_list = [4, 5, 6, 7, 8]
  
my_list.append(14)
my_list.sort()
my_list.pop()
my_list.extend([1, 2, 3])

Example 4: Circle

class Circle:
  
  def __init()__(self, radius):
    self.radius = radius
  
  def find_diameter(self):
    print(f"Diameter: {self.radius * 2}")
    return self.radius * 2

  
my_circle = Circle(5)
diameter = my_circle.find_diameter()
print(diameter)  

21. DEFAULT ARGUMENTS TO METHODS

Example 1: Player who can move

class Player:
  
  def __init()__(self, x, y):
    self.x = x
    self.y = y
  
  def move_up(self, change=5):
    self.y += change

  def move_down(self, change=5):
    self.y -= change

  def move_right(self, change=2):
    self.x += change
  
  def move_left(self, change=2):
    self.x -= change
  
my_player = Player(5, 10)
  
print(my_player.y) 
# will return 10
  
my_player.move_up()
print(my_player.y)  
# will return 15
  
my_player.move_up(8)
print(my_player.y)  
# will return 23

Example 2: Backpack

class Backpack:

  def __init__(self):
    self.items = []
  
  def add_item(self, item):
    if isinstance(item, str):
      self._items.append(item)
    else:
      print("Please provide a valid item.")
  
  def remove_item(self, item)
    if item in self._items:
      self._items.remove(item)
      return 1
    else:
      print("This item is not in the backpack.")
      return 0
  
  def has_item(self, item):
    return item in self._items

  def show_items(self, sorted_list=False):
    if sorted_list:
      print(sorted(self._items))
    else:
      print(self._items)

my_backpack = Backpack()
print(my_backpack.items)
# []

# Before camping trip add items to the backpack
my_backpack.add_item("Water Bottle")
my_backpack.add_item("Sleeping Bag")
my_backpack.add_item("Candy")

print("Not Sorted:"")
my_backpack.show_items()
["Water Bottle", "Sleeping Bag", "Candy"] 
  
print("Sorted:"")
my_backpack.show_items(True)
["Candy", "Sleeping Bag", "Water Bottle"] 

Calling Methods from another Method

Example 1: Backpack

class Backpack:

  def __init__(self):
    self.__items = []
  
  @property
  def items(self):
    return self._items
  
  def add_multiple_items(self, items):
    for item in items:
      self.add_item(item)
  
  def add_item(self, item):
    if isinstance(item, str):
      self._items.append(item)
    else:
      print("Please provide a valid item.")
  
  @property.setter
  def items(self, new_items):
    # Setters allow validation of values allowed
    if isinstance(new_name, list):
      self._items = new_items
    else:
      print("Please enter a valid list of items.")  
    
# Main Program (outside of class)  
my_backpack = Backpack()
print(my_backpack.items)
# []

# Before camping trip add items to the backpack
my_backpack.add_items(["Water Bottle","Candy"])
print(my_backpack.items)
# ["Water Bottle","Candy"]

22. AGGREGATION

  • Aggregation - Concept in OOP that describes relationship between two classes
  • Aggregation - Build complex objects from simple objects of simple classes
  • Aggregation - Class B’s instance uses some functionality of Class A’s instance

Example: Vehicle & Employee

  • Employee has a Vehicle
  • Two separate Classes
# Vehicle class  
class Vehicle:
  
  def __init()__(self, color, license_plate, is_electric):
    self.color = color
    self.license_plate = license_plate
    self.is_electric = is_electric
  
  def show_license_plate(self):
    print(self.license_plate)
  
  def show_info(self):
    print("My vehicle:")
    print(f"Color: {self.color}")
    print(f"License Plate: {self.license_plate}")
    print(f"Electric: {self.is_electric}")
  

# Emploee class
class Employee:
    
  def __init__(self, name, vehicle):
    self.name = name
    self.vehicle = vehicle
  
  def show_vehicle_info(self):
    self.vehicle.show_info()
  

# more readable than just writing False
employee_vehicle = Vehicle("black", "AXY 245", is_electric=False)
  
employee = Employee("Gino", employee_vehicle)
  
print(employee.name)
# vehicle instance inside employee instance
print(employee.vehicle)
  
employee.show_vehicle_info()
  
employee.vehicle.show_info()
  
print(employee.vehicle.color)
print(employee.vehicle.license_plate)
print(employee.vehicle.is_electric)
  
employee.vehicle.show_license_plate()

23. COMPOSITION

Create the instance of Engine inside the instance of Car. Engine object cannot exist without Car object

24. PROJECT: Build a Dice Game with Python OOPS

Game:

  • 2 players - human and computer
  • 2 dice
  • Roll the dice; record the value; compare the value; player with the greater value wins the round; decrease the counter of winner by 1; increase the counter of loser by 1; if values are equal no winner and counter remains unchanged; the one whose counter reaches zero is winner
  • Dice Class :
    • attributes - value
    • methods - roll
    • instances - player_die, computer_die
  • Player Class :
    • attributes - counter, die (Composition), is_computer
    • methods - increment_counter, decrement_counter, roll_die
    • instances - my_player, computer_player
  • Game Class :
    • attributes - [player, computer] (Composition)
    • methods - play, play_round, check_game_over
    • instance - game
  • Interactivity - Human player to roll the round
  • Messages - Game Starts, Round Starts, Game Ends, Value of Dice, Mention the Winner, Mention the Counter

Die Class

import random
  
  
class Die:
  
  def __init__(self):
    # make value non-public by adding underscore
    self._value = None
  
  # define getter
  @property
  def value(self):
    return self._value
  
  # not-defining setter because user only has access to reading the value
  
  def roll(self):
    new_value = random.randint(1, 6)
    self._value = new_value
    return new_value
  
  
# Testing the class
die = Die()

# should return None
print(die.value)
  
die.roll()
  
# should return an integer between 1 and 6
print(die.value)  

Player Class

class Player:
  
  def __init__(self, die, is_computer=False):
    # using "Composition" as die is instance of Die class
    self._die = die
    self._is_computer = is_computer
    self._counter = 10
  
  @property
  def die(self):
    return self._die
  
  @property
  def counter(self):
    return self._counter
  
  def increment_counter(self):
    self._counter += 1
  
  def decrement_counter(self):
    self._counter -= 1
  
  def roll_die(self):
    # using "Aggregation" by calling roll method of Die class inside Player class
    return self._die.roll()
    
  
# Testing the class
my_die = Die()
  
my_player = Player(my_die, is_computer=True)

# check the data type of my_player
print(my_player)
  
# check the die attribute of player
print(my_die)
print(my_player.die)
 
# check if the player is computer
print(my_player.is_computer)
  
# check the player counter
print(my_player.counter)
  
print(my_player.counter)

# check the increment method
my_player.increment_counter()
print(my_player.counter)

# check the decrement method
my_player.decrement_counter()
print(my_player.counter)
  
# check the roll method
print(my_die.value)
my_player.roll_die()
print(my_die.value)
print(my_player.die.value)

Game Class

class  DiceGame:

  def __init__(self, player, computer):
    self._player = player
    self._computer = computer
  
  # we will not write any getters & setters for player and computer as they should not be accessed outside the class
  
  # entry point to game
  def play(self):
    print("================================================================")
    print("🎲	Welcome to Roll the Dice!")
    print("================================================================")
    while True:
      self.play_round()
      game_over = self.check_game_over()
      if game_over:
        break
      
  
  def play_round(self):
    # Welcome the user
    self.print_round_welcome()
  
    # Roll the dice
    player_value = self._player.roll_die()
    computer_value = self._computer.roll_die()
  
    # Show the values
    self.show_dice(player_value, computer_value)
  
    # Determine the winner and loser of round
    if player_value > computer_value:
      print("You won the round! 🎊")
      self.update_counters(winner=self._player, loser=self._computer)
  
    elif computer_value > player_value:
      print("The computer won this round. Try again. 😦")
      self.update_counters(winner=self._computer, loser=self._player)
  
    else:
      print("It's a tie! 😎")
    
    # Show counters
    self.show_counters()

  
  def print_round_welcome(self):
    print("\n--------------------- New Round ----------------------")
    input("🎲	Press any key to roll the dice. 🎲")
  
  def show_dice(self, player_value, computer_value):
    print(f"Your die: {player_value}\n")
    print(f"Computer die: {computer_value}\n")   
  
  def update_counters(self, winner, loser):
  # will show warning since not using self. but its fine
    winner.decrement_counter()
    loser.increment_counter()
  
  def show_counters(self):
    print(f"\nYour counter: {self._player.counter}")
    print(f"\nComputer counter: {self._computer.counter}")    
    
  def check_game_over(self):
    if self._player.counter == 0:
      self.show_game_over(self._player)
      return True
    elif self._computer.counter == 0:
      self.show_game_over(self._computer)  
      return True
    else:
      return False
  
  def show_game_over(self, winner):
    if winner._is_computer:
      print("\n=========================")
      print(" G A M E  O V E R ✨")
      print("===========================")
      print("The computer won the game. Sorry...")
      print("================================")
    else:
      print("\n=========================")
      print(" G A M E  O V E R ✨")
      print("===========================")
      print("You won the game! Congratulations.")
      print("================================")    


# Play the game infintely
while True:
  # Testing the class    
  
  player_die = Die()
  computer_die = Die()
    
  my_player = Player(player_die, is_computer=False)
  computer_player = Player(computer_die, is_computer=True)
      
  game = Game()  
    
  # Start the game
  game.play()
  

25. OBJECTS IN MEMORY

In Python, everything is an object

# object is the base class
print(object)
  
print(isinstance(5, object))
  
print(isinstance([1, 5, 2, 6], object))
  
print(isinstance((1, 5, 2, 6), object))
  
print(isinstance("Hello, World!", object))
  
print(isinstance(True, object))
  
def f(x):
  return x * 2
 
print(isinstance(f, object))
  
class Movie:
  
  def __init__(self, title):
    self.title = title
  
print(isinstance(Movie, object))  

Notes:

  • An object is stored in memory with a unique id
  • Variables in Python store references to objects in memory

id() Function

# id(object)  
print(id(15))
print(id("Hello, World!"))

Example 1: Backpack

class Backpack:
  
  def __init()__(self):
    self._items = []
  
  @property
  def items(self):
    return self._items
  
  
my_backpack = Backpack()
your_backpack = Backpack()
  
print(id(my_backpack))
print(id(your_backpack))

“is” Function & “is not” Function

# object1 is object2
# object1 is not object2
  
# is operator checks the memory location of object
# == operator checks the value of object
  
a = [1, 6, 2, 6]
b = [1, 6, 2, 6]

# print memory location
print(id(a))
print(id(b))

# False
print(a is b)
print(id(a) === id(b))

# True
print(a == b) 
a = [5, 2, 1, 8, 3]
b = a

# True
print(a is b)  

“is” opeartor unexpected results depending upon Python version and programming environment

a = 5
b = 5
  
# small integers in the range [-5, 256] - just get a reference to existing objects in the memory
# True
print(a is b)
  
a = 257
b = 257
  
# You could also get this True in Pycharm environment due to its memory optimization
# True
print(a is b)
  
# "String Interning" - strings are immutable and doesn't makes sense to create separate copy in memory
# a,b,c reference the same object
a = "Hi"
b = "Hi"
c = "Hi"
  
# True
print(a is b)
print(id(a))
print(id(b))
print(id(c))
my_list = [6, 2, 8, 2]
  

# print
def print_data(seq):
  print("Inside the function:", id(seq))
  for elem in seq:
    print(elem)
  
  
print("Outside the function:", id(my_list))
print_data(my_list)
  

# mutiply
def multiply_by_two(seq):
  print("Inside the function:", id(seq))
  for i in range(len(seq)):
    seq[i] *= 2
  

print("Outside the function:", id(my_list))
multiply_by_two(my_list)  
  
print(my_list)

26. combine object oriented programming with imperative programming

# Class Sale
class Sale:
  
  def __init__(self, amount):
    self.amount = amount
  

# Function outside a class
def find_total(sales):
  total = 0
  for sale in sales:
    print("New sale...")
    total += sale.amount 
  return total
  
  
january_sales = [Sale(400), Sale(345), Sale(45)]

print(find_total(january_sales))

27. ALIASING, MUTATION & CLONING

Aliasing - different name assigned to the same object

Notes:

  • Implications - if you make changes to one alias you will end up changing all the aliases and underlying object
a = [6, 2, 3, 4]
b = a
c = b
d = c

# True
print(a is b is c is d)
  
print(id(a))
print(id(b))
print(id(c))
print(id(d))
class Circle:
  
  def __init__(self, radius):
    self.radius = radius
  
  
my_circle = Circle(4)
your_circle = my_circle
  
# True
print(my_circle is your_circle)
  
print(id(my_circle))
print(id(your_circle))

# Both are 4
print("Before:...")
print(my_circle.radius)
print(your_circle.radius)
  
my_circle.radius = 18

# Both updated to 18
print("After:...")
print(my_circle.radius)
print(your_circle.radius)

Mutability & Immutability

  • Example of mutable object ```python a = [7, 3, 2, 1] a[0] = 5

will return [5, 3, 2, 1]

print(a)

  
* Example of immutable object - tuple, string, boolean, integer, float
```python
a = (7, 3, 2, 1)
  
# error: 'tuple' object doesn't supports item assignment
a[0] = 5
  
  • Advantages VERSUS Disadvantages

Mutable Objects:

  • Don’t have to create new copy and can be modified in memory
  • Might introduce bugs as you might unintentionally mutate
def add_absolute_values(deq):
  for i in range(len(seq)):
    # example bug - we are accidentally mutating the list and that is not what we intended
    seq[i] = abs(seq[i])
  return sum(seq)
  
values = [-5, -6, -7, -8]
  
print("Values Before:", values)
  
result = add_absolute_values(values)
  
print("Values After:", values)

Potential risk of aliasing:

  • We might unintentially mutate the object and affect all other aliases
a = [1, 2, 3, 4]
b = a
  
b[0] = 15
  
print(b)

# list a is also unintentionally modified
print(a)

Imutable Objects:

  • Safe from bugs
  • Easier to understand and know their exact values
  • Less efficient in terms of memory usage as you need to create a new copy of object
a = (1, 2, 3, 4)
print(id(a))
  
a = a[:2] + (7, ) + a[2:]
  
# will return (1, 2, 7, 3, 4)
print(a)
  
# a is now refering to new tuple
print(id(a))
def remove_even_values(dictionary):
  for key, value in dictionary.items()
    if value % 2 == 0:
      del dictionary[key]
  
my_dict = {"a":1, "b":2, "c":3, "d":4}
  
# runtime error: dictionary changed size during iteration
remove_even_values(my_dict)
  
# now loop doesn't know how many iterations are left as its size changed

Cloning - creating an exact copy of an object and when you modify the clone, original is not affected

a = [1, 2, 3, 4]
# syntax to clone a list
b = a[:]

b[0] = 15

# a is not affected
# [1, 2, 3, 4]
print(a)
  
# [15, 2, 3, 4]
print(b)
def remove_even_values(dictionary):
  for key, value in dictionary.copy().items()
    if value % 2 == 0:
      del dictionary[key]
  
my_dict = {"a":1, "b":2, "c":3, "d":4}
  
# no runtime error: since we created a copy
remove_even_values(my_dict)
  
# {"a":1, "c":3|

28. PROJECT: Tic-Tac-Toe project

move.py

class Move:
  
  def __init__(self, value):
    self._value = value
  
  @property
  def value(self):
    return self._value
  
  def is_valid(self):
    return isinstance(self._value, int) and 1 <= self._value <= 9
  
  def get_row(self):
    if self._value in (1, 2, 3):
      return 0  # First row
    elif self._value in (4, 5, 6):
      return 1  # Second row
    else:
      return 2  # Third row
      
  
  def get_column(self):
    if self._value in (1, 4, 7):
      return 0  # First column
    elif self._value in (2, 5, 8):
      return 1  # Second column
    else:
      return 2  # Third column  


# | 1 | 2 | 3 |  
# | 4 | 5 | 6 |  
# | 7 | 8 | 9 |  
  
# Testing the Move class
move = Move(6)
  
# Should return True for 6
print(move.is_valid())
 
# Should return row 1 for 6
print(move.get_row())

# Should return column 2 for 6
print(move.get_column())

player.py

import random  
from move import Move  

  
class Player:
  
  PLAYER_MARKER = "X"
  COMPUTER_MARKER = "O"
  
  def __init__(self, is_human=True):
    self._is_human = is_human
  
    if is_human:
      self._marker = PLAYER_MARKER
    else:
      self._marker = COMPUTER_MARKER
  
  @property
  def is_human(self):
    return self._is_human
  
  @property
  def marker(self):
    return self._marker
  
  def get_move(self):
    if self._is_human:
      return self.get_human_move()
    else:
      return self.get_computer_move()
  
  def get_human_move()
    while True:
      user_input = int(input("Please enter your move (1-9): "))
      move = Move(user_input)
      if move.is_valid():
        break
      else:
        print("Please enter an integer between 1 and 9.")
    return move
  
  def get_computer_move():
    random_choice = random.choice(list(range(1, 10)))  # 10 is excluded
    move = Move(random_choice)
    print("Computer move (1-9):", move.value)
    return move

  
# Testing the Player class
player = Player(is_human=True)  # Human player
  
# Should return True
print(player.is_human())
 
# Should return "X"
print(player.marker())

# Should trigger get_human_move()
move = player.get_move()
# "Please enter your move (1-9): "
print(move.value)
  

computer = Player(is_human=False)  # Computer player
  
# Should return False
print(computer.is_human())
 
# Should return "0"
print(computer.marker())
  
# Should trigger get_computer_move()
move = computer.get_move()
# "Computer move (1-9):", move.value  

board.py

from move import Move
from player import Player
  
class Board:
  
    EMPTY_CELL = 0
  
    def __init__(self):
      self.game_board = [[0, 0, 0], 
                         [0, 0, 0], 
                         [0, 0, 0]]
  
    def print_board(self):
      print("\nPositions:")
      self.print_board_with_positions()
  
      print("Board:")
      for row in self.game_board:
        print("|", end="")
        for column in row:
          if columns == Board.EMPTY_CELL:
            print("   |", end="")
          else:
            print(f" {column} |", end="")
        print()
      print()
      
    def print_board_with_positions(self):
      print("| 1 | 2 | 3 |\n| 4 | 5 | 6 |\n| 7 | 8 | 9 |")
  
    def submit_move(self, player, move):
      row = move.get_row()
      col = move.get_column()
      value = self.game_board[row][col]
  
      if value == Board.EMPTY_CELL:
        self.game_board[row][col] = player.marker
      else:
        print("This position is already taken. Please enter another one.")
  
    def check_is_game_over(self, player, last_move):
      return ((self.check_row(player, last_move) or 
              (self.check_column(player, last_move) or 
              (self.check_diagonal(player) or 
              (self.check_antidiagonal(player))
  
    def check_row(self, player, last_move):
      row_index = last_move.get_row()
      baord_row = self.game_board[row_index]
  
      return baord_row.count(player.marker) == 3
  
    def check_column(self, player, last_move):
      markers_count = 0
      column_index = last_move.get_column()
      
      for i in range(3):
        if self.game_board[i][column_index] == player.marker:
          markers_count += 1
  
      return markers_count == 3  
  
    def check_diagonal(self, player):
      markers_count = 0
  
      for i in range(3):
        if self.game_board[i][i] == player.marker:
          markers_count += 1  
  
      return markers_count == 3 

    def check_antidiagonal(self, player):
      markers_count = 0
  
      for i in range(3):
        if self.game_board[i][2 - i] == player.marker:
          markers_count += 1  
  
      return markers_count == 3 
  
    def check_is_tie(self):
      empty_counter = 0
  
      for row in self.board_game:
        empty_counter += row.count(Board.EMPTY_CELL)
  
      return empty_counter == 0
  
    def reset_board(self):
      self.game_board = [[0, 0, 0], 
                         [0, 0, 0], 
                         [0, 0, 0]]    
  
# Testing the Board class
board = Board()
player = Player()
  
move1 = player.get_move()
move2 = player.get_move()
move3 = player.get_move()

board.print_board()

# any of below choices will finish the game; should return True
# 1,2,3 | 4,5,6 | 7,8,9
# 1,4,7 | 2,5,8 | 3,6,9
# 1,5,9 | 3,5,7
  
# any of below choices will not finish the game; should return False
# 1,2,4
  
board.submit_move(player, move1)  
board.submit_move(player, move2) 
board.submit_move(player, move3)  

board.print_board()
board.check_is_game_over()

# Testing the Tie and Reset Board
board = Board()
player = Player()
computer = Player(False)

board.print_board()
  
while not board.check_is_tie():
  human_move = player.get_move()
  board.submit_move(player, human_move)
  
  board.print_board()
  
  computer_move = computer.get_move()
  board.submit_move(computer, computer_move)
  
  board.print_board()
  
print("It's a tie!")

board.reset_board()
board.print_board()

game.py

from board import Board
from player import Player
  
class TictactoeGame:
  
  def start(self):
    print("********************************")
    print("   Welcome to Tic-Tac-Toe   ")
    print("********************************")
  
    board = Board()
    player = Player()
    computer = Player(False)
  
    board.print_board()
  
    # Ask the user if he/she would like to
    # play another round.
  
    while True:  # Game
  
      while True:  # Round
  
        player_move = player.get_move()
        board.submit_move(player, player_move)
        board.print_board()
  
        if board.check_is_tie():
          print("It's a tie! 👍 Try again.")
          break
        elif board.check_is_game_over(player, player_move):
          print("Awesome! You won the game. 🎊")
          break
        else:
          computer_move = computer.get_move()
          board.submit_move(computer, computer_move)
          board.print_board()
  
          if board.check_is_game_over(computer, computer_move):
            print("Oops... 😦 The computer won. Try again.")
            break
  
      play_again = input("Would you like to play again? Enter X for YES or O for NO").upper()
      
      if play_again == "O":
        print("Bye! Come back soon 👋")
        break
      elif play_again == "X":
        self.start_new_round(board)
      else:
        print("Your input was invalid but I will assume that you want to play again. 💡")
        self.start_new_round(board)
  
  
    def start_new_round(self, board):
      print("*************************")
      print("   New Game   ")
      print("*************************")
      board.reset_game()
      board.print_board()

  
# Testing the game
game - TicTacToeGame()
game.start()

29. INHERITANCE

Definition

  • Defining classes that inherit attributes and methods from other classes

Advantages:

  • Reduce code repetition
  • Reuce code
  • Improve readability

Don’t repeat yourself

    • like sqaure/triangle classes have common attributes sides/color and common methods display side length/color
  • Can inherit attributes and methods from a general class like Polygon
  • Classes generally inherits from more general classes which represent more abstract concept
  • Define common functionality in common class and add new/customize functionality in child class
  • Polygon - Triangle / Square
  • Vehicle - Car / Truck
  • Dessert - Brownie / Ice Cream
  • Teaching Assistant Class inheriting fromm multiple classes - Students & Faculty
  • Vehicle –> Land Vehicle –> Car, Truck

Attribute Inheritance

class Superclass:
  pass
  
class Subclass(Superclass):
  pass
class Polygon:
  
  def __init__(self, num_sides, color):
    self.num_sides = num_sides
    self.color = color
  
  
class Triangle(Polygon):
  # if no init method attributes of superclass are inherited by default
  pass
  

my_triangle = Triangle(3, "Blue")  
  
print(my_triangle.num_sides)
print(my_triangle.color)
  

class Triangle(Polygon):
  
  # define num_sides as class constant
  NUM_SIDES = 3
  
  # if has an init method attributes of superclass are not inherited by default
  def __init__(self, base, height, color):
    Polygon.__init__(self, Triangle.NUM_SIDES, color) # super().__init__(self, Triangle.NUM_SIDES, color)
    self.base = base
    self.height = height
  

my_triangle = Triangle(5, 4, "Blue")  
  
print(my_triangle.num_sides)
print(my_triangle.color)
print(my_triangle.base)
print(my_triangle.height)
  
  
class Square(Polygon):
  pass
class Employee:
  
  def __init__(self, full_name, salary):
    self.full_name = full_name
    self.salary = salary
  

class Programmer(Employee):
  
  def __init__(self, full_name, salary, programming_language):
    Employee.__init__(self, full_name, salary)
    self.programming_language = programming_language

  
nora = Programer("Nora Nav", 60000, "Python")
  
print(nora.full_name)
print(nora.salary)
print(nora.programming_language)
class Character:
  
  def __init__(self, x, y, num_lives):
    self.x = x
    self.y = y
    self.num_lives = num_lives

  
class Player(character):
  
  INITITAL_X = 0
  INITIAL_Y - 0
  INITIAL_NUM_LIVES = 10
  
  def __init__(self, score=0):
    Character.__init__(self, Player.INITITAL_X, Player.INITIAL_Y, Player.INITIAL_NUM_LIVES)
    self.score = score
  

# we should be able to customize enemy attributes
class Enemy(character):
  
  def __init__(self, x=15, y=15, num_lives=0, is_poisonous=False):
    Character.__init__(self, x, y, num_lives)
    self.is_poisonous = is_poisonous
  

Method Inheritance

class Superclass:
  pass
  
class Subclass(Superclass):
  pass  
class Polygon:
  
  def __init__(self, num_sides, color):
    self.num_sides = num_sides
    self.color = color
  
  def describe_polygon(self):
    print(f"This polygon has {self.num_sides} sides")
  
  
class Triangle(Polygon):
  
  # define num_sides as class constant
  NUM_SIDES = 3
  
  # if has an init method attributes of superclass are not inherited by default
  def __init__(self, base, height, color):
    Polygon.__init__(self, Triangle.NUM_SIDES, color) # super().__init__(self, Triangle.NUM_SIDES, color)
    self.base = base
    self.height = height
  
  def find_area():
    return (self.base * self.height) / 2 
  

class Square(Polygon):
  
  # define num_sides as class constant
  NUM_SIDES = 4
  
  # if has an init method attributes of superclass are not inherited by default
  def __init__(self, side_length, color):
    Polygon.__init__(self, Square.NUM_SIDES, color) # super().__init__(self, Square.NUM_SIDES, color)
    self.side_length = side_length
  
  def find_area():
    return (self.side_length ** 2)
  

my_triangle = Triangle(5, 4, "Blue")
my_triangle.describe_polygon()
  
my_square = Square(4, "Green")
my_square.describe_polygon()

30. IMPORT STATEMENTS IN PYTHON

import math


class Circle:

    def __init__(self, radius):
        self.radius = radius

    def find_area(self):
        return math.pi * (self.radius ** 2)
  
  
import random


class Die:

    def __init__(self, value):
        self.value = value

    def roll_die(self):
        random_value = random.randint(1, 6)
        self.value = random_value


my_die = Die(4)

print(my_die.value)
my_die.roll_die()
print(my_die.value)

  
  
from math import pi


class Circle:

    def __init__(self, radius):
        self.radius = radius

    def find_area(self):
        return pi * (self.radius ** 2)
  
  
from random import randint


class Die:

    def __init__(self, value):
        self.value = value

    def roll_die(self):
        random_value = randint(1, 6)
        self.value = random_value


my_die = Die(4)

print(my_die.value)
my_die.roll_die()
print(my_die.value)
  
  
from math import *

print(pi)
print(sin(54))
print(cos(34))

31. DOCSTRINGS

Documentation Strings

  • Doctrings unlike comments are not linked to the elements they describe
  • Can be used to generate documentation automatically
  • Can be read via help() function
  • two types : one-line & multi-line
  • DocString ends with period

Example 1:

help(len)
print(len.__doc__)

help(sorted)
help(sorted.__doc__)

help(list.sort)
print(list.sort.__doc__)

def add(a, b):
    """Return the sum of a and b."""
    return a + b

print(add.__doc__)

help(tuple.count)

help(str.capitalize)

print(list.sort.__doc__)

Example 2:

def print_floyds_triangle(n):
    """Print Floyd's Triangle with n rows."""
    count = 1

    for i in range(1, n+1):
        for j in range(i):
            print(count, end=" ")
            count += 1
        print()

Example 3:

def make_frequency_dict(sequence):
    """Return a dictionary that maps each element in sequence to its frequency.

    Create a dictionary that maps each element in the list sequence
    to the number of times the element occurs in the list. The element
    will be the key of the key-value pair in the dictionary and its frequency
    will be the value of the key-value pair.

    Argument:
        sequence: A list of values. These values have to be of an
            immutable data type because they will be assigned as the keys
            of the dictionary. For example, they can be integers, booleans,
            tuples, or strings.

    Return:
        A dictionary that maps each element in the list to its frequency.
        For example, this function call:

        make_frequency_dict([1, 6, 2, 6, 2])

        returns this dictionary:

        {1: 1, 6: 2, 2: 2}

    Raise:
        ValueError: if the list is empty.
    """
    if not sequence:
        raise ValueError("The list cannot be empty")

    freq = {}

    for elem in sequence:
        if elem not in freq:
            freq[elem] = 1
        else:
            freq[elem] += 1

    return freq

Example 4:

class Backpack:
	"""A class that represents a Backpack. 

	Attribute:
		items (list): the list of items in the backpack (initially empty).

	Methods:
		add_item(self, item):
			Add the item to the backpack.
		remove_item(self, item):
			Remove the item from the backpack.
		has_item(self, item):
			Return True if the item is in the backpack. Else, return False.
	"""
	def __init__(self):
		self.items = []

	def add_item(self, item):
		self.items.append(item)

	def remove_item(self, item):
		if item in self.items:
			self.items.remove(item)
		else:
			print("This item is not in the backpack")

	def has_item(self, item):
		return item in self.items

Example 5:

import math


class Circle:
    """A class that represents a circle.

    Attributes:
        radius (float): the distance from the center of the circle
            to its circumference.
        color (string): the color of the circle.
        diameter (float): the distance through the center of the circle
            from one side to the other.

    Methods:
        find_area(self):
            Return the area of the circle.
        find_perimeter(self):
            Return the perimeter of the circle.
    """

    def __init__(self, radius, color):
        """Initialize an instance of Circle.

        Arguments:
            radius (float): the distance from the center
                of the circle to its circumference.
            color (string): the color of the circle.
        """
        self._radius = radius
        self._color = color

    @property
    def radius(self):
        """Return the radius of the circle.

        This is a float that represents the distance from
        the center of the circle to its circumference."""
        return self._radius

    @property
    def color(self):
        """Return the color of the circle.

        The color is described by a string that must be capitalized.
        For example: "Red", "Blue", "Green", "Yellow".
        """
        return self._color

    @color.setter
    def color(self, new_color):
        self._color = new_color

    @property
    def diameter(self):
        """Return the diameter of the circle.

        This is a float that represents the distance through
        the center of the circle from one side to the other.
        """
        return 2 * self._radius

    def find_area(self):
        """Find and return the area of a circle.

        The area is calculated with the circle radius
        using the formula Pi * (radius ** 2).
        """
        return math.pi * (self._radius ** 2)

    def find_perimeter(self):
        """Find and return the perimeter of a circle.

        The perimeter is calculated with the circle radius
        using the formula (2 * Pi * radius).
        """
        return 2 * math.pi * self._radius


help(Circle)

print(Circle.__doc__)

Example 6:

help(len)
print(len.__doc__)

help(sorted)
help(sorted.__doc__)

help(list.sort)
print(list.sort.__doc__)

def add(a, b):
    """Return the sum of a and b."""
    return a + b

print(add.__doc__)

help(tuple.count)

help(str.capitalize)

print(list.sort.__doc__)

32. SPECIAL METHODS (Also called magic methods)

Notes:

  • Not called directly
  • Operator Overloading

Examples:

__methodname__()

__add__()
	
__str__()
	
__repr__()
	
__len__()
	
__bool__()
	
__eq__()
	
__lt__()
	
__sizeof__()

Example 1 : Intro to Special Methods

result = 5 + 6
print(result) 

# Behind the scenes
result = (5).__add__(6)
print(result) 	

Example 2 : str

# Example: Point2D

class Point2D:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({x}, {y})"


my_point = Point2D(56, 60)
print(my_point)


# Example: Student

class Student:

    def __init__(self, student_id, name, age, gpa):
        self.student_id = student_id
        self.name = name
        self.age = age
        self.gpa = gpa

    def __str__(self):
        return f"Student: {self.name} " \
               f"| Student ID: {self.student_id} " \
               f"| Age: {self.age} " \
               f"| GPA: {selg.gpa}"


student = Student("42AB9", "Nora Nav", 34, 3.76)
print(student)	

Example 3 : len

# Example: Length of Built-in Data Types

my_string = "Hello, World!"
print(len(my_string))
print(my_string.__len__())

my_list = [1, 2, 3, 4, 5]
print(len(my_list))
print(my_list.__len__())

my_tuple = (1, 2, 3, 4, 5)
print(len(my_tuple))
print(my_tuple.__len__())

my_dict = {"a": 1, "b": 2, "c": 3}
print(len(my_dict))
print(my_dict.__len__())


# Example: Customizing Length

class Backpack:

    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def remove_item(self, item):
        if item in self.items:
            self.items.remove(item)
        else:
            print("This item is not in the backpack")

    def __len__(self):
        return len(self.items)


my_backpack = Backpack()

my_backpack.add_item("Water Bottle")
my_backpack.add_item("First Aid Kit")
my_backpack.add_item("Sleeping Bag")

print(len(my_backpack))

my_backpack.remove_item("Sleeping Bag")
print(len(my_backpack))

my_backpack.remove_item("Water Bottle")
my_backpack.remove_item("First Aid Kit")
print(len(my_backpack))

my_backpack.remove_item("Water Bottle")	

Example 4 : add

# Examples with Built-in Functions

print(3 + 4)
print((3).__add__(4))

print("Hello " + "World!")
print("Hello ".__add__("World!"))

print([1, 2, 3] + [4, 5, 6])
print([1, 2, 3].__add__([4, 5, 6]))


# Example with User-Defined Class

class Point2D:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        new_x = self.x + other.x
        new_y = self.y + other.y
        return Point2D(new_x, new_y)

    def __str__(self):
        return f"({self.x}, {self.y})"


pointA = Point2D(5, 6)
print(pointA)

pointB = Point2D(2, 3)
print(pointB)

pointC = pointA + pointB
print(pointC)	

Example 5 : getitem

# Examples with List Data Type

my_list = ["a", "b", "c", "d"]

print(my_list[0])
print(my_list[1])
print(my_list[2])
print(my_list[3])

print("-----")

print(my_list.__getitem__(0))
print(my_list.__getitem__(1))
print(my_list.__getitem__(2))
print(my_list.__getitem__(3))


# Example with User-Defined Class

class Bookshelf:

    def __init__(self):
        self.content = [[],
                        [],
                        []]

    def add_book(self, book, location):
        self.content[location].append(book)

    def take_book(self, book, location):
        self.content[location].remove(book)

    def __getitem__(self, location):
        return self.content[location] 


my_bookshelf = Bookshelf()

my_bookshelf.add_book("Les Miserables", 0)
my_bookshelf.add_book("Pride and Prejudice", 0)
my_bookshelf.add_book("Frankenstein", 0)

my_bookshelf.add_book("Sense and Sensibility", 1)
my_bookshelf.add_book("Jane Eyre", 1)
my_bookshelf.add_book("The Little Prince", 1)

my_bookshelf.add_book("Moby Dick", 2)
my_bookshelf.add_book("The Adventures of Huckleberry Finn", 2)
my_bookshelf.add_book("Dracula", 2)

print(my_bookshelf[0])
print(my_bookshelf[1])
print(my_bookshelf[2])	

Example 6 : bool

# Example before defining the __bool__() method.

class BankAccount:

    def __init__(self, account_owner, account_number, initial_balance):
        self.account_owner = account_owner
        self.account_number = account_number
        self.balance = initial_balance

    def make_deposit(self, amount):
        self.balance += amount

    def make_withdrawal(self, amount):
        if self.balance - amount >= 0:
            self.balance -= amount
        else:
            print("Insufficient funds.")

my_account = BankAccount("Nora Nav", "356-2456-2455", 45045.23)
print(bool(my_account))

if my_account:
    print("True")
else:
    print("False")

my_account.balance = 0
print(bool(my_account))

if my_account:
    print("True")
else:
    print("False")


# Example after defining the __bool__() method.

class BankAccount:

    def __init__(self, account_owner, account_number, initial_balance):
        self.account_owner = account_owner
        self.account_number = account_number
        self.balance = initial_balance

    def make_deposit(self, amount):
        self.balance += amount

    def make_withdrawal(self, amount):
        if self.balance - amount >= 0:
            self.balance -= amount
        else:
            print("Insufficient funds.")

    def __bool__(self):
        return self.balance > 0

my_account = BankAccount("Nora Nav", "356-2456-2455", 45045.23)
print(bool(my_account))

if my_account:
    print("True")
else:
    print("False")

my_account.balance = 0
print(bool(my_account))
print(my_account.balance)

if my_account:
    print("True")
else:
    print("False")


# Examples of where the length determines the boolean value.

# List
my_list = [1, 2, 3, 4, 5]
print(len(my_list))

if my_list:
    print("The list is not empty.")
    print(bool(my_list))
else:
    print("The list is empty.")
    print(bool(my_list))

# String
my_string = "Hello, World!"
print(len(my_string))

if my_string:
    print("The string is not empty.")
    print(bool(my_string))
else:
    print("The string is empty.")
    print(bool(my_string))	

Example 7 : rich comparison methods

# Examples of Comparison Operators and Built-in Data Types

print(15 <= 8)
print(4 > 4)
print(5 == 5)
print(6 != 8)

print("Hello" < "World")
print("Python" >= "Java")
print("Hello" == "Hello")

print([1, 2, 3, 4] < [1, 2, 3, 5])
print([4, 5, 6] > [1, 2, 3, 4])


# Example: Comparing instances of the Circle Class

class Circle:

    def __init__(self, radius, color):
        self.radius = radius
        self.color = color

    def __lt__(self, other):
        return self.radius < other.radius

    def __le__(self, other):
        return (self.radius <= other.radius
                and self.color == other.color)

    def __gt__(self, other):
        return self.radius > other.radius

    def __ge__(self, other):
        return (self.radius >= other.radius
                and self.color == other.color)

    def __eq__(self, other):
        return (self.radius == other.radius
                and self.color == self.color)

    def __ne__(self, other):
        return (self.radius != other.radius
                or self.radius != other.radius)

circleA = Circle(5, "Blue")
circleB = Circle(5, "Green")
circleC = Circle(7, "Red")
circleD = Circle(5, "Blue")

print(circleA < circleB)
print(circleA <= circleB)

print(circleA > circleD)
print(circleA >= circleD)

print(circleA == circleB)
print(circleA == circleD)
print(circleA != circleD)	

Leave a Comment