A09 Classes
Class As Blueprint
# Class as blueprint to create objects
# ------------------------------------------------------------------------------
# A class can act as a template from which many objects are built. This file
# defines a Person blueprint containing attributes and methods that every
# instance MUST have when created. Each instance of the Person class
# will have its own unique values for these attributes.
class Person(object):
"""The Person class serves as a blueprint for creating person objects."""
# Person has what?
def __init__(self, name, age):
self.name = name
self.age = age
# Person can do what?
def say_hello(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
# Create concrete instances of the Person class
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)
person1.say_hello()
# Output: Hello, my name is Alice and I am 30 years old.
person2.say_hello()
# Output: Hello, my name is Bob and I am 25 years old.
Class Outline
# Class structure outline
# ------------------------------------------------------------------------------
# This file outlines common elements found in many classes such as attributes,
# static methods and constructors. Organizing these pieces consistently makes
# new classes easier to understand and maintain.
#
# The internal state of a class is defined by its attributes, which can be
# class variables (shared across all instances) or instance variables (unique
# to each instance). Methods can be categorized as instance methods (which
# operate on instance variables), class methods (which operate on class
# variables), or static methods (which do not operate on either).
class ClassStructure(object):
'""Class definition ....""'
# Class attributes
class_var = "Foo"
def __new__(cls, *args, **kwargs):
"""This method is called to create a new instance of the class."""
print("This is the constructor method")
return object.__new__(cls)
def __init__(self):
"""This method is called to initialize the instance after it is created."""
print("This is the initialization method")
self.inst_var = "John"
def instance_method(self):
"""This method has access to both the class and instance variables."""
print(self.class_var, self.inst_var)
@classmethod
def class_method(cls):
"""This method can access class variables but not instance variables."""
print(cls.class_var)
@staticmethod
def static_method():
"""This method does not have access to the class or instance variables."""
print(ClassStructure.class_var)
Instance Methods
# A class instance is as concrete realization of a class
# ------------------------------------------------------------------------------
# The code creates an instance of the Person class as a tangible object.
# The constructor assigns initial values to the instance. After creation, the
# object can call its methods and access stored data.
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print("Hello, I am {} and my age is {}".format(self.name, self.age))
person = Person("John", 32)
# Access to the instance attributes
print(person.name)
print(person.age)
# Output:
# John
# 32
# Call the instance method
person.say_hello()
# Output:
# Hello, I am John and my age is 32
Class Methods
# Class method used to modify the class itself
# ------------------------------------------------------------------------------
# In Python, class methods can be used to modify the behavior of all instances
# of a class by changing class-level attributes. Class methods are defined
# using the `@classmethod` decorator and take the class itself as the first
# parameter, conventionally named `cls`.
class Male(object):
NAME_PREFIX = "Mr."
def __init__(self, name):
self.name = name
@classmethod
def set_prefix(cls, prefix):
cls.NAME_PREFIX = prefix
@classmethod
def get_prefix(cls):
return cls.NAME_PREFIX
def show_name(self):
print(f"{Male.get_prefix()} {self.name}")
john = Male("John")
ivan = Male("Ivan")
# Show the initial name
john.show_name()
ivan.show_name()
# Modify the class-level attribute will affect all instances
Male.set_prefix("Mister")
# Show the modified name
john.show_name()
ivan.show_name()
Static Methods
# Static method are not bound to the class and can be used as utility functions
# ------------------------------------------------------------------------------
# Static methods operate without reference to a particular instance or class.
# They behave like regular functions that happen to live in the class's
# namespace and are often used for related utility tasks.
class Packet(object):
def __init__(self, ip_addr='192.168.10.1', mask="255.255.255.0", payload=()):
self.payload = payload
self.ip_addr = ip_addr
self.mask = mask
@staticmethod
def dot_to_bytes(val):
return bytes(map(int, val.split('.')))
@staticmethod
def bytes_to_dot(val):
return '.'.join(map(str, val))
# Convert IP address in dot notation to bytes
addr_bytes = Packet.dot_to_bytes('192.168.1.1')
print(addr_bytes)
# Convert bytes to IP address in dot notation
addr_dot = Packet.bytes_to_dot(addr_bytes)
print(addr_dot)
Nested Classes
# Nested classes for constants, settings, etc.
# ------------------------------------------------------------------------------
# Classes can contain other classes that serve as containers for related
# constants or configuration. Nesting keeps these auxiliary definitions close to
# the code that uses them.
class Settings:
LANG = "English"
THEME = "Light"
IP_ADDR = "192.168.210.10"
PORT = 8080
class AdvancedSettings:
ENABLE_LOGGING = False
MAX_CONNECTIONS = 10
class ExperimentalSettings:
ENABLE_NEW_FEATURE = False
# Access the basic settings
print(Settings.THEME)
# Access the b settings
print(Settings.AdvancedSettings.ENABLE_LOGGING) # Output: False
# Access the experimental settings
print(Settings.ExperimentalSettings.ENABLE_NEW_FEATURE) # Output: False
Object Construction
# How __new__ and __init__ cooperate in object creation
# ------------------------------------------------------------------------------
# Object creation begins with __new__, which allocates the instance. The fresh
# object is then passed to __init__ for further initialization. Separating these
# steps gives developers flexibility to customize how objects come into
# existence.
class Calculator(object):
def __new__(cls):
# Use the parent class to create the object
obj = object.__new__(cls)
# Return the object
print("__new__ : Created object {}".format(obj))
return obj
def __init__(self):
# Self is the object that was created by __new__
print("__init__: Using object {}".format(self))
# Add attributes to the object
self.name = "Cool Calculator"
calc = Calculator()
print(calc.name)
Property Decorator
# Property used to encapsulate an instance variable
# ------------------------------------------------------------------------------
# The `@property` decorator exposes getter and setter functions as attribute
# access. This allows validation or computation while keeping the public
# interface simple as if it were a regular attribute accessed using dot
# notation.
class Person(object):
def __init__(self, name, age):
self._name = name
self._age = age
# -------- Getter and Setter for name --------
def get_name(self):
return self._name
def set_name(self, value):
if not isinstance(value, str):
raise TypeError("Expected a string")
self._name = value
# -------- Getter/Setter acting as --------
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, str):
raise TypeError("Expected a string")
self._age = value
# Explicit getter/setter methods
person = Person("John", 30)
person.set_name("Jane")
print(person.get_name())
# Output: Jane
# Getter/setter as property (acting as an attribute)
person.age = 35
print(person.age)
# Output: 35
Named Constructors
# Named constructors provide alternative ways to create instances of a class
# ------------------------------------------------------------------------------
# A class can offer several named constructors for convenience. Each one accepts
# parameters tailored for a specific situation and returns a configured
# instance.
class Rectangle(object):
def __init__(self, width, height):
self.width = width
self.height = height
@classmethod
def from_diagonal(cls, x1, y1, x2, y2):
"""Usecase: Alternative constructor parameters."""
width = abs(x2 - x1)
height = abs(y2 - y1)
return cls(width, height)
@classmethod
def square(cls, size):
"""Usecase: Reduced constructor parameters."""
return cls(size, size)
@classmethod
def from_file(cls, file):
"""Usecase: Create an instance from a file."""
with open(file, 'r') as f:
data = f.read()
x1, y1, x2, y2 = data.split(',')
return cls.from_diagonal(float(x1), float(y1), float(x2), float(y2))
@classmethod
def from_json(cls, data):
"""Usecase: Create an instance from JSON data object."""
return cls(**data)
Linear Inheritance
# Multilevel inheritance
# ------------------------------------------------------------------------------
# Multilevel inheritance arranges classes in a linear hierarchy. Each
# subsequent class extends the one above it, accumulating behavior down the
# chain.
class A(object):
@staticmethod
def process():
print("Root is processing... ")
class B(A):
pass
class C(B):
pass
# The method process is searched for until the first class having the method is found (here A)
test = C()
test.process()
Multiple Inheritance
# Multiple inheritance
# ------------------------------------------------------------------------------
# A single class may inherit behavior from several parents. Combining features
# this way can reduce duplication but requires careful design to avoid
# conflicts.
class A(object):
@staticmethod
def process():
print("Class A is processing... ")
class B(object):
@staticmethod
def process():
print("Class B is processing... ")
class C(A, B):
pass
class D(B, A):
pass
# The method process is searched for until the first class having the method is found (here A)
test = C()
test.process()
# The method process is searched for until the first class having the method is found (here B)
test = D()
test.process()
Superclass Keyword
# Call __new__ method of super class
# ------------------------------------------------------------------------------
# A subclass can override __new__ while still delegating part of the creation
# process to its parent. Calling the superclass method ensures base attributes
# are initialized correctly.
class Person(object):
def __new__(cls, name):
return super(Person, cls).__new__(cls)
def __init__(self, name):
self.name = name
class Employee(Person):
def __new__(cls, name, id_number):
return super(Employee, cls).__new__(cls, name)
def __init__(self, name, id_number):
super(Employee, self).__init__(name)
self.id_number = id_number
def get_id_number(self):
return self.id_number
e = Employee("John", 1234)
print(e.name)
print(e.id_number)
e = Employee("John", 1234)
print(e.name)
print(e.id_number)
e = Employee("John", 1234)
print(e.get_id_number())
Method Resolution Order
# MRO (Method Resolution Order) - bottom first
# ------------------------------------------------------------------------------
# The method resolution order (MRO) in Python is the order in which Python
# looks for methods in a class hierarchy. It is particularly important in
# multiple inheritance scenarios, where a class inherits from multiple parent
# classes.
#
# The MRO is determined by the C3 linearization algorithm, which ensures that
# the order of method resolution is consistent and respects the order of
# inheritance. The MRO can be viewed using the `mro()` method of a class.
# Example of MRO in Python (bottom up, left to right)
class A(object):
@staticmethod
def process():
print("A.process()")
class B(object):
@staticmethod
def process():
print("B.process()")
class C(A, B):
pass
class D(C, B):
pass
test = D()
print(D.mro())
# Output:
# [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
Diamond Problem
# Mro (method resolution order) - diamond problem
# ------------------------------------------------------------------------------
# A diamond inheritance pattern occurs when two classes share a common base
# class. The method resolution order ensures that the base is initialized only
# once. The classes here are organized so that each path to the base is
# considered without duplication.
class A(object):
@staticmethod
def process():
print("A.process()")
class B(A):
@staticmethod
def process():
print("B.process()")
class C(A):
@staticmethod
def process():
print("C.process()")
class D(B, C):
pass
d = D()
print(D.mro())
d.process()
Unresolved MRO
# Mro (method resolution order) - unresolved
# ------------------------------------------------------------------------------
# Some inheritance graphs produce conflicting search orders that Python cannot
# resolve. This file sets up such a conflict and triggers an error when the
# interpreter tries to build the method resolution order. Understanding this
# failure helps diagnose complex inheritance issues.
class Player(object):
pass
class Enemy(Player):
pass
class GameObject(Player, Enemy):
pass
# Explanation (see MRO rules in the documentation):
#
# - MRO is GameObject -> Player -> Enemy -> Player (not monotonic as Player appears twice)
# - Rule 2 says Enemy should appear before Player
# - Rule 3 says Player should appear before Enemy
#
# Rules 2 and 3 are in conflict, so the MRO algorithm cannot be applied. This is called an
# "unresolvable inheritance graph" and Python will raise an exception in this case.
g = GameObject()
print(GameObject.mro())