RSS
菜单
全部文章快讯开发科技深度热点

面向对象编程(Python 从精通到入门 · 18)

内容摘要

类是蓝图,实例是照着蓝图造出来的产物。这篇把「怎么定义类、怎么用类」讲透,而不是背诵术语。

类是蓝图,实例是照着蓝图造出来的产物。这篇把「怎么定义类、怎么用类」讲透,而不是背诵术语。

你将学到

  • 用 class 定义类,理解 __init__ 和 self 到底在干什么
  • 分清实例属性与类属性,知道可变类属性为什么是坑
  • 掌握实例方法、@classmethod、@staticmethod 的区别与用途
  • 用继承与 super() 复用代码,对 MRO 有个直观认识
  • 会用 @property、__repr__ 等入门魔术方法,并记住「组合优于继承」

前置知识

假定你已经会写函数、会用列表和字典、见过缩进和 for 循环。这一篇回到语言本身,不依赖前面任何一章。

类是蓝图,实例是产物

class Dog:
    """一只狗的类。"""

    def __init__(self, name):      # 实例创建后会自动调用
        self.name = name           # self 就是"正在创建的实例",给它挂属性

    def bark(self):                # 第一个参数永远是实例本身,约定叫 self
        return "汪汪!"

d1 = Dog("旺财")                   # Dog(...) 就是"调用类",产生一个实例
d2 = Dog("小黑")
print(d1 is d2)                    # 输出: False,每次都是全新对象
print(d1.bark())                   # 输出: 汪汪!
print(d1.name)                     # 输出: 旺财

class Dog: 定义蓝图,Dog(...) 照蓝图造实例,d1 与 d2 彼此独立。几个要点:

  • self 不是关键字,只是第一个参数的约定名,写成别的也能跑,但别这么干。
  • d1.bark() 等价于 Dog.bark(d1),Python 自动把 d1 塞进 self。
  • 带 self. 的才是实例属性;不带 self. 的只是局部变量,函数结束就没了。

实例属性 vs 类属性

挂在 self 上的是实例属性,挂在类上的是类属性。

class Circle:
    pi = 3.14159               # 类属性,所有实例共享

    def __init__(self, r):
        self.r = r             # 实例属性,每个实例各一份

c1, c2 = Circle(1), Circle(2)
print(c1.pi, c2.pi)            # 输出: 3.14159 3.14159

c1.pi = 3                      # 通过实例赋值,会给 c1 新建实例属性,遮蔽类属性
print(c1.pi, Circle.pi)        # 输出: 3 3.14159   ← 类属性本身没变

查找顺序是:先找实例属性,找不到再找类属性。注意别把可变对象(列表、字典)当类属性:class Team: members = [] 会被所有实例共享,a.members.append("小明") 之后 b.members 里也会冒出"小明"。正确做法是在 __init__ 里 self.members = [],给每个实例建独立的一份。

三种方法:实例方法 / 类方法 / 静态方法

class Demo:
    def instance_method(self):     # 实例方法:能拿到 self
        return "要实例才能调"

    @classmethod
    def class_method(cls):         # 类方法:第一个参数是类本身 cls
        return "类就能调"

    @staticmethod
    def static_method():           # 静态方法:不碰 self 也不碰 cls
        return "只是挂在类里的普通函数"

print(Demo.class_method())         # 输出: 类就能调
print(Demo.static_method())        # 输出: 只是挂在类里的普通函数
print(Demo().instance_method())    # 输出: 要实例才能调

选择原则:需要读写 self 用实例方法;@classmethod 最典型的用途是"备用构造器",比如 Date.from_string("2026-10-07") 返回一个 cls(...);@staticmethod 适合逻辑上属于该类、但不需要对象状态的工具函数。用 cls 而非写死类名,子类继承后也能正确创建。

继承与 super()

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."

class Cat(Animal):                 # Cat 继承 Animal
    def speak(self):               # 覆盖(override)父类方法
        return "喵"

class Dog(Animal):
    def speak(self):
        return "汪"

for a in [Cat("咪咪"), Dog("旺财")]:
    print(a.name, a.speak())
# 输出: 咪咪 喵
# 输出: 旺财 汪

要在子类里调用父类的实现,用 super()。

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name)     # 先让父类初始化 name
        self.color = color         # 再加自己的属性

c = Cat("咪咪", "橘")
print(c.name, c.color)             # 输出: 咪咪 橘

MRO 一句话版:一个类可以继承多个父类,方法查找顺序叫 MRO(方法解析顺序),由 C3 算法算出一套唯一顺序,直观理解是"先自己的类,再按继承列表从左到右、每个分支先深后广"。用 D.__mro__ 能看到确切顺序,例如 D(B, C) 会得到 [D, B, C, A, object]。日常配合 super() 用就好,别刻意搞复杂多继承。

封装:下划线约定

Python 没有 private 关键字,靠命名约定表达可见性:

  • name:公开。
  • _name:单下划线,表示"内部使用,别碰"。
  • __name:双下划线开头,会触发名字改写(name mangling),实际变成 _ClassName__name。
class Account:
    def __init__(self, balance):
        self._balance = balance    # 约定为内部属性

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("金额必须为正")
        self._balance += amount

a = Account(100)
a.deposit(50)
print(a._balance)                  # 输出: 150

约定不是强制,靠的是"大家都懂规矩"。

@property:把方法变成属性

想对外提供只读值,或让赋值经过校验,用 @property。

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def fahrenheit(self):          # 像属性一样访问,不用加括号
        return self._celsius * 9 / 5 + 32

    @celsius.setter                # 定义 celsius 的 setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("低于绝对零度了")
        self._celsius = value

t = Temperature(25)
print(t.fahrenheit)                # 输出: 77.0
t.celsius = 30                     # 走 setter
print(t._celsius)                  # 输出: 30

魔术方法入门

双下划线包起来的方法(dunder)让对象接入 Python 的语法。入门先掌握三个:

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

    def __repr__(self):            # 给开发者看,调试时显示
        return f"Point({self.x}, {self.y})"

    def __str__(self):             # 给用户看,print 时显示
        return f"({self.x}, {self.y})"

    def __eq__(self, other):       # 定义 == 的比较规则
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

p = Point(1, 2)
print(p)                           # 输出: (1, 2)       走 __str__
print(repr(p))                     # 输出: Point(1, 2) 走 __repr__
print(p == Point(1, 2))            # 输出: True

注意:一旦定义 __eq__,对象默认就不可哈希了,想放进 set 或当 dict 键,得再定义 __hash__。

组合优于继承

继承表达 "is-a"(猫是动物),组合表达 "has-a"(车有引擎)。现实里 "has-a" 更常见。当你发现继承层次超过两三层、或子类被迫实现一堆用不到的方法,就该换成组合。

class Engine:
    def start(self):
        return "引擎启动"

class Car:
    def __init__(self):
        self.engine = Engine()     # 组合:Car 拥有一个 Engine
    def start(self):
        return f"汽车:{self.engine.start()}"

print(Car().start())               # 输出: 汽车:引擎启动

一个完整小例子:银行账户

把前面的知识点串起来。

class InsufficientFunds(Exception):
    """余额不足时抛出。"""

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金额必须大于 0")
        self._balance += amount
        return self._balance

    def withdraw(self, amount):
        if amount > self._balance:
            raise InsufficientFunds(f"余额不足:{self._balance}")
        self._balance -= amount
        return self._balance

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

acc = BankAccount("小明", 100)
acc.deposit(50)
print(acc.balance)                 # 输出: 150
try:
    acc.withdraw(500)
except InsufficientFunds as e:
    print("失败:", e)              # 输出: 失败: 余额不足:150
print(acc)                         # 输出: BankAccount(owner='小明', balance=150)

常见坑

❌ 忘记写 self.,属性没挂到实例上;✅ 加上 self.。

class Counter:
    def __init__(self):
        count = 0                  # ❌ 局部变量,实例上什么都没有
        self.count = 0             # ✅ 这才是实例属性

❌ 子类 super().__init__() 漏传父类需要的参数;✅ 传对参数。

class Cat(Animal):
    def __init__(self, name, color):
        # super().__init__()       # ❌ 父类需要 name,这里没传,稍后会报错
        super().__init__(name)     # ✅
        self.color = color

❌ 用可变对象当类属性;✅ 在 __init__ 里创建。

class Cart:
    items = []                     # ❌ 所有实例共用同一个列表
    def __init__(self):
        self.items = []            # ✅ 每个实例各自一份

小结

  • class 是蓝图,Foo() 是产物;self 指向实例自己。
  • 用 self.x = ... 才是实例属性;类属性共享,当心可变对象的坑。
  • 方法分实例方法、@classmethod、@staticmethod,按是否需要 self/cls 选。
  • 继承用 super() 复用父类;MRO 决定多继承的查找顺序,日常别滥用。
  • _x/__x 是约定式封装;@property 用起来像属性,实则走方法。
  • __repr__/__str__/__eq__ 让对象融入 Python 语法,__repr__ 尽量别省。
  • 能用组合就别硬套继承。

延伸阅读

  • 熟练篇《数据处理 json / csv / dataclasses》里的 @dataclass 能几行生成 __init__/__repr__/__eq__,写"数据类"时优先用它,别手写。
  • 大师篇《元类与类创建机制》会讲类本身是怎么被造出来的,读完回头看这篇会豁然开朗。

上一篇:网络请求与 Web 基础 · 下一篇:模块与包

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《面向对象编程(Python 从精通到入门 · 18)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。