属性、方法与 self

上一篇 类与对象基础 里讲了类的骨架。这一篇要深入类里的”血肉”:属性怎么组织、方法怎么调用、self 背后到底发生了什么。你会看到 Python 的 OOP 比 Java 灵活得多:属性可以动态添加、方法可以借给别的对象、self 只是位置参数……理解这些能让你写出更 Pythonic 的类。

一、属性的三种来源

一个对象的属性可能来自三处:

  1. 实例属性self.x = ...)——每个实例独立;
  2. 类属性(在类体里赋值)——所有实例共享;
  3. 继承的属性——从父类来的。

访问 obj.x 时,Python 按 实例 → 类 → 父类 → 父父类 … 的顺序查找(MRO 顺序,下一篇讲)。

二、动态添加属性

Python 允许你在任何时候给一个实例加属性:

1
2
3
4
5
6
7
8
class Foo:
pass

f = Foo()
f.x = 10 # 动态加实例属性
f.greeting = "hi"

print(f.x, f.greeting)

灵活是灵活,但不建议在生产代码里滥用——IDE 无法补全,读代码的人一头雾水。规范做法是在 __init__ 里把所有实例属性声明清楚。

三、如何限制属性 —— __slots__

如果一个类会创建大量实例,或你希望”禁止使用者随便加属性”,用 __slots__

1
2
3
4
5
6
7
8
class Point:
__slots__ = ("x", "y")

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

p = Point(3, 4)
p.z = 5 # ❌ AttributeError

好处:

  • 每个实例内存大幅减少(不再需要 __dict__);
  • 属性名拼错立刻报错;
  • 属性访问略快。

代价:

  • 不能动态加属性;
  • 子类要自己声明 __slots__,否则会退回普通模式。

大规模建模(几百万实例)时非常有价值。日常小类不必强求。

四、__dict__:实例属性的字典

普通类的实例其实是个字典 + 一些别的:

1
2
3
4
5
6
7
8
9
class User:
def __init__(self, name, age):
self.name = name
self.age = age

u = User("咖飞", 3)
print(u.__dict__) # {'name': '咖飞', 'age': 3}
u.__dict__["city"] = "北京"
print(u.city) # 北京

这就是为什么 Python 属性这么”动态”——它们真的存在字典里。

五、hasattr / getattr / setattr / delattr

前面 常用内置函数汇总 提过:

1
2
3
4
5
6
7
8
u = User("咖飞", 3)

hasattr(u, "name") # True
getattr(u, "name") # '咖飞'
getattr(u, "gender", "未知") # '未知' 带默认值

setattr(u, "gender", "M") # 等价 u.gender = "M"
delattr(u, "age") # 等价 del u.age

反射用法适合写”框架/序列化/迁移工具”,日常业务不常用。

六、方法的绑定

看这段代码:

1
2
3
4
5
6
7
8
9
class Dog:
def bark(self):
print(f"{self.name} 在叫")

d = Dog()
d.name = "旺财"

print(d.bark) # <bound method Dog.bark of <Dog object ...>>
print(Dog.bark) # <function Dog.bark at 0x...>
  • Dog.bark普通函数
  • d.bark绑定方法——Python 自动把 d 绑定为 self

绑定发生在属性查找那一刻,用的是描述符协议(进阶话题,可以先不深究)。

七、方法可以互相调用

1
2
3
4
5
6
7
8
9
10
11
12
class Circle:
def __init__(self, r):
self.r = r

def diameter(self):
return 2 * self.r

def area(self):
return 3.14159 * self.r ** 2

def summary(self):
return f"半径 {self.r}, 直径 {self.diameter()}, 面积 {self.area():.2f}"

一个方法调用另一个用 self.method()——因为 self 就是当前实例,self.method 就是绑定方法。

八、property:把方法伪装成属性

有时候某个”属性”其实是计算出来的,你想让使用者像访问属性一样访问它,用 @property

1
2
3
4
5
6
7
8
9
10
11
class Circle:
def __init__(self, r):
self.r = r

@property
def area(self):
return 3.14159 * self.r ** 2

c = Circle(3)
print(c.area) # 28.27...,不加括号!
c.area = 100 # AttributeError: can't set

好处:

  • 使用者不用关心是”字段”还是”计算”;
  • 将来把字段改成计算属性时,调用方代码不用改;
  • 可以配合 setter/deleter 加验证。

完整的 property 讲解见 类方法、静态方法与 property

九、动态改方法

Python 里方法也是属性,一样可以动态改:

1
2
3
4
5
6
7
8
9
10
class Foo:
def bark(self):
print("汪")

def meow(self):
print("喵")

Foo.bark = meow # 整个类都改了

Foo().bark() # 喵

或者只改一个实例:

1
2
3
4
import types

f = Foo()
f.bark = types.MethodType(meow, f) # 只 f 变了

工程实践里几乎不用,但你知道能这么玩就好。

十、self 只是约定

前面说过——Python 里 self 不是关键字,你写别的也行:

1
2
3
4
5
6
7
8
9
class Foo:
def bar(this): # 用 this
print(this)

def baz(banana): # 用 banana
print(banana)

Foo().bar()
Foo().baz()

但请永远写 self。整个 Python 社区都这么写,标新立异只会让代码难读。

十一、方法的三种类型

Python 类里有三种方法:

  • 实例方法:第一个参数是 self,最常见;
  • 类方法@classmethod,第一个参数是 cls(类本身);
  • 静态方法@staticmethod,不接受隐式参数,本质就是”放在类里的普通函数”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo:
def instance_method(self):
print(f"实例方法,self={self}")

@classmethod
def class_method(cls):
print(f"类方法,cls={cls}")

@staticmethod
def static_method():
print("静态方法")


f = Foo()
f.instance_method() # 实例方法,self=...
Foo.class_method() # 类方法,cls=Foo
Foo.static_method() # 静态方法

具体用法见 类方法、静态方法与 property

十二、类属性 vs 实例属性的读写规则

1
2
3
4
5
6
7
8
9
10
class Foo:
count = 0 # 类属性

def __init__(self):
Foo.count += 1 # 修改类属性要用类名

def bump_wrong(self):
self.count += 1 # 等价 self.count = self.count + 1
# 读取时找到类属性 0,写入时创建实例属性 1
# 之后 self.count 都是实例属性,类属性没变

规则

  • 读属性:先实例,再类;
  • 写属性:永远写实例属性(除非用 ClassName.attr = ...)。

用类属性做”实例计数”、”共享配置”时要用类名写:

1
2
3
4
5
6
7
8
class Widget:
total = 0
def __init__(self):
Widget.total += 1

Widget()
Widget()
print(Widget.total) # 2

十三、封装的实际做法

Java 里”getter/setter”是标配。Python 完全不必

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ Java 风格
class User:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, name):
self._name = name

# ✅ Python 风格
class User:
def __init__(self, name):
self.name = name

Python 提倡”直接访问属性”。如果将来需要加验证或计算,再改成 @property——调用方代码不用改一个字。

十四、__slots__ 与继承

__slots__ 需要从上到下都声明才有效:

1
2
3
4
5
class Parent:
__slots__ = ("a",)

class Child(Parent):
__slots__ = ("b",) # 只声明新的,不重复父类的

如果任何一层没写 __slots__,就会退回普通 __dict__ 模式。

十五、一个完整例子:银行账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# bank.py
"""银行账户,展示属性、方法、property、类属性。"""


class BankAccount:
# 类属性
bank_name = "咖飞银行"
_next_id = 1000

def __init__(self, owner: str, balance: float = 0):
self.id = BankAccount._next_id
BankAccount._next_id += 1
self.owner = owner
self._balance = balance # 私有约定,让外部走 property

@property
def balance(self) -> float:
return self._balance

@balance.setter
def balance(self, value: float) -> None:
if value < 0:
raise ValueError("余额不能为负")
self._balance = value

def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("金额必须 > 0")
self._balance += amount

def withdraw(self, amount: float) -> None:
if amount > self._balance:
raise ValueError("余额不足")
self._balance -= amount

def transfer(self, other: "BankAccount", amount: float) -> None:
self.withdraw(amount)
other.deposit(amount)

def __repr__(self):
return f"BankAccount(id={self.id}, owner={self.owner!r}, balance=¥{self._balance})"


if __name__ == "__main__":
a = BankAccount("咖飞", 1000)
b = BankAccount("小王", 500)

a.deposit(500)
a.transfer(b, 300)
print(a)
print(b)
print(f"银行:{BankAccount.bank_name}")

十六、常见陷阱

  1. 忘了 selfdef method(x) 会导致调用时报错。
  2. 给类属性 += 变成加实例属性:想改类属性用 ClassName.attr = ...
  3. __slots__ 与 dataclass 组合:3.10+ 用 @dataclass(slots=True)
  4. 过度封装:写一堆 getter/setter 不 Pythonic,直接暴露属性即可。
  5. 动态属性拼写错误f.nmae = "x" 不报错,但读的时候没有。用 __slots__ 或 dataclass 可以避免。
  6. 修改方法查找规则:一般别改,除非你在写元编程。

十七、小结与延伸阅读

  • 实例属性在 __init__ 里初始化,类属性写在类体;
  • self.x 读时查实例→类,写时只创建实例属性;
  • hasattr/getattr/setattr 是反射三件套;
  • __slots__ 减内存、防止乱加属性;
  • 方法可以互相调用,self.method() 就是绑定方法;
  • Python 不写 getter/setter,需要计算/验证时用 @property
  • self 只是约定,永远写 self。

延伸阅读:

下一篇 继承与多态 我们讲 OOP 的核心特性之一——从父类继承。