14  OOP in Python

14.1 Class

14.1.1 Define Class

class Car:
    "Car Class"
    # Class Attribute
    fuel = "Electric"
    # initialize dunder
    def __init__(self, color: str, mileage: int):
        self.color = color
        self.mileage = mileage
    # Print dunder
    def __str__(self) -> str:
        return f"color: {self.color}, mileage: {self.mileage}"
    # Instance method
    def drive(self):
        return f"Ventured {self.mileage} miles"            

14.1.2 Create instance

Create instance of a class Car

car_blue = Car("blue", 20000)
print(car_blue)
#> color: blue, mileage: 20000
# Doc string
car_blue.__doc__
#> 'Car Class'
car_blue.__dict__
#> {'color': 'blue', 'mileage': 20000}

Access Attribute

car_turbo.fuel
#> NameError: name 'car_turbo' is not defined

Access method

car_turbo.drive()
#> NameError: name 'car_turbo' is not defined

14.1.3 Class attribute vs Instance attribute

  1. Class Attributes:
    • Defined directly in the class body.
    • Shared across all instances of the class.
    • Accessed using the class name or through an instance.
    • Changes to a class attribute affect all instances that haven’t overridden the attribute.

Example:

class Manual:
    A = "hi"
    B = "there"

# Accessing class attributes
print(Manual.A)  # Output: hi
#> hi
print(Manual.B)  # Output: there
#> there

# Creating instances
m1 = Manual()
m2 = Manual()

# Accessing class attributes through instances
print(m1.A)  
#> hi
print(m2.A) 
#> hi

# Modifying class attribute
Manual.A = "hello"
print(m1.A)  
#> hello
print(m2.A)   
#> hello
  1. Instance Attributes:
    • Defined within the __init__ method.
    • Unique to each instance of the class.
    • Accessed using the instance name.
    • Changes to an instance attribute affect only that instance.

Example:

class Manual:
    def __init__(self):
        self.A = "hi"
        self.B = "there"

# Creating instances
m1 = Manual()
m2 = Manual()

# Accessing instance attributes
print(m1.A)  
#> hi
print(m2.A)  
#> hi

# Modifying instance attribute
m1.A = "hello"
print(m1.A)  
#> hello
print(m2.A)  
#> hi

14.1.3.1 Key Differences

  1. Scope and Sharing:
    • Class Attributes: Shared by all instances of the class. If you change a class attribute, the change is reflected in all instances unless overridden.
    • Instance Attributes: Unique to each instance. Changing an instance attribute affects only that particular instance.
  2. Definition and Initialization:
    • Class Attributes: Defined directly in the class body, outside any methods.
    • Instance Attributes: Defined within the __init__ method, which is called when a new instance of the class is created.
  3. Usage Context:
    • Class Attributes: Useful for constants or attributes that should be shared across all instances.
    • Instance Attributes: Used for attributes that need to be unique to each instance, such as data specific to that instance.

14.1.3.2 Summary

  • Use class attributes when you want to share data across all instances of the class.
  • Use instance attributes when you need each instance of the class to have its own unique data.

14.1.4 Interitance

class Parent:
    hair_color = "brown"

class Child(Parent):
    pass
ch1 = Child()
ch1.hair_color
#> 'brown'

Overwrite Parent

class Parent:
    hair_color = "brown"

class Child(Parent):
    hair_color = "purple" # Overwrite
ch2 = Child()
ch2.hair_color
#> 'purple'

Extend Parent Attribute

class Parent:
    speaks = ["English"]

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.speaks.append("German")
ch3 = Child()
ch3.hair_color
#> AttributeError: 'Child' object has no attribute 'hair_color'
# Check parent class
type(ch3) 
#> <class '__main__.Child'>
isinstance(ch3, Parent)
#> True

14.1.5 Multiple Child from Parent Class

Parent Class

class Dog:
    species = "Canis familiaris"

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

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound="..."):
        return f"{self.name} says {sound}"

Child Class

Each dog breed bark differently

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return super().speak(sound)

class Bulldog(Dog):
    def speak(self, sound="Woof"):
        return super().speak(sound)

class Dachshund(Dog):
    pass
miles = JackRussellTerrier("Miles", 4)
buddy = Dachshund("Buddy", 9)
jack = Bulldog("Jack", 3)
jim = Bulldog("Jim", 5)
miles.speak()
#> 'Miles says Arf'
buddy.speak()
#> 'Buddy says ...'

14.2 Class Method (Dunder)

14.2.1 String Representation __repr__, __str__

class Pair:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return 'Pair({0.x!r}, {0.y!r})'.format(self) 
    def __str__(self):
        return '({0.x!s}, {0.y!s})'.format(self)
p = Pair(3, 4)
p
#> Pair(3, 4)

the special !r formatting code indicates that the output of __repr__() should be used

print('p is {0!r}'.format(p)) 
#> p is Pair(3, 4)
print('p is {0}'.format(p))
#> p is (3, 4)

14.2.2 String Formatting __format__()

_formats = {
        'ymd' : '{d.year}-{d.month}-{d.day}',
        'mdy' : '{d.month}/{d.day}/{d.year}',
        'dmy' : '{d.day}/{d.month}/{d.year}'
        }

class Date:
    def __init__(self, year, month, day):
                self.year = year
                self.month = month
                self.day = day
    def __format__(self, code): 
        if code == '':
            code = 'ymd'    
        fmt = _formats[code] 
        return fmt.format(d=self)
d = Date(2012, 12, 21)
format(d)
#> '2012-12-21'
format(d, 'mdy')
#> '12/21/2012'
'The date is {:ymd}'.format(d)
#> 'The date is 2012-12-21'

14.2.3 Setter / Getter

class Person:
    def __init__(self, first_name):
        self.first_name = first_name
    # Getter function
    @property
    def first_name(self): 
        return self._first_name
    # Setter function
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string') 
        self._first_name = value
    # Deleter function (optional)
    @first_name.deleter
    def first_name(self):
        raise AttributeError("Can't delete attribute")
a = Person('Guido')
a.first_name
#> 'Guido'
a.first_name = 42
#> TypeError: Expected a string