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

🔄 正在渲染 Mermaid 图表...

Class Design Principles

PrincipleDescriptionPylint Check
Single ResponsibilityA class should have only one reason to changetoo-many-public-methods
Open/Closed PrincipleOpen for extension, closed for modification-
Liskov SubstitutionSubclass must be able to replace parent classabstract-method
Interface SegregationClient should not depend on interfaces it doesn’t needtoo-few-public-methods
Dependency InversionDepend 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
  1. Single Responsibility Principle: Each class should have only one reason to change
  2. Open/Closed Principle: Open for extension, closed for modification
  3. Composition over Inheritance: Prefer composition over inheritance
  4. Interface Segregation Principle: Use small, focused interfaces
  5. Dependency Inversion Principle: Depend on abstractions, not concrete implementations
Precautions
  1. Avoid Deep Inheritance: Inheritance hierarchy should not be too deep
  2. Control Class Complexity: Limit the number of attributes and methods in a class
  3. Use Access Control Correctly: Use public, protected, and private members appropriately
  4. 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.