๐Ÿ“ LanguagesPythonOOP

Classes

Blueprints for creating objects. They define attributes (variables) and methods (functions) that objects instantiated from the class will have. self refers to the object instance

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self._protected = True
        self.__private = True
 
    def greet(self):
        return f"Hi, I'm {self.name}"
 
person = Person("Alice", 25)
print(person.greet())

Methods

Different types of methods include: instance methods, class methods, and static methods

class Person:
    def __init__(self, name):
        self.name = name
 
    # instance method belongs to the instance
    def greet(self): # self refers to the instance
        return f"Hi, I'm {self.name}"
 
    @classmethod # belongs to the class not the instance
    def info(cls): # cls not self
        cls.species = "homo sapien"
        print(f"I am a {cls.species}")
 
    @staticmethod # helper method that belongs to the class, but doesnt use attributes
    def name_generator(letter): # doesnt take on cls or self
        if letter == "a":
            print("Arthur")
        else:
            print("Sam")
 
person = Person('Bob')
person.greet()  # Hi, I'm Bob
Person.info()   # I am a homo sapien
Person.name_generator('a') # Arthur

Abstract Classes

Blueprints for other classes, cannot be instantiated directly, must be inherited. Abstract methods must be overridden by whatever inherits the abstract class.

from abc import ABC, abstractmethod
 
class Animal(ABC): # cant be instantiated, must be inherited
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
    @abstractmethod # must be overridden by subclass
    def sleep(self):
        pass

Inheritance

Allows a class to inherit from another. super initializes the parent class.

class Animal: # base class
    def __init__(self, species):
        self.species = species
 
class Dog(Animal): # subclass
    def __init__(self, name):
        super().__init__("Dog")  # passes init args to base class
        self.name = name
 
dog = Dog("Buddy")
print(dog.species, dog.name) # Dog Buddy

Overriding/Polymorphism

When the subclass overrides a base/parent class method.

class Animal:
    def speak(self):  # parent class method
        return "Some sound"
 
class Dog(Animal):
    def speak(self):  # overrides the parent class method
        return "Bark"
 
animals = [Dog(), Animal()]
for animal in animals:
    print(animal.speak())  # Bark, Some sound

Multiple Inheritance

A class can inherit from multiple classes

class Mammal: # base class
    def __init__(self, color):
        self.color = color
 
class WingedAnimal: # mixin class (implements a single, well defined feature)
    def __init__(self):
        print('I have wings')
 
class Bat(Mammal, WingedAnimal):  # subclass with multiple inheritance
    def __init__(self):
        Mammal.__init__(self, 'brown')  # call each class init method
        WingedAnimal.__init__(self)
        print('I\'m a bat')
 
bat = Bat()  # I have wings, I'm a bat
help(Bat)  # shows the MRO (Method Resolution Order) Bat -> Mammal -> WingedAnimal -> object

Decorators

A function that wraps and extends another function without modifying it (syntactic sugar)

def my_decorator(func):
    def wrapper(*args, **kwargs):  # *args, **kwargs so it can take in any args
        print("1 Hi.")
        func()
        print("3 Hey.")
    return wrapper
 
@my_decorator  # decorates the say_hello function
def say_hello():
    print("2 Hello!")
 
say_hello()  # 1 Hi. 2 Hello! 3 Hey.

Closures

A nested function that accesses variables from its enclosing scope. The inner function remembers variables even after the outer function has finished execution

def outer_function(msg):
    def inner_function():  # a closure
        print(msg)  # references msg from the outer function's scope
    return inner_function
 
hi_func = outer_function('hi')
hi_func()  #  hi

Manipulating Built-ins

Magic Methods (Dunder Methods)

Built in methods that are not usually meant to be called by us but we can override some of them to change their behavior

class Animal:
    animal_count = 0
 
    def __init__(self, name): # creates class instance
        self._name = name
        Animal.animal_count += 1
 
    def __str__(self): # string representation (shown on print)
        return f"Hi I'm {self._name})"
 
    def __repr__(self): # representation (how to use the class)
        return f"Animal({self._name})"
 
 
animal = Animal("Buddy") # new instance animal
print(animal)  # __str__ Hi I'm Buddy
print(repr(animal)) # __repr__ Animal(Buddy)

Inheriting from Built-in Classes

Built-in classes like list, dict, and tuple can be extended/inherited from.

class IntFloatList(list):  # inherits list methods
    def append(self, inpuut):
        if isinstance(inpuut, (int, float)):
            super().append(inpuut) # calls method from super() directly
        else:
            print("Only integers or floats are allowed")
 
my_list = IntFloatList()
my_list.append(3.14)
my_list.append("text")  # Only integers or floats are allowed
print(my_list)  # [3.14]