Chapter 7: Class and Object-Oriented Programming Checks
Haiyue
24min
Chapter 7: Class and Object-Oriented Programming Checks
Learning Objectives
- Master Pylint’s object-oriented programming checking features
- Understand best practices and common issues in class design
- Learn to analyze and optimize class structure and inheritance relationships
- Master the correct use of abstract classes and interfaces
Key Concepts
Class-Related Check Types
🔄 正在渲染 Mermaid 图表...
Class Design Principles
| Principle | Description | Pylint Check |
|---|---|---|
| Single Responsibility | A class should have only one reason to change | too-many-public-methods |
| Open/Closed Principle | Open for extension, closed for modification | - |
| Liskov Substitution | Subclass must be able to replace parent class | abstract-method |
| Interface Segregation | Client should not depend on interfaces it doesn’t need | too-few-public-methods |
| Dependency Inversion | Depend on abstractions, not concrete implementations | - |
Code Examples
Class Design Issues and Optimization
# R0903: too-few-public-methods - Too few public methods
class BadDataContainer: # pylint: disable=too-few-public-methods
"""Bad data container design"""
def __init__(self, data):
self.data = data
def get_data(self):
"""Get data"""
return self.data
# Fix solution 1: Use dataclass or namedtuple
from dataclasses import dataclass
from typing import Any
@dataclass
class DataContainer:
"""Data container - using dataclass"""
data: Any
def process_data(self):
"""Process data"""
return f"Processed: {self.data}"
def validate_data(self):
"""Validate data"""
return self.data is not None
def serialize_data(self):
"""Serialize data"""
return str(self.data)
# Fix solution 2: Add more related methods
class ImprovedDataContainer:
"""Improved data container"""
def __init__(self, data):
self._data = data
self._metadata = {}
def get_data(self):
"""Get data"""
return self._data
def set_data(self, data):
"""Set data"""
self._data = data
def add_metadata(self, key, value):
"""Add metadata"""
self._metadata[key] = value
def get_metadata(self, key):
"""Get metadata"""
return self._metadata.get(key)
def clear(self):
"""Clear data"""
self._data = None
self._metadata.clear()
def is_empty(self):
"""Check if empty"""
return self._data is None
# Fix solution 3: Consider if a class is really needed
# If it's just simple data storage, a class may not be needed
def create_data_container(data):
"""Create data container - functional approach"""
return {
'data': data,
'metadata': {},
'created_at': __import__('datetime').datetime.now()
}
def process_container_data(container):
"""Process container data"""
return f"Processed: {container['data']}"
# R0902: too-many-instance-attributes - Too many instance attributes
class BadUserProfile: # pylint: disable=too-many-instance-attributes
"""Bad user profile design"""
def __init__(self, user_id, username, email, first_name, last_name,
age, phone, address, city, country, postal_code,
job_title, company, department, salary, hire_date):
# 16 attributes, exceeding default limit of 7
self.user_id = user_id
self.username = username
self.email = email
self.first_name = first_name
self.last_name = last_name
self.age = age
self.phone = phone
self.address = address
self.city = city
self.country = country
self.postal_code = postal_code
self.job_title = job_title
self.company = company
self.department = department
self.salary = salary
self.hire_date = hire_date
# Fix solution 1: Use composition pattern
@dataclass
class PersonalInfo:
"""Personal information"""
first_name: str
last_name: str
age: int
phone: str
@dataclass
class AddressInfo:
"""Address information"""
address: str
city: str
country: str
postal_code: str
@dataclass
class EmploymentInfo:
"""Employment information"""
job_title: str
company: str
department: str
salary: float
hire_date: str
class UserProfile:
"""User profile - using composition"""
def __init__(self, user_id: str, username: str, email: str):
self.user_id = user_id
self.username = username
self.email = email
self.personal_info: PersonalInfo = None
self.address_info: AddressInfo = None
self.employment_info: EmploymentInfo = None
def set_personal_info(self, personal_info: PersonalInfo):
"""Set personal information"""
self.personal_info = personal_info
def set_address_info(self, address_info: AddressInfo):
"""Set address information"""
self.address_info = address_info
def set_employment_info(self, employment_info: EmploymentInfo):
"""Set employment information"""
self.employment_info = employment_info
def get_full_name(self):
"""Get full name"""
if self.personal_info:
return f"{self.personal_info.first_name} {self.personal_info.last_name}"
return self.username
def get_display_address(self):
"""Get display address"""
if self.address_info:
return f"{self.address_info.city}, {self.address_info.country}"
return "Address not set"
# Fix solution 2: Use inheritance and mixins
class BaseUser:
"""Base user class"""
def __init__(self, user_id: str, username: str, email: str):
self.user_id = user_id
self.username = username
self.email = email
class PersonalInfoMixin:
"""Personal information mixin"""
def __init__(self):
self._personal_data = {}
def set_personal_info(self, first_name, last_name, age, phone):
"""Set personal information"""
self._personal_data.update({
'first_name': first_name,
'last_name': last_name,
'age': age,
'phone': phone
})
def get_full_name(self):
"""Get full name"""
return f"{self._personal_data.get('first_name', '')} {self._personal_data.get('last_name', '')}".strip()
class AddressMixin:
"""Address mixin"""
def __init__(self):
self._address_data = {}
def set_address(self, address, city, country, postal_code):
"""Set address"""
self._address_data.update({
'address': address,
'city': city,
'country': country,
'postal_code': postal_code
})
def get_display_address(self):
"""Get display address"""
city = self._address_data.get('city', '')
country = self._address_data.get('country', '')
return f"{city}, {country}" if city and country else "Address not set"
class ExtendedUserProfile(BaseUser, PersonalInfoMixin, AddressMixin):
"""Extended user profile"""
def __init__(self, user_id: str, username: str, email: str):
BaseUser.__init__(self, user_id, username, email)
PersonalInfoMixin.__init__(self)
AddressMixin.__init__(self)
Inheritance Relationship Optimization
# R0901: too-many-ancestors - Too many ancestor classes
class Level1:
"""Level 1"""
pass
class Level2(Level1):
"""Level 2"""
pass
class Level3(Level2):
"""Level 3"""
pass
class Level4(Level3):
"""Level 4"""
pass
class Level5(Level4):
"""Level 5"""
pass
class Level6(Level5):
"""Level 6"""
pass
class Level7(Level6):
"""Level 7"""
pass
class BadDeepInheritance(Level7): # pylint: disable=too-many-ancestors
"""Bad deep inheritance"""
pass
# Fix solution 1: Use composition instead of inheritance
from abc import ABC, abstractmethod
class Drawable(ABC):
"""Drawable interface"""
@abstractmethod
def draw(self):
"""Draw"""
pass
class Movable(ABC):
"""Movable interface"""
@abstractmethod
def move(self, x, y):
"""Move"""
pass
class Resizable(ABC):
"""Resizable interface"""
@abstractmethod
def resize(self, width, height):
"""Resize"""
pass
class Shape:
"""Base shape class"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
class Rectangle(Shape, Drawable, Movable, Resizable):
"""Rectangle - using multiple inheritance interfaces"""
def __init__(self, x=0, y=0, width=0, height=0):
super().__init__(x, y)
self.width = width
self.height = height
def draw(self):
"""Draw rectangle"""
return f"Drawing rectangle at ({self.x}, {self.y}) with size {self.width}x{self.height}"
def move(self, x, y):
"""Move rectangle"""
self.x = x
self.y = y
def resize(self, width, height):
"""Resize rectangle"""
self.width = width
self.height = height
# Fix solution 2: Use composition pattern
class RenderEngine:
"""Render engine"""
def render(self, shape):
"""Render shape"""
return f"Rendering {shape}"
class AnimationController:
"""Animation controller"""
def animate_move(self, obj, target_x, target_y):
"""Animate move"""
return f"Animating move to ({target_x}, {target_y})"
class TransformManager:
"""Transform manager"""
def apply_transform(self, obj, transform):
"""Apply transform"""
return f"Applying transform: {transform}"
class CompositeShape:
"""Composite shape class"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
self.render_engine = RenderEngine()
self.animation_controller = AnimationController()
self.transform_manager = TransformManager()
def draw(self):
"""Draw"""
return self.render_engine.render(self)
def move(self, x, y):
"""Move"""
return self.animation_controller.animate_move(self, x, y)
def resize(self, width, height):
"""Resize"""
transform = f"scale({width}, {height})"
return self.transform_manager.apply_transform(self, transform)
Method and Attribute Checks
# R0201: no-self-use - Method doesn't use self
class BadMathUtils:
"""Bad math utils class"""
def __init__(self, precision=2):
self.precision = precision
def add_numbers(self, a, b): # pylint: disable=no-self-use
"""Addition - doesn't use self"""
return a + b
def multiply_numbers(self, a, b): # pylint: disable=no-self-use
"""Multiplication - doesn't use self"""
return a * b
def format_number(self, number):
"""Format number - correctly uses self"""
return round(number, self.precision)
# Fix solution 1: Declare static methods as static
class MathUtils:
"""Math utils class"""
def __init__(self, precision=2):
self.precision = precision
@staticmethod
def add_numbers(a, b):
"""Addition - static method"""
return a + b
@staticmethod
def multiply_numbers(a, b):
"""Multiplication - static method"""
return a * b
def format_number(self, number):
"""Format number - instance method"""
return round(number, self.precision)
@classmethod
def create_with_precision(cls, precision):
"""Create instance with specified precision - class method"""
return cls(precision)
# Fix solution 2: Refactor to module-level functions
def add_numbers(a, b):
"""Addition function"""
return a + b
def multiply_numbers(a, b):
"""Multiplication function"""
return a * b
class NumberFormatter:
"""Number formatter"""
def __init__(self, precision=2):
self.precision = precision
def format_number(self, number):
"""Format number"""
return round(number, self.precision)
# W0212: protected-access - Accessing protected member
class BadAccessExample:
"""Bad access example"""
def __init__(self):
self._protected_value = 10
self.__private_value = 20
def get_value(self):
"""Get value"""
return self._protected_value
class BadClient:
"""Bad client"""
def __init__(self):
self.example = BadAccessExample()
def bad_access(self):
"""Bad access method"""
return self.example._protected_value # pylint: disable=protected-access
# Fix solution: Provide public interface
class GoodAccessExample:
"""Good access example"""
def __init__(self):
self._protected_value = 10
self.__private_value = 20
def get_value(self):
"""Get value - public interface"""
return self._protected_value
def set_value(self, value):
"""Set value - public interface"""
if self._validate_value(value):
self._protected_value = value
def _validate_value(self, value):
"""Validate value - protected method"""
return isinstance(value, (int, float)) and value >= 0
@property
def value(self):
"""Value property"""
return self._protected_value
@value.setter
def value(self, value):
"""Set value property"""
self.set_value(value)
class GoodClient:
"""Good client"""
def __init__(self):
self.example = GoodAccessExample()
def good_access(self):
"""Good access method"""
return self.example.get_value() # Use public interface
def good_property_access(self):
"""Good property access"""
return self.example.value # Use property
Abstract Class and Interface Design
# W0223: abstract-method - Abstract method not implemented
from abc import ABC, abstractmethod
from typing import Protocol
# Bad example: Abstract method not implemented
class BadAnimal(ABC):
"""Bad animal class"""
@abstractmethod
def make_sound(self):
"""Make sound"""
pass
@abstractmethod
def move(self):
"""Move"""
pass
class BadDog(BadAnimal): # pylint: disable=abstract-method
"""Bad dog class - not all abstract methods implemented"""
def make_sound(self):
"""Make sound"""
return "Woof!"
# Missing move method implementation
# Good example: Complete implementation of abstract methods
class Animal(ABC):
"""Animal abstract class"""
def __init__(self, name: str):
self.name = name
@abstractmethod
def make_sound(self):
"""Make sound"""
pass
@abstractmethod
def move(self):
"""Move"""
pass
def introduce(self):
"""Self introduction - concrete method"""
return f"I am {self.name}"
class Dog(Animal):
"""Dog class - complete implementation"""
def make_sound(self):
"""Make sound"""
return "Woof!"
def move(self):
"""Move"""
return "Running on four legs"
class Bird(Animal):
"""Bird class - complete implementation"""
def make_sound(self):
"""Make sound"""
return "Tweet!"
def move(self):
"""Move"""
return "Flying with wings"
# Use Protocol for type checking
class Drawable(Protocol):
"""Drawable protocol"""
def draw(self) -> str:
"""Draw"""
...
def get_bounds(self) -> tuple:
"""Get bounds"""
...
class Circle:
"""Circle - implements Drawable protocol"""
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> str:
"""Draw circle"""
return f"Drawing circle with radius {self.radius}"
def get_bounds(self) -> tuple:
"""Get bounds"""
return (0, 0, self.radius * 2, self.radius * 2)
class Square:
"""Square - implements Drawable protocol"""
def __init__(self, side: float):
self.side = side
def draw(self) -> str:
"""Draw square"""
return f"Drawing square with side {self.side}"
def get_bounds(self) -> tuple:
"""Get bounds"""
return (0, 0, self.side, self.side)
def render_shape(shape: Drawable) -> str:
"""Render shape"""
bounds = shape.get_bounds()
drawing = shape.draw()
return f"{drawing} at bounds {bounds}"
Properties and Descriptors
# W0201: attribute-defined-outside-init - Attribute defined outside __init__
class BadAttributeExample:
"""Bad attribute example"""
def __init__(self, name):
self.name = name
# Missing age attribute initialization
def set_age(self, age):
"""Set age"""
self.age = age # pylint: disable=attribute-defined-outside-init
def get_info(self):
"""Get info"""
# May raise AttributeError here
return f"{self.name} is {self.age} years old"
# Fix solution 1: Initialize all attributes in __init__
class GoodAttributeExample:
"""Good attribute example"""
def __init__(self, name, age=None):
self.name = name
self.age = age
def set_age(self, age):
"""Set age"""
self.age = age
def get_info(self):
"""Get info"""
if self.age is not None:
return f"{self.name} is {self.age} years old"
return f"{self.name} (age unknown)"
# Fix solution 2: Use property decorator
class PropertyExample:
"""Property decorator example"""
def __init__(self, name):
self.name = name
self._age = None
@property
def age(self):
"""Age property"""
return self._age
@age.setter
def age(self, value):
"""Set age"""
if value is not None and value < 0:
raise ValueError("Age cannot be negative")
self._age = value
@property
def info(self):
"""Info property"""
if self.age is not None:
return f"{self.name} is {self.age} years old"
return f"{self.name} (age unknown)"
# Use descriptors for advanced property management
class ValidatedAttribute:
"""Validated attribute descriptor"""
def __init__(self, name, validator=None):
self.name = name
self.validator = validator
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, f"_{self.name}", None)
def __set__(self, obj, value):
if self.validator and not self.validator(value):
raise ValueError(f"Invalid value for {self.name}: {value}")
setattr(obj, f"_{self.name}", value)
def positive_number(value):
"""Positive number validator"""
return isinstance(value, (int, float)) and value > 0
def non_empty_string(value):
"""Non-empty string validator"""
return isinstance(value, str) and len(value.strip()) > 0
class Person:
"""Person class - using descriptors"""
name = ValidatedAttribute("name", non_empty_string)
age = ValidatedAttribute("age", positive_number)
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person(name='{self.name}', age={self.age})"
Advanced Class Features
# Metaclasses and class decorators
class SingletonMeta(type):
"""Singleton metaclass"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
"""Database connection - singleton pattern"""
def __init__(self):
self.connection_string = "database://localhost:5432"
self.is_connected = False
def connect(self):
"""Connect to database"""
if not self.is_connected:
print(f"Connecting to {self.connection_string}")
self.is_connected = True
def disconnect(self):
"""Disconnect"""
if self.is_connected:
print("Disconnecting from database")
self.is_connected = False
# Class decorator
def add_repr(cls):
"""Decorator to add __repr__ method"""
def __repr__(self):
class_name = self.__class__.__name__
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{class_name}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class Product:
"""Product class"""
def __init__(self, name, price):
self.name = name
self.price = price
# Context manager
class FileManager:
"""File manager"""
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Enter context"""
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context"""
if self.file:
self.file.close()
def __str__(self):
return f"FileManager({self.filename}, {self.mode})"
# Usage example
def demonstrate_advanced_features():
"""Demonstrate advanced features"""
# Singleton pattern
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(f"Same instance: {db1 is db2}") # True
# Class decorator
product = Product("Laptop", 999.99)
print(product) # Product(name='Laptop', price=999.99)
# Context manager
with FileManager('test.txt', 'w') as f:
f.write("Hello, World!")
Object-Oriented Design Principles
- Single Responsibility Principle: Each class should have only one reason to change
- Open/Closed Principle: Open for extension, closed for modification
- Composition over Inheritance: Prefer composition over inheritance
- Interface Segregation Principle: Use small, focused interfaces
- Dependency Inversion Principle: Depend on abstractions, not concrete implementations
Precautions
- Avoid Deep Inheritance: Inheritance hierarchy should not be too deep
- Control Class Complexity: Limit the number of attributes and methods in a class
- Use Access Control Correctly: Use public, protected, and private members appropriately
- Implement All Abstract Methods: Ensure complete implementation of abstract classes
Good object-oriented design is the foundation for building maintainable and extensible software systems. Pylint’s checks help identify and fix design issues.