1. 从“狗”说起:为什么计算机需要“类”和“对象”
我先问你一个问题:如果不用类和对象,你怎么在代码里描述一只狗?
你可能想,这还不简单,定义几个变量不就行了:
python复制dog_name = "旺财"
dog_age = 3
dog_color = "棕色"
dog_breed = "中华田园犬"
没问题,你确实“描述”了一只狗。但是如果你的程序里有十只狗呢?难道要写十遍这个变量组合?如果张三、李四、王五的程序都要狗的“名、年龄、颜色、品种”,难道每个人都重新写一遍?
更麻烦的是,假设你的程序里有一百只狗,每只都要执行“叫”这个动作——难道你要写一百个函数,分别叫 dog1_bark()、dog2_bark()、dog3_bark()?这不叫编程,这叫折磨自己。
类和对象存在的根本原因就是:代码里出现了大量“结构相同、数据不同”的东西。你需要一个统一的“模板”来定义这类东西长什么样、会做什么事,然后用这个模板批量制造出每一个具体的个体。
这个“模板”就是类,也就是标题里的 Class;每一个“具体的个体”就是对象。
拿生活类比一下:类是饼干模具,对象是用这个模具压出来的一块块饼干。同一个模具可以做出无穷无尽的饼干,它们形状规则一致,但每一块上面可能撒了不同的芝麻、烤的程度也略有差异。模具本身吃不了,但饼干能吃;同样,类本身不存储具体数据,对象才存储具体数据,类只负责规定“数据长什么样、方法怎么实现”。
再换一个角度,你可以把类理解成房产开发商手里的户型图,对象就是用这个户型图盖出来的每一套房。户型图规定了“这个户型有几个卧室、几个卫生间、门在哪、窗在哪”,但它不是房子;每一套房子才是对象,同一张户型图可以盖出一模一样但位于不同楼层、不同朝向的很多套房。
这两种类比帮我在过去带过的每一批新手学生脑子里刻下了两个核心结论:
- 类是抽象的规定,对象是具体的实例。
- 类是一次定义,到处创建;对象是真正参与程序运行的数据实体。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写第一个类:属性、方法和构造函数的完整拆解
2.1 从“模具”到“饼干”的最小代码
不管你是学 Python、Java、C++ 还是 JavaScript,类的基本骨架几乎长得差不多。我下面用 Python 写一个最简版本,因为它语法最接近人话,适合先把概念串通:
python复制class Dog:
# 构造函数:创建对象时自动调用,负责初始化具体数据
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color
self.energy = 100
# 普通方法:描述狗会做什么事
def bark(self):
print(f"{self.name} 汪汪汪!")
def run(self, distance):
self.energy -= distance * 0.1
if self.energy < 0:
self.energy = 0
print(f"{self.name} 跑了 {distance} 公里,剩余精力 {self.energy}")
创建对象只需要一行:
python复制wangcai = Dog("旺财", 3, "棕色")
wangcai.bark()
wangcai.run(5)
运行结果:
code复制旺财 汪汪汪!
旺财 跑了 5 公里,剩余精力 50
这一步,你实际上已经完成了类与对象的全部核心操作:定义类 → 调用构造函数创建对象 → 通过对象调方法。后面所有的概念,什么继承、封装、多态,都是在这个骨架上加料。
2.2 属性:对象身上挂着的数据
我们把上面代码里的 name、age、color、energy 称为属性。属性可以理解为对象的“状态”或“特征”,它回答的是“这个对象现在是什么样”的问题。
属性分两种:
- 实例属性:上面代码里用
self.xxx给每个对象单独绑定的数据。每只狗的name不一样,互不干扰。 - 类属性:定义在类内部、方法外部的变量,所有对象共享同一份。比如你想统计整个狗类一共创建了多少只狗对象:
python复制class Dog:
total_count = 0 # 类属性,所有实例共享
def __init__(self, name, age):
self.name = name # 实例属性,每个对象独立
self.age = age
Dog.total_count += 1
区别在哪里?Dog.total_count 只要修改一次,所有对象读到的值都变了;而 self.name 改只影响当前对象。这个区分在实战中特别容易踩坑,我在第 4 部分会专门讲一个“类属性被误当实例属性”的经典 Bug。
2.3 方法:对象会做的动作
方法就是定义在类里的函数,它描述对象“能做什么”。bark()、run() 都是方法。
这里有个新手最容易困惑的点:为什么 Python 里每个方法第一个参数都是 self?
self 在 Python 中代表“当前对象本身”。当你调用 wangcai.run(5) 时,Python 背后实际执行的是 Dog.run(wangcai, 5)——也就是说,你写的 self 就是调用这个方法的那个对象。它保证了方法操作的是“这只狗的精力”,而不是“别的狗的精力”,更不是“一个抽象的狗的精力”。
Java、C++ 里看不到这个参数,是因为编译器偷偷替你传了。JavaScript 里用 this 来代替 self,概念一模一样。所以别看语言千差万别,底层逻辑全是同一个:方法必须知道“我是谁”,才能操作“我的数据”。
2.4 构造函数:对象的初始化器
Python 里构造函数写成 __init__,Java 里叫 new Dog(...) 后面的 Dog(...) 方法,C++ 里与类同名。名字不同,作用完全一致:
创建对象时,自动完成“给对象填初始数据”这件事。
为什么不能省掉构造函数?
假设没有构造函数,你得这样创建一只狗:
python复制wangcai = Dog()
wangcai.name = "旺财"
wangcai.age = 3
wangcai.color = "棕色"
代码量变大还不是最致命的,致命的是你容易漏掉某个属性。万一你哪天忘了给 color 赋值,后续代码读 wangcai.color 时直接报错或者得到一个 undefined。构造函数把“创建一个合法对象必须具备的数据”给强制规定死了:想让对象诞生,就先把该给的数据给齐。
这有点像办身份证,你不能只给个姓名就领证,出生年月、住址都得现场录进去,不然系统里存的就是个半成品。
3. 类和对象在主流语言中的写法对比
光看 Python 不够,因为很多读者实际工作用的是 Java、C++。把这几种主流语言写一遍,你就会发现“换语言不换思想”这句话是什么意思。
3.1 Java 版
java复制public class Dog {
// 属性
private String name;
private int age;
private String color;
// 构造函数
public Dog(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
// 方法
public void bark() {
System.out.println(name + " 汪汪汪!");
}
// Getter / Setter 是 Java 的常见约定
public String getName() {
return name;
}
}
创建对象:
java复制Dog wangcai = new Dog("旺财", 3, "棕色");
wangcai.bark();
Java 重点看三点:
this与 Pythonself完全对应,this.name = name就是把外部传入的name值赋给当前对象的name。new关键字专门用来创建对象,它背后的动作是:分配内存 → 调用构造函数 → 返回对象引用。private把属性封装起来,不允许外部直接乱改,必须通过方法来访问。这是封装思想的语言级体现。
3.2 C++ 版
cpp复制#include <iostream>
#include <string>
class Dog {
private:
std::string name;
int age;
public:
Dog(std::string name, int age) : name(name), age(age) {}
void bark() {
std::cout << name << " 汪汪汪!" << std::endl;
}
};
int main() {
Dog wangcai("旺财", 3);
wangcai.bark();
return 0;
}
C++ 的构造函数写法 : name(name), age(age) 叫初始化列表,效率比在函数体里逐个赋值略高——因为它直接在内存中初始化属性,而不是先创建“空属性”再赋值。初学者不用过度纠结这个,理解成“在进入构造体之前就把数据塞进去了”即可。
3.3 JavaScript 版
javascript复制class Dog {
constructor(name, age, color) {
this.name = name;
this.age = age;
this.color = color;
this.energy = 100;
}
bark() {
console.log(`${this.name} 汪汪汪!`);
}
}
const wangcai = new Dog("旺财", 3, "棕色");
wangcai.bark();
constructor 就是构造函数,this 就是当前对象。JavaScript 的类本质上也是构造函数的语法糖,底层还是通过“原型链”实现继承逻辑。对初学者来说,先按“类和对象”的思想理解完全没有问题。
我把这几种语言的对应关系整理成一个对照表,方便你收藏:
| 概念 | Python | Java | C++ | JavaScript |
|---|---|---|---|---|
| 定义类 | class Dog: |
class Dog { } |
class Dog { }; |
class Dog { } |
| 构造函数 | __init__(self) |
Dog(...) |
Dog(...) |
constructor(...) |
| 当前对象 | self |
this |
this |
this |
| 创建对象 | Dog(...) |
new Dog(...) |
new Dog(...) |
new Dog(...) |
| 访问属性 | obj.name |
obj.name |
obj.name |
obj.name |
4. 继承、封装、多态:类与对象的三大核心特性
4.1 封装:把“该藏起来的”藏起来
封装的意思非常朴素:对象对外只暴露必要的接口,内部细节不让人随便乱动。
比如你的手机,外部只有电源键、音量键、充电口,但内部有 CPU、主板、电池——你不会没事儿拿螺丝刀拆开它去调电压,你只会通过按键和屏幕来使用它。代码里的封装就是这个道理:
python复制class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # 双下划线,外部无法直接访问
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
外部代码不能直接写 account.__balance = 9999999,只能通过 deposit() 存钱、通过 get_balance() 查看余额。这样你就把“余额不允许被随意修改”的规则嵌进了代码本身。
封装的价值一句话总结:你改内部实现,只要对外接口不变,所有外部代码都不用动。 比如你把余额从 int 类型改成 Decimal 类型,别人调用 get_balance() 的地方一行都不用改。
4.2 继承:在已有基础上扩展,而不是从零重写
继承解决的是“两个类有很多共同点,但不完全相同”的问题。你可以先写一个通用类,再让它下面的子类自动拥有父类的属性和方法,然后加自己的特色。
python复制class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 正在吃饭")
def make_sound(self):
print("某种动物的叫声")
python复制class Cat(Animal):
def make_sound(self):
print(f"{self.name} 喵喵喵")
def climb_tree(self):
print(f"{self.name} 爬上树了")
创建猫对象:
python复制mimi = Cat("咪咪")
mimi.eat() # 从 Animal 继承的方法
mimi.make_sound() # Cat 覆写(Override)了父类的方法
mimi.climb_tree() # Cat 独有的方法
继承最核心的副产品是代码复用。Cat 不需要重新写 eat(),直接白拿 Animal 的。真实项目里,你可以有一个 BaseModel 基类统一定义数据库连接、日志记录、错误处理,然后所有业务模型类继承它,每个模型只需要写自己的业务字段和方法。
4.3 多态:同一句代码,不同对象给出不同表现
多态有点绕,我先给大白话定义:同一个接口(方法名),在不同类里可以有不同的实现,最终表现由“调用时的真实对象类型”决定。
python复制def let_it_speak(animal):
animal.make_sound()
cat = Cat("咪咪")
another_animal = Animal("宠物")
let_it_speak(cat) # 输出:咪咪 喵喵喵
let_it_speak(another_animal) # 输出:宠物 正在发出声音?
注意,let_it_speak 函数完全不知道传入的是 Cat 还是 Animal,它只负责统一调用 make_sound()。程序运行时,Python 自动根据对象类型找到正确的 make_sound()。这就是“一个接口,多种实现”。
Java 里最常见的多态用法是面向接口编程:
java复制List<String> list = new ArrayList<>();
左边用接口 List 声明,右边给了具体实现 ArrayList。后面如果想换成 LinkedList,只改右边一处即可,整段代码其他地方不用动。这就是多态在工程上最大的威力——降低代码之间的耦合度。
5. 实战案例:用类实现一个“学生选课系统”的雏形
把前面的知识点串起来,我们写一个稍微综合一点的例子。假设你在为学校写一个简易选课系统,需要管理学生、课程,以及学生选课的行为。
python复制class Course:
def __init__(self, course_id, name, max_students):
self.course_id = course_id
self.name = name
self.max_students = max_students
self.students = []
def add_student(self, student):
if len(self.students) >= self.max_students:
print(f"课程 {self.name} 已满,无法加入 {student.name}")
return False
if student in self.students:
print(f"{student.name} 已经选过这门课了")
return False
self.students.append(student)
print(f"{student.name} 成功选课:{self.name}")
return True
class Student:
def __init__(self, student_id, name):
self.student_id = student_id
self.name = name
self.courses = []
def enroll(self, course):
if course.add_student(self):
self.courses.append(course)
测试一下:
python复制python_course = Course("CS101", "Python 编程入门", 2)
alice = Student("S001", "爱丽丝")
bob = Student("S002", "鲍勃")
carol = Student("S003", "卡罗尔")
alice.enroll(python_course)
bob.enroll(python_course)
carol.enroll(python_course)
输出结果:
code复制爱丽丝 成功选课:Python 编程入门
鲍勃 成功选课:Python 编程入门
课程 Python 编程入门 已满,无法加入 卡罗尔
这个案例虽然只有两个类,但已经体现了完整的面向对象思维:
- 谁来负责什么:
Course负责管理课程容量和名单,Student负责管理自己的选课列表。你不需要在主流程里写一堆散乱的函数,所有操作都在对象内部闭环。 - 双向关联怎么做:选课成功时,既要往课程的
students列表加人,也要往学生的courses列表加课。传统面向过程写法很容易漏掉其中一边,而对象方法内的逻辑可以集中管理。 - 数据校验放哪:课程容量检查放
Course.add_student(),因为“能不能进”这件事,课程自己最清楚。
我在实际给新人讲这段代码的时候,发现把它背下来不是重点,重点是理解“哪个类该管哪些数据、哪些行为”这个决策过程。每个类都应该是现实世界某个真实概念的映射,而不是一堆函数的随便归类。
6. 新手最容易踩的 5 个坑和我的排查经验
6.1 对象判空 / 未初始化就使用
最常见的第一坑:创建对象后忘记调用构造函数,然后直接访问属性。
python复制wangcai = Dog # 这里少了括号
print(wangcai.name) # TypeError: 'type' object is not subscriptable
Java 里则是:
java复制Dog wangcai; // 只声明,没有 new
wangcai.bark(); // NullPointerException
解决办法是养成习惯:任何对象使用前,确认它已经通过构造函数完成初始化。 收到外部传入的对象时,先判空再使用,这在 Java 里是铁律。Python 里虽然不抛空指针,但访问不存在的属性会抛 AttributeError,排查思路是一样的:打印 dir(obj) 查看真实可用属性。
6.2 类属性当作实例属性修改
这是个特别经典的坑,很多工作两三年的开发者都会栽:
python复制class Dog:
tricks = [] # 类属性
fido = Dog()
rex = Dog()
fido.tricks.append("翻滚")
print(rex.tricks) # ['翻滚'] —— 这只狗也会翻滚了?
因为 tricks 是类属性,所有实例共享同一个列表。你往 fido 的列表里加元素,rex 看到的自然也是同一个列表。
如果本意是每只狗有自己独立的技巧列表,应该在构造函数里创建实例属性:
python复制class Dog:
def __init__(self):
self.tricks = [] # 每只狗独立
排查技巧:当发现一个对象修改数据后“莫名其妙影响另一个对象”,大概率是类属性和实例属性用混了。用 obj.__dict__ 能看到实例自己的属性,就能快速识别是不是被类属性坑了。
6.3 可变对象作为默认参数
这个坑和上一个有亲缘关系:
python复制def add_student(student, course_list=[]):
course_list.append(student)
return course_list
Python 的默认参数在函数定义时只创建一次,后续调用都用这同一个列表。第一次调用加入 Alice,第二次调用时这个列表里已经有 Alice 了。正确写法是:
python复制def add_student(student, course_list=None):
if course_list is None:
course_list = []
course_list.append(student)
return course_list
6.4 循环里闭包捕获对象的错误引用
这在 JavaScript 中特别常见:
javascript复制var students = [new Student("Alice"), new Student("Bob")];
for (var i = 0; i < students.length; i++) {
setTimeout(() => {
students[i].printName(); // i 已经变成 2
}, 100);
}
var 的作用域是函数级,循环结束后 i 已经越界了。把 var 改成 let 可以解决。核心教训是:异步代码里捕获循环变量时,务必确认它捕获的是“循环内部当前的副本”还是“循环最后的值”。
6.5 面向对象写成“伪面向对象”
这个坑最隐蔽。很多人写了类,但整个程序除了封装函数外没有一点对象状态,本质还是面向过程的函数集合。鉴别方法是问自己一个问题:这个类的方法里,是否用到了它的 self / this 所绑定的数据? 如果每个方法都只是纯函数、不读不写对象属性,那真的没必要硬套一个类,直接写函数更实在。
我在评审代码时,经常和新人说“别为了用类而用类”。类的价值在于管理“数据和操作这条数据的方法”,如果连数据都没独立出来,类就只是个昂贵的外壳。
7. 调试技巧与对象信息观察
写类与对象时,调试方式比写普通函数多几个工具上的讲究。
Python 里最实用的三个工具:
print(obj.__dict__):直接看到对象自己的属性字典,快速定位属性名拼写错误。print(isinstance(obj, Dog)):确认对象类型。继承体系复杂时特别有用,看一个对象到底是哪个类的实例。- 自定义
__repr__方法:
python复制def __repr__(self):
return f"Dog(name={self.name}, age={self.age})"
有了它,直接输出对象时不再是 <__main__.Dog object at 0x...>,而是一眼能看懂的信息。这在打日志、排查数据时省太多时间。
Java 里对应的是快捷键生成 toString() 方法。我见过不少新人排查半天,最后发现只是没打印出真正想看的属性值——调试的第一步永远是确认你看到的对象数据是完整的。
8. 根据我的带教经验:掌握类与对象的正确姿势
讲了这么多,最后根据我自己的学习过程和带新人的经验,说几点大家普遍觉得有效的学习路径。
第一步:别急着背概念,先写“一个类 + 一个对象”的最小程序。 用 Python 定义 Dog、Cat、Car,随便什么都行,包含一个构造函数、两三个属性、一两个方法,创建三五个对象试试。这个阶段唯一目标是让“类”和“对象”这两个词落实到能在编译器里跑起来。
第二步:练习“把现实场景翻译成类”。 比如设计一个图书馆系统:书是一个类,读者是一个类,借阅记录是一个类。不要求写得多好,先尝试回答“有哪些实体、各自有什么属性、各自做什么行为”。这个过程强迫你训练真正的面向对象建模思维,而不是背代码。
第三步:读优秀的开源代码,里面全是“老手设计的类结构”。 比如看 Django 的模型类、Java 的集合框架源码。你不一定能全看懂,但观察大项目里类与类的组织方式,比任何教程都来得有效。
我反复和新学员讲:类与对象本身并不难,难的是“什么时候该用类”“类与类之间怎么分工”“哪些数据该放哪个类里”。这三个问题的答案没有绝对标准,但随着你写的项目越来越多,你会形成自己的判断力。
最后分享一个训练心法: 每次写一个类之前,先用一句人话描述清楚这个类是什么。如果你发现一句话说不清,说明这个类的职责可能太杂了,不如拆开成几个更小的类。我在实际项目中靠这个简单判断规避过无数次“大泥球”式设计,希望对你同样有用。
