Procedural vs Object-Oriented Programming Paradigms
Procedural programming focuses on solving problems through sequential steps, resembling an assembly line approach with mechanical thinking patterns.
Advantages: Complex problems become streamlined and simplified. Disadvantages: Poor extensibility.
Object-oriented programming treats objects as combinations of attributes and behaviors. This approach involves manipulating individual objects by providing them with methods and properties to achieve specific goals without concerning oneself with the underlying process.
Advantages: High extensibility. Disadvantages: Higher complexity compared to procedural programming.
Classes
Objects combine features and skills, while classes represent shared characteristics and abilities among multiple objects.
Key points:
- Objects are concrete entities, whereas classes are abstract concepts
- Different perspectives yield different class and object definitions
In programs: define classes first, then instantiate objects from those classes.
# Class definition example
class Learner:
# Shared attributes
institution = "Fourth Educational Facility"
# Shared behavior
def learn(self):
print("Diligent learning approach.")
# A class serves as a namespace containing variables and functions
# Class body executes immediately during definition, creating a namespace
# Access operations
print(Learner.__dict__) # View class namespace
print(Learner.institution) # Equivalent to Learner.__dict__['institution']
print(Learner.learn)
# Modification operations
Learner.institution = "Academic Institution" # Learner.__dict__['institution'] = 'Academic Institution'
Learner.nation = "Country" # Learner.__dict__["nation"] = "Country"
del Learner.nation # del Learner.__dict__["nation"]
# Using behavior (function)
Learner.learn(123)
Summary:
- Classes fundamentally serve as namespaces containing variable and function names
- Purpose: Extract names for usage or instantiate objects from the class
Objects
Calling a class creates an object; objects also function as namespaces.
The process of calling a class is called instantiation, and the returned value is an object/instance of the class.
class Learner:
# Shared attributes
institution = "Fourth Educational Facility"
def __init__(self, full_name, years, gender):
self.full_name = full_name
self.years = years
self.gender = gender
# Shared behavior
def learn(self):
print("Diligent learning approach.")
student1 = Learner('jones', 23, 'male')
# When calling the class:
# 1. Creates empty object student1, then returns it
# 2. Triggers execution of __init__ function, passing object and parameters
# __init__ purpose: Initialize unique attributes for each instance during instantiation
# Note: Must not return values, otherwise errors occur
Attribute lookup sequence: First search in the object, then in the class, then raise error if not found.
Attributes defined in classes are shared among all objects; both objects and classes can access them.
When objects modify class-defined attributes, they add new attributes to their own namespace.
When classes modify their defined attributes, all objects see the changes upon access.
# Counting instances
class Learner:
# Shared attributes
institution = "Fourth Educational Facility"
counter = 0
def __init__(self, full_name, years, gender):
self.full_name = full_name
self.years = years
self.gender = gender
Learner.counter += 1
# Shared behavior
def learn(self):
print("Diligent learning approach.")
student1 = Learner('alpha', 13, 'male')
student2 = Learner('beta', 13, 'female')
student3 = Learner('gamma', 16, 'male')
print(Learner.counter, student1.counter, student2.counter, student3.counter)
# Output: 3 3 3 3
Bound methods: Functions defined in classes become class function attributes and are bound for object usage.
class Learner:
# Shared attributes
institution = "Fourth Educational Facility"
def __init__(self, full_name, years, gender):
self.full_name = full_name
self.years = years
self.gender = gender
# Shared behavior
def learn(self, parameter):
print("Diligent learning.%s" % self.full_name)
student1 = Learner('alpha', 13, 'male')
# Usage by class
# Learner.learn(student1,123)
# Object invocation
# When invoked by object, passes itself as first parameter
# Additional parameters go directly in parentheses
student1.learn(123)
Inheritance and Specialization
Inheritance is a way to create new classes. New classes are called subclasses/derived classes, while inherited classes are parent/base/super classes.
Python inheritance characteristics:
- Subclasses can reuse parent class attributes
- Python allows single class to inherit multiple parent classes simultaneously
- Classes are categorized as new-style or classic in inheritance contexts
New-style classes: Any clas inheriting from object, including subclasses Python 3: All classes implicitly inherit object even without explicit declaration Classic classes: Classes not inheriting object or their subclasses Python 2: Only distinguishes between new-style and classic classes
Purpose of inheritance: Reduce code redundancy between classes
View parent classes: __bases__
# Method one: Explicitly reference specific class function
# This approach operates independently of inheritance
# Accesses class functions without automatic value passing
class Person:
institution = 'Fourth Educational Facility'
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
class Student():
def __init__(self, name, age, gender, grade=0):
# Use parent class attributes
Person.__init__(self, name, age, gender)
# Derive new attributes
self.grade = grade
def study(self):
print('%s is studying.' % self.name)
class Instructor():
def __init__(self, name, age, gender, rank):
Person.__init__(self, name, age, gender)
self.rank = rank
def assign_grade(self, student, number):
student.grade = number
student1 = Student('jones', 29, 'male')
print(student1.__dict__)
student1.study()
instructor1 = Instructor('bbb', 19, 'male', 11)
print(instructor1.__dict__)
Polymorphism
Polymorphism refers to different forms of the same type of entity.
Polymorphic behavior: Under polymorphic conditions, objects can be used directly without considering their specific types.
Core principle of polymorphism: Standardization.
# First approach:
# Python provides strict interface standardization (not recommended)
import abc
class Creature(metaclass=abc.ABCMeta):
@abc.abstractmethod
def vocalize(self):
pass
@abc.abstractmethod
def move(self):
pass
# Creature() # Parent class only establishes standards, cannot instantiate
class Human(Creature):
def vocalize(self):
print('speak greetings')
def move(self):
pass
class Canine(Creature):
def vocalize(self):
print('bark bark bark')
def move(self):
pass
class Swine(Creature):
def vocalize(self):
print('grunt grunt grunt')
def move(self):
pass
obj1 = Human()
obj2 = Canine()
obj3 = Swine()
# Instantiation doesn't cause errors,
# but calling methods without required vocalize, move methods causes errors
# This enforces standardized interfaces
Encapsulation
Encapsulation involves hiding internal implementation while exposing controlled interfaces.
How to encapsulate:
Add __ prefix to attributes or methods in class definition (no __ suffix)
Summary:
__prefixed attributes create syntactic transformation, not true external restriction (Encapsulated method:__funcbecomes_ClassName__func)- Transformation occurs once during class definition phase
- Parent classes can prevent child class overrides using
__prefix
class Container:
__x = 111 # _Container__x
__y = 222 # _Container__y
def __init__(self, identifier, duration):
self.__identifier = identifier
self.__duration = duration
def __process(self): # _Container__process
print('process')
def retrieve_info(self):
print(self.__identifier, self.__duration, self.__x) # print(self._Container__identifier, self._Container__duration, self._Container__x)
Why encapsulation: For data attributes: Hide data internally, preventing direct external manipulation. Create internal interfaces for controlled access with validation logic.
class Individual:
def __init__(self, name, age):
self.__name = name
self.__age = age
def display_info(self):
print('<name:%s age:%s>' % (self.__name, self.__age))
def update_info(self, name, age):
if type(name) is not str:
print('Name must be string type')
return
if type(age) is not int:
print('Age must be integer type')
return
self.__name = name
self.__age = age
person = Individual('jones', 19)
person.display_info()
# person.update_info(123,20)
person.update_info('jones', '19')
# Neither modification succeeds
Property Decorator
Transforms class methods into attribute-like access.
# Usage example
class Individual:
def __init__(self, name):
self.__name = name
@property
def name(self):
return '<name:%s>' % self.__name
@name.setter
def name(self, value):
if type(value) is not str:
print('Name must be string type')
return
self.__name = value
@name.deleter
def name(self):
del self.__name
obj = Individual('jones')
# View
# print(obj.name)
# Modify
# obj.name='JONES'
# obj.name=123
# print(obj.name)
# Delete
# del obj.name
# print(obj.__dict__)
Built-in Methods
isinstance and issubclass
isinstance checks if an object is an instance of a class, issubclass checks subclass relationships.
isinstance() vs type() differences:
type() doesn't consider subclass-parent class relationships
isinstance() considers inheritance relationships
Recommend isinstance() for type comparison
Example:
class Parent:
pass
class Child(Parent):
pass
isinstance(Parent(), Parent) # returns True
type(Parent()) == Parent # returns True
isinstance(Child(), Parent) # returns True
type(Child()) == Parent # returns False
Reflection
Map strings to object or class attributes (often used with input).
# hasattr checks if object/class has method/attribute matching string
hasattr(obj, 'name') # 'name' in obj.__dict__
getattr(obj, 'name') # obj.__dict__['name']
setattr(obj, 'age', 18) # obj.sex = 18
delattr(obj, 'name')
print(obj.__dict__)
Exception Handling
Exceptions are signals indicating errors. Unhandled exceptions terminate programs.
Exception information includes:
- Traceback: exception tracking information
- Exception type
- Exception content
Handling exceptions prevents program crashes, enhancing robustness.
Classification:
- Syntax errors: must be corrected before execution
- Logic errors: preventable through if statements or try-except blocks
try:
code.....
except NameError as e:
# e contains error information
# Execute when exception matches NameError
code.....
except NameError2:
# Multiple branches
code....
# Can be on single line
except (NameError, NameError2)
# Universal exception
Exception
# else: execute when no exceptions occur
try:
.....
else:
....
# finally: execute regardless of exceptions
Custom exception types:
class CustomException(BaseException):
def __str__(self): # Print exception value
return ""
# Raise exception
raise CustomException("Custom exception defined")
Assertion:
assert condition, raises exception if condition fails