前四周你写的程序,都是同一个套路:把数据传给函数,函数处理完返回结果。函数当然是好东西,但写多了你会发现问题——数据和操作它的函数是两拨人,各管各的。要描述一个学生,你得用几个变量存姓名、成绩,再写个专门的函数去打印它们,稍不留神就对不上号。
今天换一种组织代码的方式:类和对象。学完你会发现,写代码的思路从"一行行命令"变成了"造东西、用东西"。先记住一句话:类是图纸,对象是按图纸造出来的实物。后面的封装、继承,还有周五的图书管理项目,全都站在这句话上面。
类:一份能反复开模的图纸
想象你去家具店订做衣柜。设计师先画一张图纸,长、宽、高、几个抽屉都标在上面;工厂照着图纸造出一个实物衣柜,摆进你家。
类就是那张图纸:
- 类(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 是小白,互不干扰。这就是"图纸造实物":图纸只有一份,实物要多少有多少。
两个细节先说清楚:
- 类名用大驼峰命名法(每个单词首字母大写,
Dog、StudentInfo都这么写),变量和函数才用小写加下划线。这是 Python 社区的约定,照做,以后不管是老师检查还是同事 code review,都挑不出毛病。 class Dog:后面要有冒号,类里所有内容都要缩进,和之前写if、def的规则一模一样。
还有一件事特别容易懵:只写 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 变成你自己的狗
- 新建一个 Python 文件,把上面的 Dog 类完整抄一遍并运行,确认能看到两只狗叫。
- 把两只狗的名字改成你自己的宠物名(没有宠物就起两个喜欢的名字),再运行一遍。
- 给 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() 方法,打印「XX:XX 分」
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,会发生什么?自己敲一遍试试,明天上课可以聊聊你的答案。