最近在刷 CS61A 的时候发现,Python 的面向对象编程(OOP)这块内容其实挺有意思的。之前写 Python 一直是函数式风格,能用 function 就不用 class,但学到后面发现 class 这东西有些场景下是真的好用。今天就来聊聊 Python OOP 的那些事儿。
为什么要用 Class?
说实话,Python 里很多东西不用 class 也能搞。但是当你需要把数据和操作数据的方法绑在一起的时候,class 就很香了。比如你在做一个游戏,有很多角色,每个角色有名字、血量、攻击力,还能攻击、回血——这时候用 dict 来管理就很混乱了。
举个简单的例子:
class Character:
def __init__(self, name, hp, attack):
self.name = name
self.hp = hp
self.attack = attack
def is_alive(self):
return self.hp > 0
def take_damage(self, damage):
self.hp -= damage
if self.hp < 0:
self.hp = 0
这样每个角色就是一个 Character 对象,数据和行为都在一起。比起用一堆 dict 加一堆函数,可读性好太多了。
__init__ 和 self 到底是什么
Python 新手最困惑的可能就是 self 这个东西了。每次定义方法都要写 self 作为第一个参数,调用的时候又不用传,这是什么鬼?
self就是指向当前实例的引用。当你调用hero.take_damage(10)的时候,Python 会自动把它变成Character.take_damage(hero, 10)。所以self是 Python 帮你传进去的,你不用手动写。
__init__ 是一个 special method(也叫 dunder method,因为两边都是 double underscore),它在你创建对象的时候被调用。注意它不是 constructor,严格来说 Python 的 constructor 是 __new__,但 99% 的情况下你只需要管 __init__ 就行了。
# 这两行是等价的
hero = Character("Alice", 100, 25)
hero = Character.__init__(hero, "Alice", 100, 25) # 不推荐这样写,但意思到了
Inheritance:继承这东西其实挺好理解的
继承就是让你基于已有的 class 创建新的 class。比如我们有 Character,现在想加一个 Wizard 角色,他除了普通攻击还能放魔法。不需要重写一遍所有代码,直接继承:
class Wizard(Character):
def __init__(self, name, hp, attack, mana):
super().__init__(name, hp, attack) # 调用父类的 __init__
self.mana = mana
def cast_spell(self, spell_name, cost):
if self.mana >= cost:
self.mana -= cost
print(f"{self.name} casts {spell_name}!")
return True
print("Not enough mana!")
return False
super().__init__() 这个调用很重要。如果不写的话,父类的 __init__ 就不会被调用,你的 Wizard 对象就不会有 name、hp 这些属性。
Python 支持多重继承(multiple inheritance),但说实话日常开发中很少用到。用多了容易出问题,比如著名的 diamond problem。MRO(Method Resolution Order)能帮你解决一部分,但我觉得能避免就避免。
Dunder Methods:让对象更好用
Dunder method(double underscore method)是 Python OOP 里最酷的部分。通过定义这些 special method,你可以让你的对象像内置类型一样使用。
几个最常用的:
__str__:定义print(obj)时显示的内容__repr__:定义在交互式环境里直接输入obj显示的内容__len__:定义len(obj)的行为__eq__:定义obj1 == obj2的比较逻辑__add__:定义obj1 + obj2的行为
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __len__(self):
# 让 len() 返回向量的维度
return 2
# 用起来就很自然
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 == v2) # False
print(len(v1)) # 2
一开始我也搞不懂这些 dunder method 有什么用,后来发现一旦你开始写 library 或者 API,这些东西就变得非常重要了。让你的 class 用起来像 Python 原生类型,用户就不需要记新的接口。
Property:让方法看起来像属性
有时候你想在访问属性的时候做一些额外操作(比如验证、计算),但又不想改调用方式。这时候 @property 就派上用场了:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""计算面积,不需要括号"""
import math
return math.pi * self._radius ** 2
c = Circle(5)
print(c.area) # 78.53... 注意没有括号
c.radius = 10 # 用的是 setter
print(c.area) # 314.15...
用了 @property 之后,c.area 看起来像是在访问一个普通属性,但实际上每次访问都会调用方法来计算。这样 API 的使用者不需要知道背后是方法还是属性,用起来更自然。
Class Variable vs Instance Variable
这个区别刚开始很容易搞混。简单来说:
- Instance variable:每个实例各有一份,定义在
__init__里的self.xxx - Class variable:所有实例共享一份,定义在 class body 里
class Dog:
species = "Canis familiaris" # class variable,所有狗共享
dog_count = 0 # 统计创建了多少只狗
def __init__(self, name):
self.name = name # instance variable,每只狗不同
Dog.dog_count += 1
d1 = Dog("Buddy")
d2 = Dog("Max")
print(d1.species) # Canis familiaris
print(d2.species) # Canis familiaris
print(Dog.dog_count) # 2
一个常见的坑:如果你试图通过 instance 去修改 class variable,实际上会创建一个同名的 instance variable,而不是修改 class variable:
d1.species = "Wolf" # 创建了一个 instance variable
print(d1.species) # Wolf(instance variable)
print(d2.species) # Canis familiaris(class variable 没变)
print(Dog.species) # Canis familiaris
实用技巧和 Pattern
最后分享几个实用的 pattern:
1. dataclass —— 懒人福音
Python 3.7 引入的 dataclass 可以自动生成 __init__、__repr__、__eq__ 等方法,省去大量样板代码:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance_to(self, other):
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0) 自动生成的 __repr__
2. Composition over Inheritance
虽然继承很方便,但在很多情况下 composition(组合)是更好的选择。简单说就是”has-a” 比 “is-a” 更灵活:
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine() # Car has an Engine
def start(self):
self.engine.start()
print("Car is ready to go")
3. __slots__ 节省内存
如果你要创建大量对象(比如几百万个),可以用 __slots__ 来减少内存占用:
class Point:
__slots__ = ['x', 'y'] # 只允许这两个属性
def __init__(self, x, y):
self.x = x
self.y = y
# 每个实例省掉了 __dict__ 的开销,内存少用约 40%
总结
Python 的 OOP 跟 Java、C++ 那套不太一样,它更灵活也更 “Pythonic”。几个关键点:
self就是实例引用,Python 自动传- 继承用
super()调用父类方法 - Dunder method 让你的对象像内置类型一样好用
@property让方法伪装成属性- Class variable 是所有实例共享的,instance variable 是每个实例独有的
- 能用
dataclass就用,省事 - Composition 很多时候比 Inheritance 更好
好了,以上就是我对 Python OOP 的一些理解和笔记。写 class 这件事,一开始会觉得很繁琐,但用熟了之后会发现它确实能让代码组织得更清楚。尤其是在写稍微大一点的项目的时候,没有 class 真的会很乱。