07 · 类与对象(Classes & Objects)

📅 预计 100 分钟 | ⭐ = 高区分度考点
✍️ 本讲配套练习:本地 python grader.py 07,或网页练习


引子:为什么要有"类"?

前面几讲是"面向过程"——数据(变量)和操作(函数)分开。系统一复杂(学生管理、银行、游戏)就乱套:stu1_namestu1_scorestu2_name……变量越来越多,函数参数传来传去,改一处全局跟着改。

面向对象(OOP)把数据和操作它的函数打包:先做"模板"(类 class),再用模板批量造"实物"(对象 object / instance)。属性 + 方法封装在一个整体里,结构一下清晰了。


7.1 类就是图纸,对象就是造出来的实物 ⭐

盖房子打比方:类 = 图纸(规定了几间房、什么颜色,图纸本身不能住人);对象 = 按图纸盖的房子(能住人,同一张图纸能盖很多套,每套独立)。

class Dog:
    """狗:一张「狗」的图纸"""

    def __init__(self, name):    # 构造方法:造实例时自动执行
        self.name = name         # self.name 是实例属性(每只狗的名字)

    def bark(self):              # 实例方法:狗的行为
        return f"{self.name}在叫"

d1 = Dog("旺财")   # 按图纸造一只狗(旺财)
d2 = Dog("大黄")   # 再造一只(大黄)
print(d1.bark())   # 旺财在叫
print(d2.name)     # 大黄
  • 类名用大驼峰 PascalCaseBankAccount)是惯例;定义时不写括号,创建对象才写括号
  • d1d2独立对象,改 d1.name 不影响 d2.name

⚠️ 常见错误

  1. 实例方法少写 selfdef bark(): 调用 d1.bark()TypeError: ... takes 0 positional arguments but 1 was given
  2. 创建实例忘加括号d = Dog 得到"类本身"而非对象,调用方法报错。

7.2 init 与 self:对象出生的那一刻 ⭐

__init__构造方法(constructor),是对象出生时自动执行的"初始化程序",创建时传的参数原样传进来:

class Student:
    def __init__(self, name, score):
        self.name = name          # 把参数"钉"到对象身上
        self.score = score

s = Student("小明", 85)   # 自动执行 __init__("小明", 85)
print(s.name, s.score)   # 小明 85

self 是"当前正在操作的这个对象自己"——执行 s = Student("小明", 85)self 就指 s。第一参数约定叫 self。类不需要初始化属性时可以省略 __init__

⚠️ 常见错误

  1. __init__ 里写 return 返回值:报 TypeError: __init__() should return None。想返回值用普通方法。
  2. 漏写 selfdef __init__(name) → 参数错位,直接报错。
  3. 属性没建好:忘了 self.score = score,一访问 s.scoreAttributeError。好习惯:所有属性一次性在 __init__ 里建好。

7.3 属性:实例变量 vs 类变量 ⭐

  • 实例变量:每造一个对象复制一份,各管各的,用 self.xxx 定义。
  • 类变量:写在类体里、所有实例共享一份
class Dog:
    species = "犬科"            # 类变量:所有狗共享

    def __init__(self, name):
        self.name = name       # 实例变量:每只狗独立

print(Dog.species)   # 犬科(通过类名直接访问)
d = Dog("旺财")
print(d.species)     # 犬科(实例也能读到类变量)
d.species = "猫科"   # ⚠️ 只是在 d 上新建了一个实例变量
print(Dog.species)   # 犬科(类变量一点没变)

最经典的坑d.species = "猫科" 表面像"改了类变量",实际只是给 d 新建了一个同名实例变量,把类变量"遮住"了。

类变量最实用的场景是给所有实例记账——统计创建了多少个对象:

class Student:
    total = 0                      # 类变量:计数器

    def __init__(self, name):
        Student.total += 1         # 关键:用 类名.total,不是 self.total
        self.name = name

s1 = Student("小明")
s2 = Student("小红")
print(Student.total)    # 2
print(s1.name, s2.name) # 小明 小红

若误写 self.total += 1:每个实例把类变量当前值 +1 后赋给实例,于是计数器永远 1、1、1。改类变量必须用 类名.变量名

⚠️ 常见错误

  1. self.xxx 改类变量:改不进去,只会新建同名实例变量。
  2. 可变默认值陷阱(高频)def __init__(self, items=[]) 的默认列表在定义类时只创建一次,所有没传参的对象共用它:
class Bug:
    def __init__(self, items=[]):
        self.items = items

b1 = Bug(); b1.items.append(1)
b2 = Bug()
print(b2.items)   # [1]?!共享了

class Fixed:
    def __init__(self, items=None):
        self.items = items if items is not None else []   # 正确写法
  1. 同名类变量和实例变量混用:一会儿 self. 一会儿 类名.,容易绕晕。选一种,全程一致。

7.4 方法:对象的"行为" ⭐

方法是"属于对象的函数",第一个参数永远是 self,调用时自动传入:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):       # 行为:加一
        self.count += 1

    def value(self):           # 行为:读出当前值
        return self.count

c = Counter()
c.increment()
c.increment()
print(c.value())   # 2

调用方法要加括号c.value 拿的是"方法本身",c.value() 才是执行。

⚠️ 常见错误

  1. 忘写括号print(c.value) 输出 <bound method ...>,不是数值。
  2. 访问属性忘了 self:方法里写 count 而不是 self.countNameError。对象身上存的都要 self.xxx
  3. 方法间互相调用忘写 self.self.reset() 别写成 reset()

7.5 魔法方法:让对象"像内置类型" ⭐

以双下划线开头结尾的方法叫魔法方法(magic method / dunder),不用手动调,Python 在特定时刻自动调用:

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

    def __str__(self):           # print(对象) / str(对象) 时调用 ⭐
        return f"({self.x}, {self.y})"

    def __repr__(self):          # 交互式终端显示对象时调用
        return f"Point({self.x}, {self.y})"

    def __len__(self):           # len(对象) 时调用,必须返回非负整数 ⭐
        return 2

    def __eq__(self, other):     # == 比较时调用 ⭐
        return self.x == other.x and self.y == other.y

    def __add__(self, other):    # + 运算时调用(运算符重载)
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2); p2 = Point(1, 2)
print(p1)            # (1, 2)         调 __str__
print(p1 == p2)      # True           调 __eq__
print(p1 + p2)       # (2, 4)         调 __add__
  • __str__ 面向人(给用户看);__repr__ 面向开发者(理想是"能复现对象的代码")。列表打印内部元素用 __repr__,所以 print([p1]) 显示 [Point(1, 2)]两个都写,调试方便。
  • __eq__:不定义时 p1 == p2 比的是"内存地址",内容一样也不相等;定义后按"值"比较。
  • __len__:必须返回非负整数运算符重载__add__+ 有了新含义。

⚠️ 常见错误

  1. __str__ 不返回字符串:写 return self.x(整数)→ TypeError: __str__ returned non-string
  2. __eq__ 忘了 other 参数:签名必须 def __eq__(self, other),还要提防 other 是别的类型。
  3. __len__ 返回负数/浮点数:如 return -1return 2.0 → 报错。注意 bool 也是整数。
  4. 只写 __str__ 不写 __repr__:把对象放进列表再打印,还是"内存地址"。

7.6 封装:把"私有"藏起来 ⭐

封装(encapsulation):属性一般不让外部直接改,通过方法读写,这样能加校验、保持内部一致。Python 没有真正意义的私有,靠下划线约定:

  • _x:单下划线,"受保护,别乱碰",纯约定,硬要访问也不拦。
  • __x:双下划线,触发 name mangling(名称改写),变成 _类名__x,主要防继承命名冲突,别当安全锁
class Account:
    def __init__(self):
        self._balance = 0          # 受保护:外部不要直接改

    def deposit(self, n):
        if n > 0:
            self._balance += n     # 在方法里做校验

    def withdraw(self, n):
        if 0 < n <= self._balance:
            self._balance -= n
            return n
        return 0

    def get_balance(self):
        return self._balance

acc = Account()
acc.deposit(100)
acc.withdraw(30)
print(acc.get_balance())   # 70

更现代的写法用 @property,把 getter/setter 伪装成"普通属性":

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius        # 走下面的 setter

    @property
    def celsius(self):                # getter:读 t.celsius 时调用
        return self._celsius

    @celsius.setter
    def celsius(self, value):         # setter:写 t.celsius 时调用
        if value < -273.15:           # 绝对零度,物理上不可能
            value = -273.15           # 校验不过就钳制
        self._celsius = value

    @property
    def fahrenheit(self):             # 只读属性:没有 setter
        return self._celsius * 9 / 5 + 32

t = Temperature(100)
print(t.fahrenheit)     # 212.0
t.celsius = 0
print(t.fahrenheit)     # 32.0

t.fahrenheit 读起来像属性(没括号),背后是方法——想加校验、缓存、计算都行。只写 @property 不写 @xxx.setter 的属性是只读的,赋值报 AttributeError

⚠️ 常见错误

  1. 以为 _x 是强制私有:外部照样 acc._balance = 999 不报错,只是"君子协定"。真防改动用 @property 做成只读。
  2. setter 里没做校验:写了 @property 却让外部随便赋值,等于没封装。
  3. property 和同名属性冲突@property def celsius 下面又写 self.celsius = value → 无限递归 RecursionError对外属性名与内部存储名分开(统一存 self._celsius)。

7.7 继承:子类站在父类的肩膀上 ⭐

继承(inheritance):父类写公共部分,子类写自己的特色,不重复造轮子。

class Animal:
    """动物基类"""
    def __init__(self, name):
        self.name = name

    def sound(self):
        return "..."

class Dog(Animal):               # 括号里写父类
    def __init__(self, name):
        super().__init__(name)   # ⭐ 先调父类构造,把 name 初始化好
        self.legs = 4            # 再补自己的属性

    def sound(self):             # 重写 override:覆盖父类方法
        return "汪汪"

class Cat(Animal):
    def sound(self):
        return "喵"

d = Dog("旺财")
print(d.name)     # 旺财(来自父类的 __init__)
print(d.sound())  # 汪汪(子类重写后的)
  • 子类自动继承父类的所有方法和属性。
  • 重写(override):子类定义同名方法覆盖父类的。
  • super() 拿到父类"代理",super().__init__(name) 是"让父类先初始化该初始化的"。子类 __init__ 若需要父类属性,必须调它,否则 self.name 不存在——这是最高频考点。

多态(polymorphism):不同子类重写同一方法,调用方只调 sound(),各叫各的:

for a in [Dog("旺财"), Cat("咪咪")]:
    print(a.sound())    # 旺财 → 汪汪;咪咪 → 喵

类型判断isinstance(obj, Animal)obj 是不是 Animal 的实例,含子类实例);issubclass(Dog, Animal)Dog 是不是 Animal 的子类)。

⚠️ 常见错误

  1. 子类 __init__ 忘了 super().__init__():父类属性(如 self.name)没建立,一访问就 AttributeError
  2. 重写后想用父类方法:要写 super().方法名(),直接写 方法名() 会找不存在的全局函数。
  3. 多继承的坑class C(A, B) 的查找顺序(MRO)是 C → A → B → object。做题先画继承链,沿链从子类往父类找。
  4. isinstance 方向搞反Dog(...)Animal 类型 → TrueAnimal(...) 不是 Dog 类型 → False

7.8 类方法 @classmethod 与静态方法 @staticmethod

实例方法操作"一个对象"(有 self)。有些功能跟"类"有关、跟具体对象无关,用两种装饰器:

class Dog:
    species = "犬科"

    @classmethod                        # 类方法:操作类 / 造对象
    def create(cls, name):              # 第一个参数是类 cls(约定名)
        return cls(name)                # 相当于 Dog(name)

    @staticmethod                       # 静态方法:与类无关的工具函数
    def info():                         # 没有 self 也没有 cls
        return "狗是人类的朋友"

d = Dog.create("旺财")   # 用类方法造对象(工厂模式)
print(d.name)            # 旺财
print(Dog.info())        # 狗是人类的朋友
  • @classmethod:第一参数 cls(类本身)。典型用途是工厂方法(如"用直径创建圆")和读写类变量。
  • @staticmethod:不接收 self/cls,是"放在类里的普通函数",放逻辑上属于该类的工具函数(如 Calculator.add)。
  • 两者都不强制通过实例调用;习惯上用类名调用

⚠️ 常见错误

  1. 类方法忘了写 clsdef create(name): → 调用 Dog.create("旺财") 报错(跟 self 忘写同理)。
  2. 静态方法里写了 self@staticmethod 没有 self,想用类变量就用 Dog.species
  3. 分不清两者:需要类变量 / 要返回 cls(...)@classmethod;完全不碰类 → @staticmethod

7.9 组合:一个类"装"着另一个类

继承是 IS-A(狗是动物),组合是 HAS-A(车有引擎)。组合 = 把一个类的对象作为另一个类的属性

class Engine:
    def __init__(self, power):
        self.power = power

    def describe(self):
        return f"{self.power} 马力"

class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine          # 组合:Car 持有 Engine 对象

    def describe(self):
        return f"{self.brand},搭载 {self.engine.describe()}"

engine = Engine(200)
car = Car("奥迪", engine)
print(car.describe())   # 奥迪,搭载 200 马力

组合的现实意义:引擎可以独立存在、可以换(换引擎不用重新设计车),比继承更灵活。设计原则常说"多用组合,少用继承"。

⚠️ 常见错误

  1. 忘传组合对象Car("奥迪") 少了 engineTypeError: missing 1 required positional argument
  2. 方法调用链写错self.engine.describe() 别漏中间的 .engine
  3. 组合继承硬混:B 只是"有一个" A 就别让 B 继承 A。

7.10 综合案例:把本讲串起来的 TodoList

一个"待办清单"系统,几乎用到本讲所有知识点——组合(清单装任务)、魔法方法(__len__/__str__)、实例状态、方法:

class Task:
    """单个任务"""
    def __init__(self, title):
        self.title = title
        self.done = False          # 实例属性:完成状态

    def mark_done(self):
        self.done = True

    def __str__(self):             # 完成打勾,未完成打空框
        return ("✓ " if self.done else "□ ") + self.title

class TodoList:
    """待办清单:组合了一组 Task"""
    def __init__(self):
        self.tasks = []            # 组合:清单里装一堆任务

    def add(self, title):
        self.tasks.append(Task(title))

    def complete(self, index):
        self.tasks[index].mark_done()

    def pending_count(self):
        return sum(1 for t in self.tasks if not t.done)

    def __len__(self):
        return len(self.tasks)

    def __str__(self):
        return "\n".join(str(t) for t in self.tasks)

todo = TodoList()
todo.add("写作业")
todo.add("背单词")
todo.complete(0)
print(len(todo))             # 2
print(todo.pending_count())  # 1
print(todo)
# 输出:
# ✓ 写作业
# □ 背单词

⚠️ 常见错误

  1. __str__ 里用 print 而不是 return__str__ 必须返回字符串,在里头 print 会导致 print(todo) 输出两遍。
  2. __len__ 忘了 return
  3. 组合类里忘了 self.tasks:直接写 tasks.append(...)NameError

7.11 常见题型与考试套路

  1. 写出运行结果(白给分题):给出类定义 + 调用,写出 print 结果。顺序:先看 __str__/__repr__(决定 print(对象) 显示什么)→ 再盯 self.xxx 赋值(画"对象属性表")→ 最后看类变量有没有被改(只有 类名.xxx = ... 才改到类变量)。
  2. 设计类:给需求(学生/账户/图书/形状)让你补全。按"属性 → 方法 → 魔法方法"顺序写。
  3. 继承调用顺序:问 obj.method() 实际调谁。沿继承链从子类往父类找,子类先命中先用子类的。

💡 记忆口诀

  • 建类三步走class 大写开头、__init__ 里建属性、方法第一参数写 self
  • self 三问:声明要写、访问属性要写、调用兄弟方法要写——方法里啥都要 self.
  • 类变量改名用类名Student.total += 1,别用 self.total
  • 魔法方法记触发时机:打印找 __str__,比较找 __eq__,求长找 __len__,相加找 __add__
  • 继承两个必答:子类要父类属性先 super().__init__();要父类方法用 super().方法名()

📌 双语术语表(本讲)

中文 English
类 / 对象 class / object(instance)
实例 instance
属性 attribute
方法 method
构造方法 constructor(__init__
继承 inheritance
重写 override
多态 polymorphism
封装 encapsulation
魔法方法 magic method / dunder
类变量 class variable
实例变量 instance variable
属性装饰器 property
类方法 class method
静态方法 static method
组合 composition
运算符重载 operator overloading

⭐ 本讲考点清单

  1. __init__ + self(忘写 self / return 值)
  2. 类变量 vs 实例变量(类名.xxx 才改类变量、可变默认值坑)
  3. 继承 + super().__init__() + 方法重写
  4. 多态:同一方法不同子类不同行为
  5. 魔法方法 __str__ / __repr__ / __eq__ / __len__ / __add__
  6. 封装:单下划线 _x + getter/setter + @property
  7. @classmethod(工厂方法)/ @staticmethod(工具函数)
  8. 组合(HAS-A):一个类持有另一个类的对象
  9. isinstance / issubclass
  10. 综合设计:属性 → 方法 → 魔法方法