☰
Python中访问类变量与实例变量的完整步骤
2026/10/9 21:43:00 网站建设 项目流程

前言


「怎么访问类变量」「怎么访问实例变量」听起来像入门问题,但真正让人翻车的不是「读」,而是读写的方向不一致:读的时候 Python 会沿着实例 → 类 → 基类一路向上找,写的时候却只会往实例自己的字典里塞。你写的两个看起来对称的表达式,可能一个改了全局、一个只改了眼前。


更隐蔽的是可变类变量:把一个list或dict直接写成类变量,然后某个实例对它append,结果所有实例都跟着变——因为那个list只有一份,实例们共享的是同一个对象引用。


本文只讲读写行为这一面:读类变量与实例变量各自的写法、写操作到底落在谁身上、可变类变量为什么会被所有实例共享、以及__slots__与类变量如何相互影响。代码按 Python 3.8+ 给出。


一、读:三种写法,去三个地方找


读取时,Python 的规则是「就近命中」:先看实例字典,没有就回退到类字典,再没有就沿基类往上找。


# 适用于 Python 3.8+
class Employee:
company = "示例科技" # 类变量
workdays = 5

def __init__(self, name):
self.name = name # 实例变量


e1 = Employee("小明")
e2 = Employee("小红")

print(e1.company) # 推演输出:示例科技 —— 实例没有,回退到类
print(e2.company) # 推演输出:示例科技
print(Employee.company) # 推演输出:示例科技 —— 直接查类
print(e1.name, e2.name) # 推演输出:小明 小红 —— 各是各的

三种写法的差异可以用一张表看清楚:




写法查找范围适用场景



self.company(方法内)实例字典 → 类 → 基类希望「实例可覆盖、否则用默认」

Employee.company该类 → 基类明确要类上的那个值,不接受实例覆盖

e1.company(外部)实例字典 → 类 → 基类普通读取



在方法内部尤其要注意:如果你在父类的方法里写死Employee.company,子类即使覆盖了company,这个方法也还是读到父类的值。想让方法跟随实际类型,就用type(self).company。


二、写:什么写法改类、什么写法建实例


写操作只有一条规则:赋值的对象决定写到哪里。


# 适用于 Python 3.8+
e1 = Employee("小明")
e2 = Employee("小红")

# ① 通过类名写:改的是类变量,所有没被覆盖的实例都会看到
Employee.company = "新公司"
print(e1.company, e2.company, Employee.company) # 推演输出:新公司 新公司 新公司

# ② 通过实例写:创建 / 修改的是实例变量,不碰类
e1.company = "只属于小明的公司"
print(e1.company, e2.company, Employee.company) # 推演输出:只属于小明的公司 新公司 新公司
print("company" in e1.__dict__) # 推演输出:True
print("company" in e2.__dict__) # 推演输出:False

第 ② 步之后,e1的实例字典里多了一个company,从此e1.company在第一步就命中,再也看不到类上的值。这就是「读走类、写归实例」的完整表现。


由此还引出一个经典错误:


# 适用于 Python 3.8+
class Counter:
total = 0

def __init__(self):
self.total += 1 # ❌ 读类变量 → 写实例变量,类上的 total 永远是 0

Counter()
Counter()
print(Counter.total) # 推演输出:0

self.total += 1会被拆成「读self.total(从类拿到 0)」和「写self.total = 1(写进实例字典)」两步,于是类上的计数器纹丝不动。要真的计数,必须写Counter.total += 1。


三、可变类变量的共享陷阱


如果类变量是不可变对象(数字、字符串、元组),「共享」通常无害——你改的时候是新建一个值,影响范围自己说了算。但如果类变量是list、dict、set这类可变对象,共享就会变成陷阱:所有实例看到的是同一个对象。


# 适用于 Python 3.8+
class Team:
members = [] # ❌ 可变的类变量

a = Team()
b = Team()
a.members.append("小明") # 原地修改,改的是类里那个 list
print(b.members) # 推演输出:['小明'] —— b 也看到了
print(a.members is b.members) # 推演输出:True —— 根本是同一个对象

# ✅ 正确做法:每个实例各建一份
class Team2:
def __init__(self):
self.members = [] # 实例变量,各是各的

x = Team2()
y = Team2()
x.members.append("小明")
print(y.members) # 推演输出:[]

判断要点:看你是「重新绑定」还是「原地修改」。



  • self.members = [...]是重新绑定,写进实例字典,各不影响;

  • self.members.append(...)是原地修改,self.members先回退读到类上那个 list,然后就地改它,于是所有实例一起中招。


同一个坑还有另一种形态:用字典当「注册表」。如果它是类变量,注册项会在不同的测试、不同的实例之间串味。凡是「每个实例应当各有一份」的可变状态,一律在__init__里创建。


四、__slots__ 与类变量的关系


__slots__的作用是声明实例允许拥有的属性,并阻止自动创建__dict__,从而省下每个实例的一份字典开销。它和类变量的关系有两个必须记住的点。


第一,__slots__里列出的名字会变成类一级的描述符(官方文档的说法是「__slots__通过为每个变量名创建描述符来实现」)。因此你不能再用同名的类变量给这个实例属性设默认值——那样只会让类变量把描述符覆盖掉,赋值行为随之失效。


第二,__slots__只约束「实例属性」,类变量照常可以存在,只要名字不与槽名冲突:


# 适用于 Python 3.8+
class Coord:
__slots__ = ("x", "y") # 只允许这两个实例属性
dim = 2 # 普通类变量,dim 不在槽里,没有任何冲突

def __init__(self, x, y):
self.x = x
self.y = y


p = Coord(1, 2)
print(p.x, p.dim) # 推演输出:1 2
print(hasattr(p, "__dict__")) # 推演输出:False —— 没有实例字典了
try:
p.z = 3
except AttributeError as exc:
print("拒绝:", exc) # 推演输出:拒绝:'Coord' object has no attribute 'z'

还要注意继承上的两个细节:如果父类没有定义__slots__,子类实例照样会有__dict__;如果子类自己不定义__slots__,它也会重新获得__dict__。想让整条继承链都保持「无字典」,每一层都得声明__slots__。




写法结果



__slots__ = ("x", "y")实例只允许x、y,没有__dict__

__slots__ = "xy"字符串被当作可迭代对象,等价于两个槽x、y

槽名同名再写类变量类变量覆盖描述符,槽的赋值行为失效

子类不写__slots__子类实例重新拥有__dict__

类变量名不在槽里互不影响,可以正常读写



常见坑点


1. 用实例名给类变量「赋值」,以为改了全局。


❌ 在方法里写self.total += 1做全局计数。 ✅ 想改类变量就用类名:Counter.total += 1;写self.total只会新建一个实例属性。


2. 把可变对象直接写成类变量。


❌members = []当默认容器,一处append处处见效。 ✅ 需要实例各一份的可变状态,一律在__init__里self.members = []。


3. 用类名改类变量后,疑惑为什么有的实例没变。


❌Employee.company = "新公司"之后发现e1.company还是旧值。 ✅ 因为e1的实例字典里已有同名项,挡住了类属性;检查"company" in e1.__dict__即可确认。


4.__slots__里同时写了槽名和同名类变量。


❌__slots__ = ("x",)又写x = 0想给默认值,结果槽的赋值约束被覆盖。 ✅ 默认值应当在__init__里赋给self.x;槽名不要再用类变量定义一次。


5.__slots__写成了字符串。


❌__slots__ = ("x"),其实这不是元组而是字符串"x",多字符时会按字符逐个建槽。 ✅ 单元素元组要写逗号:__slots__ = ("x",);或者直接用__slots__ = ["x", "y"]。


6. 子类没写__slots__,以为能保持无__dict__。


❌ 父类有__slots__就以为整个继承链都省内存。 ✅ 子类会重新获得__dict__;要么子类也声明__slots__(只列新增字段),要么接受字典开销。


7. 对实例执行del以为删掉的是类变量。


❌del e1.company之后以为类上的company消失了。 ✅del只作用于实例字典;实例里没有该属性时会直接抛AttributeError。


总结




操作写法落点



读类变量Cls.name或self.name(未覆盖时)类字典

读实例变量self.name或obj.name实例字典

改写类变量Cls.name = v该类自己的字典

新建实例变量self.name = v该实例的字典

原地改可变类变量self.name.append(v)类里的那个对象(全实例共享)

声明实例属性集合__slots__生成类一级描述符,去掉__dict__



一句话收尾:读是回退查找,写是看「赋值给谁」;而可变的类变量因为只有一份对象,任何原地修改都会被所有实例看见——把这两条记牢,类变量与实例变量的所有怪现象就都有了解释。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询