我来设计一个直观的案例,通过内存地址和实际修改效果来展示Python类变量与实例变量的本质区别:
class Student:
# 类变量 - 存储在类对象的命名空间中
school = "清华大学" # 全体实例共享
grade = "大一" # 全体实例共享
# 用于跟踪所有实例的工具变量
all_students = []
def __init__(self, name, student_id):
# 实例变量 - 存储在实例对象的命名空间中
self.name = name
self.student_id = student_id
self.scores = [] # 每个实例独立
# 类变量可通过类名访问
Student.all_students.append(self)
def show_info(self):
print(f"学生: {self.name}")
print(f" 学校(类变量): {Student.school}")
print(f" 学校(通过self): {self.school}")
print(f" 班级: {self.grade}")
print(f" 学号: {self.student_id}")
print(f" 分数: {self.scores}")
print()
# ===== 第一部分:内存地址验证 =====
print("=" * 60)
print("第一部分:内存地址验证")
print("=" * 60)
# 创建两个学生实例
zhang = Student("张三", "2024001")
li = Student("李四", "2024002")
# 验证类变量和实例变量的存储位置
print("类变量 'school' 的地址:")
print(f" 通过类访问: {hex(id(Student.school))}")
print(f" 通过实例 zhang 访问: {hex(id(zhang.school))}")
print(f" 通过实例 li 访问: {hex(id(li.school))}")
print("\n实例变量 'name' 的地址:")
print(f" zhang.name: {hex(id(zhang.name))}")
print(f" li.name: {hex(id(li.name))}")
print("\n实例变量 'scores' 的地址:")
print(f" zhang.scores: {hex(id(zhang.scores))}")
print(f" li.scores: {hex(id(li.scores))}")
# ===== 第二部分:修改测试 =====
print("\n" + "=" * 60)
print("第二部分:修改测试")
print("=" * 60)
# 测试1:通过类修改类变量(影响所有实例)
print("\n测试1:通过类修改类变量")
Student.school = "北京大学"
zhang.show_info()
li.show_info()
# 测试2:通过实例修改同名变量(实际创建实例变量,不会影响类变量)
print("\n测试2:通过实例修改同名变量")
zhang.school = "复旦大学" # 这会在zhang实例中创建实例变量school
print(f"通过实例修改后:Student.school = {Student.school}")
print(f"通过实例修改后:zhang.school = {zhang.school}")
print(f"通过实例修改后:li.school = {li.school}")
# 验证zhang现在有实例变量school
print(f"\nzhang的__dict__: {zhang.__dict__}")
print(f"li的__dict__: {li.__dict__}")
print(f"Student类的__dict__: {Student.__dict__['school']}")
# 测试3:修改可变类变量(列表)
print("\n\n测试3:修改可变类变量(列表)")
Student.all_students.append("被添加的字符串")
print(f"通过Student类修改后:")
print(f" Student.all_students: {Student.all_students}")
print(f" zhang.all_students: {zhang.all_students}")
print(f" li.all_students: {li.all_students}")
print(f" id(Student.all_students): {hex(id(Student.all_students))}")
print(f" id(zhang.all_students): {hex(id(zhang.all_students))}")
# 测试4:修改不可变类变量
print("\n\n测试4:修改不可变类变量(字符串)")
print(f"初始: Student.grade = {Student.grade}")
print(f"初始: zhang.grade = {zhang.grade}")
print(f"初始: li.grade = {li.grade}")
Student.grade = "大二" # 字符串是不可变的,这是重新赋值
print(f"\n修改后: Student.grade = {Student.grade}")
print(f"修改后: zhang.grade = {zhang.grade}")
print(f"修改后: li.grade = {li.grade}")
# 测试5:验证实例变量屏蔽类变量
print("\n\n测试5:实例变量屏蔽类变量")
zhang.grade = "大三" # 创建实例变量
print(f"zhang.grade = {zhang.grade}")
print(f"li.grade = {li.grade}")
print(f"Student.grade = {Student.grade}")
# ===== 第三部分:常见陷阱演示 =====
print("\n" + "=" * 60)
print("第三部分:常见陷阱演示")
print("=" * 60)
class TrapClass:
"""演示常见的类变量陷阱"""
shared_list = [] # 类变量 - 可变类型
def __init__(self, name):
self.name = name # 实例变量
# 错误用法:直接修改类变量
# self.shared_list.append(self.name) # 危险!
def add_to_list_correct(self):
"""正确方法:明确说明要修改类变量"""
TrapClass.shared_list.append(self.name)
def add_to_list_wrong(self):
"""错误方法:看起来像在修改实例变量"""
self.shared_list.append(self.name) # 实际修改的是类变量!
def show_list(self):
print(f"{self.name} 看到的 shared_list: {TrapClass.shared_list}")
print("创建三个实例:")
a = TrapClass("A")
b = TrapClass("B")
c = TrapClass("C")
print("\n使用正确的方法添加:")
a.add_to_list_correct()
a.show_list()
b.add_to_list_correct()
b.show_list()
print("\n使用看起来像实例方法但实际上修改类变量的方式:")
c.add_to_list_wrong()
c.show_list()
# ===== 第四部分:最佳实践建议 =====
print("\n" + "=" * 60)
print("第四部分:最佳实践建议")
print("=" * 60)
print("""
1. 类变量适用于:
- 所有实例共享的常量(如学校名称)
- 配置参数(如默认值)
- 跟踪所有实例(如计数器)
2. 实例变量适用于:
- 每个对象特有的数据(如姓名、分数)
- 对象的状态信息
3. 修改规则:
- 修改类变量:使用 ClassName.class_variable
- 修改实例变量:使用 self.instance_variable
- 避免通过 self 修改类变量,特殊需求除外
4. 安全建议:
- 可变类变量(列表、字典)要特别小心
- 如果需要在实例中修改类变量,明确使用类名
- 经常检查 __dict__ 了解属性归属
""")
# 最终检查
print("最终状态检查:")
print(f"Student类变量 school: {Student.school}")
print(f"zhang实例变量 school: {zhang.school if 'school' in zhang.__dict__ else '未定义'}")
print(f"li实例变量 school: {li.school}") # 仍然使用类变量
这个案例通过实际的内存地址验证、各种修改测试和常见陷阱演示,清晰地展示了:
- 存储位置不同:类变量存储在类对象中,实例变量存储在实例对象中
- 访问顺序:实例优先查找自己的命名空间,没有才找类变量
- 修改影响:通过类修改影响所有实例,通过实例修改只影响该实例
- 可变性陷阱:修改可变类变量时容易产生意外影响
- 最佳实践:如何正确使用两种变量
运行这个代码,你可以看到属性查找机制、内存地址变化及属性屏蔽的具体效果。