第21章-类与对象


前四周你写的程序,都是同一个套路:把数据传给函数,函数处理完返回结果。函数当然是好东西,但写多了你会发现问题——数据和操作它的函数是两拨人,各管各的。要描述一个学生,你得用几个变量存姓名、成绩,再写个专门的函数去打印它们,稍不留神就对不上号。

今天换一种组织代码的方式:类和对象。学完你会发现,写代码的思路从"一行行命令"变成了"造东西、用东西"。先记住一句话:类是图纸,对象是按图纸造出来的实物。后面的封装、继承,还有周五的图书管理项目,全都站在这句话上面。

类:一份能反复开模的图纸

想象你去家具店订做衣柜。设计师先画一张图纸,长、宽、高、几个抽屉都标在上面;工厂照着图纸造出一个实物衣柜,摆进你家。

类就是那张图纸:

  • 类(class) 只是描述,本身不能直接用
  • 对象(object) 是按图纸造出来的实物,有自己的数据
  • 实例化(instantiate) 就是"拿着图纸去造对象"这个动作

程序里的类,一张图纸上画两类东西:

图纸上的内容 程序里的说法 例子(Dog 类)
衣柜长什么样、有哪些部件 属性(attribute):数据 名字 name、年龄 age
衣柜能怎么用(开门、挂衣) 方法(method):行为 叫 bark()、跑 run()

同一个类能造出无数个对象,结构一模一样,但各自的属性值互不相同——这是类和对象关系的核心,今天后面的代码你会反复看到这一点。

先看代码。定义一个"狗"的类:

# 定义一个"狗"的类(图纸)
class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(f"{self.name}:汪汪!")

# 用图纸造对象(实例化)
d1 = Dog("旺财")
d2 = Dog("小白")

print(d1.name)   # 旺财
print(d2.name)   # 小白
d1.bark()        # 旺财:汪汪!
d2.bark()        # 小白:汪汪!

输出长这样:

旺财
小白
旺财:汪汪!
小白:汪汪!

同一份代码,d1 是旺财、d2 是小白,互不干扰。这就是"图纸造实物":图纸只有一份,实物要多少有多少。

两个细节先说清楚:

  • 类名用大驼峰命名法(每个单词首字母大写,DogStudentInfo 都这么写),变量和函数才用小写加下划线。这是 Python 社区的约定,照做,以后不管是老师检查还是同事 code review,都挑不出毛病。
  • class Dog: 后面要有冒号,类里所有内容都要缩进,和之前写 ifdef 的规则一模一样。

还有一件事特别容易懵:只写 class Dog: 不创建对象,程序不会报错,但也什么都不发生——图纸画好了没开模,等于白画。定义类和创建对象是两件事,缺一不可。

造对象时自动执行的 init

刚才的代码里有个怪名字:__init__(读作"dunder init",也有人叫"双下划线 init")。它叫构造方法,作用相当于工厂的生产线入口。每当你写 Dog("旺财") 想造一只狗,Python 会自动调用 __init__,把传进来的 "旺财" 交给它,由它把名字装到这只狗身上。

前后各两个下划线是 Python 的"特殊方法"记号,意思是"Python 会在特定时机自动调用我"。所以你不需要手动调 d1.__init__(),写了反而画蛇添足。

那为什么需要它?因为造出来的对象不应该是个空壳——旺财一出生就该有名字,手机一出厂就该有颜色。没有 __init__,你只能造完再手动一条条塞属性,容易忘,也容易乱。看个能证明"自动执行"的例子:

class Student:
    def __init__(self, name, score):
        print("正在创建学生对象……")   # 验证:创建对象时会自动执行
        self.name = name
        self.score = score

s1 = Student("小明", 95)   # 创建对象 → __init__ 自动执行
s2 = Student("小红", 88)

print(s1.name)    # 小明
print(s2.score)   # 88

输出长这样:

正在创建学生对象……
正在创建学生对象……
小明
88

"正在创建学生对象"打印了两次——每创建一次对象,__init__ 就自动跑一遍。这就是"自动执行"的直接证据。

__init__ 有三个坑:

  • 两边是各两个下划线__init__,不是 _init_)。少写一个,它就变成普通方法,不会被自动调用,属性装不上去。
  • __init__ 里写多少个参数,创建对象时就要传多少个(self 除外,马上讲)。__init__(self, name, score) 对应 Student("小明", 95),个数和顺序都要对上。
  • 忘了定义 __init__ 不报错,但对象身上什么都没有。想让对象一出生就有属性,就必须写它。

self:这个对象自己

两个对象用的是同一份方法代码,Python 怎么知道 bark() 是在给旺财叫还是给小白叫?靠的就是 self

self 是"这个对象自己"的代号,你可以把它想成快递单上的收件人:快递站(类)知道怎么派件(方法代码),但每次派件都得贴上不同的收件人(对象)。d1.bark() 时 self 是 d1,d2.bark() 时 self 是 d2。

class Dog:
    def __init__(self, name):
        self.name = name          # 把参数 name 存进"自己"的属性

    def introduce(self):
        # 这里用 self.name,因为我不知道"将来"是哪个对象来调用我
        print(f"我叫{self.name}")

d1 = Dog("旺财")
d2 = Dog("小白")
d1.introduce()   # 我叫旺财   (此时 self = d1)
d2.introduce()   # 我叫小白   (此时 self = d2)

输出长这样:

我叫旺财
我叫小白

方法里写 self.name,意思是"我这个对象自己的 name 属性"。有几点要记牢:

  • self 不是 Python 关键字,但约定必须叫 self。你换成 this 也能跑,只是会被同事当成异类,别这么干。
  • 定义方法时 self 必须写在第一个参数位置;调用时却不用传——Python 会自动把"调用它的那个对象"塞进去。
  • 方法里如果不写 self. 而直接写 name,Python 会去找普通变量,找不到就报 NameError。属性一律写 self.属性名
  • 定义方法时漏写 self,调用会报 TypeError: bark() takes 1 positional argument but 2 were given。意思是 Python 把对象自动传进去了,你的方法却没地方接。这是新手最经典的报错之一,解法一句话:方法的第一个参数永远是 self
  • 在类外面访问属性(比如 print(d1.name))时不要写 self,只有类里面才用。

方法:装在类里的函数

方法(method) 就是定义在类里面的函数。和普通函数比,差别在这儿:

对比项 普通函数 方法
定义位置 类外面,独立 类里面,缩进
第一个参数 随意 必须是 self
调用方式 函数名(参数) 对象.方法名(参数)
能访问的数据 只有传进来的参数 除了参数,还有 self 上的所有属性

怎么判断该写函数还是方法?一句话:纯干活、不碰任何人的数据 → 函数;要动某个对象自己的属性 → 方法。比如"计算两个数之和"是函数;"让我的余额增加"是方法,因为它要改对象自己的余额。

方法也能带参数,除了 self 之外照常加:

class Cat:
    def __init__(self, name):
        self.name = name

    def meow(self, times):          # 方法可以带参数(除了 self)
        for _ in range(times):
            print(f"{self.name}:喵~")

c = Cat("咪咪")
c.meow(3)       # 用"对象.方法(参数)"调用

输出长这样:

咪咪:喵~
咪咪:喵~
咪咪:喵~

调用方法必须带括号:c.meow() 是"执行",不写括号 c.meow 只是"引用方法本身",什么都不会发生。参数个数也要对:定义 meow(self, times),调用只传一个数,因为 self 是自动传的。

方法之间还能互相调用:在一个方法里写 self.方法B(),就能调同类里的另一个方法。别写成裸方法名,Python 找不到。

属性:长在对象身上的数据

属性(attribute) 就是挂在对象身上的数据:名字、年龄、成绩……在 __init__ 里用 self.xxx = ... 装上去,之后在方法里、在类外面都能读。

属性在哪能访问,有个清晰的边界:

位置 能不能访问属性 写法
类内部的方法里 self.name
类外部(代码主体) 对象.name
类的定义里、方法之外 不能直接写 只能在方法里通过 self 操作

属性不强制全放在 __init__ 里,但建议都放——打开类的开头,一眼就能看到这个对象身上有哪些数据,好维护。

方法里改自己的属性,和读一样简单:

class Student:
    def __init__(self, name, score):
        self.name = name        # 属性:名字
        self.score = score      # 属性:成绩

    def show(self):
        print(f"{self.name}{self.score} 分")

    def upgrade(self, add_score):     # 方法内修改自己的属性
        self.score += add_score

s = Student("小明", 90)
s.show()          # 小明:90 分
s.upgrade(5)      # 方法里改了 self.score
s.show()          # 小明:95 分

输出长这样:

小明:90 分
小明:95 分

两个对象可以有同名的属性、不同的值(d1.name 是旺财,d2.name 是小白),互不影响——属性是"长在对象身上的",这正是类最核心的意义。

两个提醒:

  • 在类外面直接改属性(s.score = 60)也是合法的,但明天你会学到,有些属性不希望被外面随便改,那就是"封装"要解决的问题,今天先记住有这回事。
  • 属性名拼错(比如 self.nam)不会当场报错,等调用时才给你一个 AttributeError。这类错误 Python 没法提前提醒,拼写要仔细。

随堂练习

练习一:把 Dog 变成你自己的狗

  1. 新建一个 Python 文件,把上面的 Dog 类完整抄一遍并运行,确认能看到两只狗叫。
  2. 把两只狗的名字改成你自己的宠物名(没有宠物就起两个喜欢的名字),再运行一遍。
  3. 给 Dog 类加一个 age 属性(构造时传入),再写一个方法 info() 打印「我叫 XX,今年 X 岁」。

练习二:Student 类(重点练习,先自己写再看提示)

要求:建一个 Student 类,有姓名和成绩两个属性,有一个 show() 方法打印「XX:XX 分」;再创建 2 个学生对象并调用。

提示:

  • class Student:__init__(self, name, score) 里装两个属性
  • show 方法里用 print(f"{self.name}:{self.score} 分")
  • 最后两行创建对象并调用:s1 = Student("小明", 95)s1.show()

进阶题 ★:upgrade 加分方法(今天最重要的一道题)

给 Student 类加一个 upgrade(score) 方法,调用后能把成绩提高指定分数。加分后调用 show() 能看到新成绩。

提示:

  • 方法里用 self.score += score 修改自己的属性(参考上面的代码)
  • 想挑战自己:加分前判断 score 大于 0 才加,负数直接打印「分数必须大于 0」——这就是明天"封装"思想的雏形

让 AI 帮你写类

想让 AI 写或改一个类,又怕它写得看不懂?把这段复制给它,要求它边写边讲:

我是 Python 零基础初学者。请帮我写一个类,要求:
1. 类名 Student,属性有姓名和成绩
2. 有一个 show() 方法,打印「XXXX 分」
3. 有一个 upgrade(score) 方法,能给成绩加分
请用最简单的写法,每行代码都加中文注释,写完后再用大白话逐行解释一遍这段代码在做什么。

报错速查

报错信息 什么意思 怎么办
TypeError: __init__() missing 1 required positional argument: 'score' 创建对象时少传了参数 数一数 对象(参数)__init__(self, ...) 的参数个数、顺序,要一一对应
AttributeError: 'Dog' object has no attribute 'name' 调用了对象上不存在的属性/方法 检查拼写;确认 __init__ 里写了 self.name = ...(尤其检查是不是少写了一个下划线)
NameError: name 'name' is not defined 在方法里直接写了 name,没加 self. 方法里访问属性一律写 self.name
TypeError: bark() takes 1 positional argument but 2 were given 定义方法时忘了写 self 方法的第一个参数永远是 self
IndentationError: expected an indented block class 或 def 后面没有缩进内容 类/方法内部所有代码统一缩进(通常 4 个空格)

走之前留个问题

今天核心就三个词:类、对象、self。类是图纸,对象是实物,__init__ 负责造对象时把属性装上,self 负责分清"现在是哪个对象"。这三样是面向对象的地基,明天要在这个地基上盖房子。

明天学封装与继承:把数据"锁"在对象里不让外面乱改,以及让新类"白捡"旧类的全部能力。今天把类的写法敲熟,明天就能把注意力放在"为什么这么设计"上。

临走留个问题:如果 Dog("旺财") 创建对象时不传名字,class Dog__init__ 又只接收一个 name,会发生什么?自己敲一遍试试,明天上课可以聊聊你的答案。