Python · 7 分钟阅读
OOP 面向对象编程
OOP 把「数据」和「操作数据的函数」封装到 对象 里,强调「仔细设计」「代码复用」「大型程序组织」。本文用一组逐步演化的例子把 Python OOP 核心概念串起来:类与对象、__init__、self、继承与 super()、@property、魔术方法、抽象基类。
读完这一篇你应该能:
- 定义类、创建对象
- 理解继承与多态的关系
- 知道什么时候用
@property/@classmethod/@staticmethod - 用魔术方法把对象接入 Python 协议
- 用
abc.ABC写明确的接口约定
1. 类与对象
类(class) 是创建对象的蓝图,封装了 属性(数据) 和 方法(行为);对象(object / instance) 是类的实例化产物。
Python 中一切都是对象——整数、字符串、列表、字典、函数、模块,都是某个类的实例。
类名建议用 大驼峰(MyClass):
class Dog:
"""一个简单的狗类。"""
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def sit(self) -> None:
print(f"{self.name} 坐下了。")
def roll_over(self) -> None:
print(f"{self.name} 打滚了。")
一个类通常由以下部分组成:
| 成员 | 作用 |
|---|---|
| 类属性 | 所有实例共享,写在方法外 |
__init__ |
构造器,初始化实例属性 |
| 实例方法 | 第一个参数是 self |
| 类方法 | @classmethod,第一个参数是 cls |
| 静态方法 | @staticmethod,不依赖类或实例 |
| 属性方法 | @property,把方法当属性访问 |
| 魔术方法 | __repr__ / __str__ / __eq__ / __hash__ / … |
__init__是构造函数,每次Person()都会自动调用。它不构造对象本身(那是__new__的事),而是初始化新对象的属性。
创建对象
my_dog = Dog("旺财", 3)
print(f"我的狗叫 {my_dog.name},它 {my_dog.age} 岁了。")
my_dog.sit()
my_dog.roll_over()
Dog("旺财", 3)自动调用__init__;my_dog.sit()自动把my_dog作为self传入。
2. 实例方法与 self
实例方法的第一个参数 必须 是 self(约定俗成),表示"当前实例"。
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def display(self) -> None:
print(f"姓名: {self.name}, 年龄: {self.age}")
def have_birthday(self) -> None:
self.age += 1
调用 p.have_birthday() 时,Python 自动把 p 作为 self 传进去。
3. __str__ / __repr__
class Person:
def __init__(self, name: str = "", age: int = 0) -> None:
self.name = name
self.age = age
def __str__(self) -> str:
return f"Person({self.name!r}, {self.age})"
def __repr__(self) -> str:
return f"Person({self.name!r}, {self.age})"
p = Person("Richie", 18)
print(p) # Person(name='Richie', age=18)
print(repr(p)) # Person('Richie', 18)
| 方法 | 用途 |
|---|---|
__str__ |
给 print / str() 用,面向用户 |
__repr__ |
给 REPL / repr() 用,面向开发者;目标能复制回来重建对象 |
经验:写类时至少实现
__repr__;str()在没有__str__时会退回到__repr__。
4. @property:把方法当属性用
直接暴露属性 vs. 用 @property 加验证:
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self._age = 0 # 用下划线表示"受保护"
self.age = age # 走 setter,做校验
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, value: int) -> None:
if not (0 < value <= 150):
raise ValueError("age 必须在 1~150 之间")
self._age = value
Person("Mia", 30).age # 30
# Person("Mia", 200) # ValueError
“方法当属性用” 让你写 m.age 而不是 m.get_age();同时仍然能在赋值时做校验。
5. 私有与受保护
Python 没有真正的"私有"——只有约定和名字改编:
| 形式 | 含义 |
|---|---|
self.name |
公有 |
self._name |
“受保护”,约定上不外部访问 |
self.__name |
名字改编为 _ClassName__name,更难被意外访问 |
class Secret:
def __init__(self) -> None:
self.__token = "shh" # 私有
s = Secret()
# s.__token # AttributeError
s._Secret__token # 'shh' —— 仍能访问,但不推荐
6. 继承与方法重写
继承允许"在新类里复用既有类的属性和方法":
class GoldenRetriever(Dog):
"""金毛猎犬,继承自 Dog。"""
def fetch(self) -> None:
print(f"{self.name} 捡回了物品。")
gold = GoldenRetriever("Luffy", 5)
gold.sit() # 来自 Dog
gold.fetch() # 自己的
方法重写 + super() 协同父类:
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
for a in [Dog("旺财"), Cat("小咪")]:
print(a.name, "->", a.speak())
# 旺财 -> Woof!
# 小咪 -> Meow!
子类想扩展父类行为时用 super():
class Dog(Animal):
def __init__(self, name: str, breed: str) -> None:
super().__init__(name) # 让父类初始化 name
self.breed = breed
继承的几种姿势:
| 语法 | 说明 |
|---|---|
class B(A): |
单继承 |
class C(A, B): |
多继承(注意 MRO) |
super().__init__(...) |
调用父类的方法 |
class D(A, metaclass=...): |
元类(高阶话题,初学不必深究) |
7. 多态:鸭子类型
Python 的多态不依赖继承,而是 “只要有这个方法/属性,就能用”:
class Cat:
def make_sound(self) -> None:
print("喵喵")
class Dog:
def make_sound(self) -> None:
print("汪汪")
def animal_sound(animal) -> None:
animal.make_sound() # 不在意 animal 是哪一类
animal_sound(Cat())
animal_sound(Dog())
同一段代码能作用于不同类型的对象,就是多态。Python 的多态主要靠 鸭子类型 实现:只要对象有
make_sound这个方法就能用。
这就是 鸭子类型(Duck Typing):“如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子”。这种"行为决定类型"的风格让代码更灵活,但也更需要单元测试来兜底。
8. 类方法与静态方法
from datetime import date
class Person:
species = "Homo sapiens" # 类属性
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name: str, year: int) -> "Person":
"""另一种构造方式:给定出生年份。"""
return cls(name, date.today().year - year)
@staticmethod
def is_adult(age: int) -> bool:
"""不依赖实例/类的纯逻辑。"""
return age >= 18
p = Person.from_birth_year("Bob", 2000)
print(p.age) # 26(视当前年份而定)
print(Person.is_adult(20)) # True
@classmethod:第一个参数是cls(类本身),常用于替代构造。@staticmethod:与类/实例都没关系,组织上挂在类下而已。
9. 魔术方法(部分速查)
| 方法 | 触发时机 |
|---|---|
__init__ / __new__ |
实例化 |
__del__ |
销毁(不保证立即触发) |
__repr__ / __str__ |
字符串 / 打印 |
__len__ |
len(obj) |
__getitem__ / __setitem__ |
索引 / 切片 |
__iter__ / __next__ |
for ... in obj |
__contains__ |
x in obj |
__eq__ / __lt__ / __hash__ |
比较与哈希 |
__call__ |
obj() 像函数一样调用 |
__enter__ / __exit__ |
with obj: |
示例:让对象可比较、可哈希:
class Point:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self) -> int:
return hash((self.x, self.y))
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
定义
__eq__时默认会让对象不可哈希;需要哈希就显式实现__hash__,二者一起改才安全。
10. 抽象基类(abc)
当一组类应该有同样的接口时,用 abc.ABC 明确表达"必须实现":
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Rect(Shape):
def __init__(self, w: float, h: float) -> None:
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
def perimeter(self) -> float:
return 2 * (self.w + self.h)
- 子类必须实现所有抽象方法才能被实例化。
@abstractmethod只是声明约束;具体方法、属性都可以在抽象基类中提供。
11. 综合案例:Undercut 游戏
把所有点串起来做一个简单的小游戏,看 OOP 怎么自然地让代码长出形状:
import random
class Player:
def __init__(self, name: str) -> None:
self._name = name
self._score = 0
def get_name(self) -> str:
return self._name
def reset_score(self) -> None:
self._score = 0
def incr_score(self) -> None:
self._score += 1
def get_move(self) -> int:
raise NotImplementedError
def __str__(self) -> str:
return f"Player(name={self._name!r}, score={self._score})"
class Human(Player):
def get_move(self) -> int:
while True:
try:
n = int(input(f"{self.get_name()} move (1-10): "))
if 1 <= n <= 10:
return n
except ValueError:
pass
print("Oops! 请重新输入 1~10 的整数。")
class Computer(Player):
def get_move(self) -> int:
return random.randint(1, 10)
def play_undercut(p1: Player, p2: Player) -> Player | None:
p1.reset_score()
p2.reset_score()
m1, m2 = p1.get_move(), p2.get_move()
print(f"{p1.get_name()}: {m1} {p2.get_name()}: {m2}")
if m1 == m2 - 1:
p1.incr_score()
return p1
if m2 == m1 - 1:
p2.incr_score()
return p2
return None # 平局
if __name__ == "__main__":
winner = play_undercut(Human("Lia"), Computer("Hal-9000"))
print("winner:", winner or "draw")
观察 OOP 在这里带来的好处:
Player把"共有行为"抽出来;Human/Computer只重写"自己不一样"的部分(get_move);play_undercut只依赖Player接口,不关心是人是机器。
12. 设计建议
- 先私有再公开:先
_age,等真有需要再升级为@property。 - 不要写"什么都装的 god class":一个类一个清晰职责。
- 优先组合而不是继承:
has-a关系比is-a关系更灵活。 - 魔术方法按需实现:
__repr__几乎总要;其它按需。 - 类型注解加上:
__init__(self, name: str, age: int = 0)让 IDE /mypy帮忙查错。 - 测试覆盖:
unittest/pytest,把每个 public 方法都过一遍。
13. 小结
| 概念 | 关键点 |
|---|---|
| 类 / 对象 | class 是蓝图,对象是实例 |
__init__ |
构造时初始化,self 指向当前实例 |
| 继承 | 子类复用父类逻辑;super().__init__() 协同父类 |
| 多态 | 鸭子类型:不靠继承,靠"行为存在" |
@property |
用方法语法做属性访问,附带校验逻辑 |
| 类/静态方法 | @classmethod(cls) / @staticmethod(无依赖) |
| 抽象基类 | abc.ABC + @abstractmethod 明确接口契约 |
| 魔术方法 | __repr__ / __eq__ / __hash__ / __len__ … |