返回博客

28. 实例属性与类属性:面向对象的数据组织

28. 实例属性与类属性:面向对象的数据组织

第二十八篇:实例属性与类属性——面向对象的数据组织

前言

实例属性和类属性是面向对象编程中最基础也是最重要的概念之一。理解它们的区别、优先级和使用场景,能够帮助你设计出更清晰、更高效的类结构。本文将深入讲解实例属性的动态绑定、类属性的定义与共享机制,以及类方法与静态方法的使用。

实例属性 vs 类属性

基本定义

class User:
    # 类属性:所有实例共享
    company = "TechCorp"
    total_users = 0
    
    def __init__(self, name, email):
        # 实例属性:每个实例独立拥有
        self.name = name
        self.email = email
        User.total_users += 1

user1 = User("小明", "xiaoming@example.com")
user2 = User("小红", "xiaohong@example.com")

# 访问类属性
print(User.company)       # TechCorp
print(user1.company)      # TechCorp — 通过实例也能访问
print(User.total_users)   # 2

# 访问实例属性
print(user1.name)         # 小明
print(user2.name)         # 小红

核心区别对比

特性 实例属性 类属性
定义位置 __init__ 内(self.xxx) 类体内(非方法中)
存储位置 每个实例的 __dict__ 类的 __dict__
共享性 各实例独立 所有实例共享
内存占用 每创建一个实例多一份 只有一份
修改影响 只影响当前实例 影响所有实例

经典误区:实例修改类属性

class Counter:
    count = 0  # 类属性
    
    def increment(self):
        self.count += 1  # ⚠️ 错误!这会创建实例属性而非修改类属性

c1 = Counter()
c2 = Counter()
c1.increment()
print(c1.count)  # 1 — 实际创建了实例属性
print(c2.count)  # 0 — 类属性未变,仍为 0
print(Counter.count)  # 0

# ✅ 正确做法
class Counter:
    count = 0
    
    def increment(self):
        Counter.count += 1  # 显式使用类名引用

c1 = Counter()
c2 = Counter()
c1.increment()
print(Counter.count)  # 1 — 正确更新类属性

实例属性的动态绑定

运行时添加/删除属性

Python 允许在运行时动态添加或移除实例属性:

class Employee:
    def __init__(self, name, department):
        self.name = name
        self.department = department

emp = Employee("小明", "技术部")

# 动态添加新属性
emp.salary = 20000
emp.hire_date = "2024-01-15"

print(emp.salary)     # 20000
print(emp.hire_date)  # 2024-01-15

# 动态删除属性
del emp.hire_date
# print(emp.hire_date)  # AttributeError!

使用 dict 查看和遍历实例属性

class Product:
    def __init__(self, name, price, weight):
        self.name = name
        self.price = price
        self.weight = weight

product = Product("机械键盘", 399.9, 850)

# 查看所有实例属性(不包括方法等)
print(product.__dict__)
# {'name': '机械键盘', 'price': 399.9, 'weight': 850}

# 遍历并格式化输出
for key, value in product.__dict__.items():
    print(f"{key}: {value}")
# name: 机械键盘
# price: 399.9
# weight: 850

# 动态设置属性
for attr_name, attr_value in {"color": "黑色", "material": "铝合金"}.items():
    setattr(product, attr_name, attr_value)

print(product.color)  # 黑色

类属性的高级用法

类属性作为数据字典

class AppConfig:
    # 配置项全部定义为类属性,便于统一管理
    APP_NAME = "MyApp"
    VERSION = "1.0.0"
    
    DATABASE = {
        "host": "localhost",
        "port": 5432,
        "name": "myapp_db",
        "user": "admin",
    }
    
    FEATURES = {
        "authentication": True,
        "logging": True,
        "cache": False,
    }

# 读取配置
print(AppConfig.APP_NAME)           # MyApp
print(AppConfig.DATABASE["host"])   # localhost
print(AppConfig.FEATURES["cache"])  # False

# 修改配置(全局生效)
AppConfig.VERSION = "1.1.0"

类属性与方法

import hashlib
from datetime import datetime

class PasswordHasher:
    """密码哈希器 - 类方法和类属性"""
    
    algorithm = "sha256"
    iterations = 10000
    
    @classmethod
    def hash_password(cls, password: str, salt: str = None) -> dict:
        """使用类属性计算密码哈希"""
        if salt is None:
            salt = hashlib.sha256(password.encode()).hexdigest()[:8]
        
        hashed = hashlib.pbkdf2_hmac(
            cls.algorithm,
            password.encode(),
            salt.encode(),
            cls.iterations
        )
        
        return {
            "algorithm": cls.algorithm,
            "iterations": cls.iterations,
            "salt": salt,
            "hash": hashed.hex()
        }

result = PasswordHasher.hash_password("mypassword")
print(result["algorithm"])  # sha256

类方法与静态方法

三种方法类型对比

方法类型 第一个参数 适用场景 装饰器
实例方法 self 访问/修改实例属性 无
类方法 cls 访问/修改类属性,工厂方法 @classmethod
静态方法 无 独立于类和实例的工具函数 @staticmethod

实例方法

class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score
    
    def get_grade(self):
        """根据分数返回等级"""
        if self.score >= 90:
            return "A"
        elif self.score >= 80:
            return "B"
        elif self.score >= 60:
            return "C"
        else:
            return "D"

student = Student("小明", 85)
print(student.get_grade())  # B

类方法 @classmethod

from datetime import datetime

class DateHelper:
    current_year = datetime.now().year
    
    @classmethod
    def year_of_birth(cls, age: int) -> int:
        """计算出生年份"""
        return cls.current_year - age
    
    @classmethod
    def from_string(cls, date_str: str, fmt: str = "%Y-%m-%d"):
        """从字符串创建日期对象"""
        parsed = datetime.strptime(date_str, fmt)
        return {
            "year": parsed.year,
            "month": parsed.month,
            "day": parsed.day,
        }

print(DateHelper.year_of_birth(25))
# 2026 - 25 = 2001

print(DateHelper.from_string("2024-09-09"))
# {'year': 2024, 'month': 9, 'day': 9}

静态方法 @staticmethod

class StringUtils:
    """纯工具函数集合,不需要访问类或实例"""
    
    @staticmethod
    def is_palindrome(s: str) -> bool:
        """判断是否为回文"""
        cleaned = s.lower().replace(" ", "")
        return cleaned == cleaned[::-1]
    
    @staticmethod
    def truncate(text: str, max_length: int = 50, suffix="...") -> str:
        """截断文本"""
        if len(text) <= max_length:
            return text
        return text[:max_length - len(suffix)] + suffix
    
    @staticmethod
    def slugify(text: str) -> str:
        """将中文转换为 URL-friendly slug"""
        return text.lower().replace(" ", "-").replace("_", "-")

print(StringUtils.is_palindrome("level"))      # True
print(StringUtils.is_palindrome("hello"))      # False
print(StringUtils.truncate("Hello World", 5))  # Hello...
print(StringUtils.slugify("Python 教程"))       # python-教程

实际应用场景选择

class Configuration:
    DEFAULT_CONFIG = {"timeout": 30, "retries": 3}
    
    def __init__(self, timeout=None, retries=None):
        # 实例属性:使用默认值或传入值
        self.timeout = timeout or self.DEFAULT_CONFIG["timeout"]
        self.retries = retries or self.DEFAULT_CONFIG["retries"]
    
    def update(self, **kwargs):
        """更新实例配置"""
        for key, value in kwargs.items():
            setattr(self, key, value)
    
    @classmethod
    def from_env(cls, env_vars: dict):
        """从环境变量创建配置"""
        return cls(
            timeout=int(env_vars.get("TIMEOUT", 30)),
            retries=int(env_vars.get("RETRIES", 3)),
        )
    
    @staticmethod
    def validate_timeout(value):
        """验证超时值的合法性"""
        try:
            val = int(value)
            return 0 < val <= 300
        except (ValueError, TypeError):
            return False

# 使用示例
config = Configuration(timeout=60)
print(config.validate_timeout(45))  # True
print(config.validate_timeout(-1))  # False

env_config = Configuration.from_env({"TIMEOUT": "60"})
print(env_config.timeout)  # 60

实战示例

学生管理系统(综合应用)

class Student:
    # 类属性
    school = "阳光中学"
    total_students = 0
    valid_grades = list(range(0, 101))
    
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.scores = {}
        Student.total_students += 1
    
    @property
    def average_score(self):
        """平均分(只读属性)"""
        if not self.scores:
            return 0
        return sum(self.scores.values()) / len(self.scores)
    
    def add_score(self, subject, score):
        """添加成绩"""
        if not isinstance(score, (int, float)):
            raise ValueError("成绩必须是数字")
        if not 0 <= score <= 100:
            raise ValueError("成绩必须在 0~100 之间")
        self.scores[subject] = score
    
    @classmethod
    def create_from_dict(cls, data: dict):
        """从字典创建学生"""
        student = cls(data["name"], data["student_id"])
        for subject, score in data.get("scores", {}).items():
            student.add_score(subject, score)
        return student
    
    def __repr__(self):
        avg = self.average_score
        return f"{self.name}({self.student_id}): 平均={avg:.1f}"

# 测试
data1 = {
    "name": "张三",
    "student_id": "001",
    "scores": {"数学": 92, "语文": 88, "英语": 95},
}
s1 = Student.create_from_dict(data1)
print(s1)
# 张三(001): 平均=91.7

print(Student.school)  # 阳光中学
print(Student.total_students)  # 1

最佳实践

何时用实例属性?

# ✅ 适合实例属性的场景:
# 1. 对象状态数据(用户信息、商品详情、订单金额)
# 2. 需要不同实例有不同值的字段
# 3. 对象生命周期内会改变的属性

class Order:
    def __init__(self, item, quantity):
        self.item = item      # 实例属性
        self.quantity = quantity  # 实例属性
        
    def calculate_total(self):
        return self.item.price * self.quantity  # 访问实例属性

何时用类属性?

# ✅ 适合类属性的场景:
# 1. 常量(MAX_ATTEMPTS = 3)
# 2. 统计计数器(total_records += 1)
# 3. 统一配置(DATABASE_URL = "...")
# 4. 枚举值(STATUS_NEW, STATUS_PROCESSING)

class APIRequest:
    MAX_RETRIES = 3
    TIMEOUT = 30
    base_url = "https://api.example.com"
    
    def make_request(self, endpoint):
        url = f"{self.base_url}/{endpoint}"  # 类属性
        # ... 发送请求

封装原则

class BankAccount:
    """银行账号示例:展示封装的重要性"""
    
    # 类属性:账户前缀
    _bank_code = "ICBC"
    _next_account_id = 1
    
    def __init__(self, owner):
        self.owner = owner
        # 双下划线开头表示私有属性(名称修饰)
        self.__balance = 0  # 外部不能直接访问
        self._account_number = f"{self._bank_code}{BankAccount._next_account_id:08d}"
        BankAccount._next_account_id += 1
    
    @property
    def balance(self):
        """公开余额查询"""
        return self.__balance
    
    def deposit(self, amount):
        self.__balance += amount
    
    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("余额不足")
        self.__balance -= amount

account = BankAccount("小明")
account.deposit(1000)
print(account.balance)    # 1000
# print(account.__balance)  # AttributeError! 无法直接访问

总结

本文讲解了实例属性与类属性的核心知识:

  1. 实例属性 — 每个实例独立,在 __init__ 中定义,支持动态增删
  2. 类属性 — 所有实例共享,在类体中定义,常用于常量/配置/统计
  3. 动态绑定 — 运行时可添加/移除/修改实例属性
  4. 类方法 — @classmethod 接收 cls,用于工厂方法和类级操作
  5. 静态方法 — @staticmethod 无需 self/cls,实现工具函数
  6. 封装实践 — 双下划线前缀表示私有属性
  7. 最佳实践 — 实例属性放状态,类属性放配置/常量

下一篇文章将深入讲解获取对象信息:type、isinstance、dir。


本文是 Python 教程系列的第二十八篇。