在 Python 面向对象编程中,最先接触的两个词就是“类”和“实例”。很多初学者能背出“类是模板,实例是具体对象”这句话,但真到自己写代码时,仍然不知道什么时候该定义类、类里该放什么、创建实例后属性为什么拿不到,或者报了TypeError也不知道往哪查。这篇教程会从零开始,用一个小例子把类和实例讲清楚,并带出__init__、self、实例属性、类属性、方法调用这些绕不开的知识点。读完以后,你不仅能写出自己的第一个类,还能明白每一行代码背后在做什么,遇到报错时知道按什么顺序排查。
1. 先解决一个问题:为什么普通函数写法不够用
1.1 用字典存学生信息,很快会遇到三个麻烦
假设现在要写一个小程序,记录学生的姓名和年龄。如果不用类,最直接的写法是用字典:
stu1 = {"name": "Alice", "age": 18} stu2 = {"name": "Bob", "age": 19} print(stu1["name"], stu1["age"])这段代码能跑,但一旦数据量变大、逻辑变多,问题就暴露出来了。
第一个麻烦是结构靠自觉。每个学生都必须有name和age两个键,但 Python 不会强制检查。如果你不小心写成了{"name": "Alice", "age": 18, "score": 90},别的代码根本不知道这个字典多了一个字段,也没有人能告诉你这样写是否合理。
第二个麻烦是行为没地方放。如果要把学生信息格式化成一句话,你得单独写一个函数:
def intro(stu): return f"{stu['name']} is {stu['age']} years old." print(intro(stu1))每增加一种操作,就要增加一个函数;每个函数都要接受一个字典参数。调用者还得记得传入的字典长什么样。这种“数据结构”和“操作数据的函数”分离的写法,项目小时很灵活,项目变大后很容易出现到处传参、改一个结构要改很多函数的情况。
第三个麻烦是数据和行为没有绑定关系。一个学生应该有哪些属性、能做什么事,散落在代码各处,读代码的人很难一眼看出“学生”这个概念到底长什么样。
1.2 类把“数据结构”和“行为”绑定在一起
面向对象编程并不是魔法,它的核心价值是把数据和操作数据的方法放到同一个结构里。这个结构在 Python 里就是类。
用类改写上面的学生信息:
class Student: def __init__(self, name, age): self.name = name self.age = age def intro(self): return f"{self.name} is {self.age} years old."创建学生和调用自我介绍时,代码变成:
stu1 = Student("Alice", 18) print(stu1.intro())对比一下两种写法:
| 对比维度 | 字典函数写法 | 类和实例写法 |
|---|---|---|
| 数据结构是否固定 | 不强制,缺少键容易出错 | 创建实例时必须传入name和age |
| 数据与操作关系 | 分离,函数需要接收字典 | 数据和方法绑定在同一个实例上 |
| 扩展性 | 新增字段要同步改多处函数 | 给类新增属性或方法,调用方基本不用改 |
| 语义表达 | 只能靠变量名猜 | 类名和方法名直接表达业务含义 |
这个例子并不复杂,但它说明了一个关键点:类不是初学者想象中“很高级、很抽象”的东西,而是把相关数据和函数组织起来的一种代码结构。理解到这一步,再去看类和实例的定义,会更自然。
2. 类和实例的核心概念:模板、产品与创建过程
2.1 类是模板,实例是模板创建出来的具体对象
在 Python 中,类是对一类事物的抽象描述。它定义这类事物“有什么属性”和“能做什么行为”。实例则是按照这个模板创建出来的一个具体个体。
用“学生”举例:
Student是类,它描述所有学生都有的属性和行为。stu1 = Student("Alice", 18)创建了一个实例。stu1是一个具体的学生,它的名字是 Alice,年龄是 18。
这句话很多人听过,但实操时会忽略另一个事实:在 Python 里,类本身也是对象。类是type类型的对象,实例是类的对象。
可以用内置函数验证:
class Student: pass stu1 = Student() print(type(Student)) # <class 'type'> print(type(stu1)) # <class '__main__.Student'>输出结果很直观:
type(Student)是<class 'type'>,说明Student这个类本身也是一个对象,由type创建。type(stu1)是<class '__main__.Student'>,说明stu1是由Student类创建出来的实例。
“类本身也是对象”这一点,在刚入门时不需要深入理解,但它能解释很多后续现象。比如你可以在函数里把类当作参数传入,也可以动态创建类。现在先把“类是模板、实例是产品”这个印象建立起来。
2.2 实例化过程发生了什么
当执行Student("Alice", 18)时,Python 并不是只做了一次简单函数调用。它内部实际经历了两步:
- 调用类的
__new__方法,在内存中创建一个新对象。 - 调用
__init__方法,对这个新对象进行初始化,把参数绑定到实例属性上。
__new__负责“创建空壳”,__init__负责“填充内容”。对于普通入门场景,只需要关注__init__,因为绝大多数初始化逻辑都写在__init__中。
注意:
__init__不是严格意义上的构造函数。真正创建对象的是__new__,__init__只是实例创建后自动执行的一个初始化钩子。这个概念在初级教程里容易被忽略,但后面学继承、元类时会用得上。
3. 环境准备:先确认 Python 版本能正常执行
3.1 检查 Python 环境
这篇文章里的所有代码都基于 Python 3。执行类和实例相关语法,不需要安装第三方库,也不需要配置虚拟环境,普通 Python 解释器就能完成。
先打开终端或命令行,检查 Python 版本:
python --version如果电脑上同时安装了多个 Python 版本,可能需要使用python3命令:
python3 --version只要输出类似Python 3.10.x、Python 3.11.x这样的信息,就可以继续。如果python命令不存在,可以先用系统安装的 Python 或 IDE 自带解释器确认版本后再运行。
3.2 推荐使用交互式环境或小脚本文件
学习类和实例时,有两种运行方式都合适:
- 在终端输入
python进入交互式环境,逐行输入代码,立刻看到结果。 - 新建一个
student.py文件,把完整代码写进去,用python student.py运行。
交互式环境适合验证小片段,脚本文件适合保存完整示例。这篇文章后面的示例,建议保存为脚本文件运行,这样改起来更清楚。
4. 第一个类和实例:从最简写法开始
4.1 定义一个只有__init__的类
先写一个最简单但有实际意义的类。它只负责接收参数并保存到实例属性中:
class Student: def __init__(self, name, age): self.name = name self.age = age拆解这段代码:
class Student:定义了一个名为Student的类。类名约定使用驼峰命名法,首字母大写。def __init__(self, name, age):定义了初始化方法。self表示即将被创建的实例对象本身。self.name = name把传入的name参数保存到实例属性name上。self.age = age同理。
这里要注意一个容易混淆的地方:name是初始化方法的参数,self.name是实例属性。两者名字相同,但作用不同。参数负责接收外部传入的值,实例属性负责把这个值绑定到当前实例上,供之后的方法使用。
4.2 创建实例并访问属性
创建实例的语法就是调用类名并传入参数:
stu1 = Student("Alice", 18) print(stu1.name) print(stu1.age)运行结果:
Alice 18这里最关键的一步是Student("Alice", 18)。这个语句触发__init__的执行,把"Alice"和18分别绑定到stu1.name和stu1.age上。
创建第二个实例也很自然:
stu2 = Student("Bob", 19) print(stu2.name) print(stu2.age)两个实例互不影响。stu1.name是 Alice,stu2.name是 Bob。每个实例维护自己独立的一份属性,这就是“实例”的独立性。
4.3 给实例添加方法,让实例能“做事”
只有属性还不够,面向对象的核心是让实例具备行为。给Student类添加一个方法:
class Student: def __init__(self, name, age): self.name = name self.age = age def intro(self): return f"{self.name} is {self.age} years old."调用方式:
stu1 = Student("Alice", 18) print(stu1.intro())运行结果:
Alice is 18 years old.在这个例子里,intro方法内部直接访问了self.name和self.age。因为self指向的是调用这个方法的实例,所以stu1调用intro()时,拿到的是stu1自己的属性。
注意:方法定义时,第一个参数必须是
self,但调用方法时不需要手动传self。Python 会自动把实例对象作为第一个参数传给方法。这一点是新手最容易困惑的地方,下一节会展开解释。
4.4 加上类型注解,让代码更可读
Python 3 支持类型注解。虽然运行时不会强制检查,但加上注解后,代码的意图更清晰,编辑器也能给出更好的提示:
class Student: def __init__(self, name: str, age: int) -> None: self.name = name self.age = age def intro(self) -> str: return f"{self.name} is {self.age} years old."这里标注了name是字符串类型、age是整数类型、intro返回字符串类型。类型注解不影响代码运行,但对团队协作和后续维护很有帮助。
5. 深入理解__init__和self:这两个点决定你是否真的入门
5.1__init__是初始化方法,不是一定要写
很多初学者以为定义一个类必须写__init__。实际上,如果类不需要接收外部参数,也可以不写__init__:
class Student: pass stu1 = Student()pass表示这个类暂时是空的。此时stu1可以正常创建,但它没有任何属性,也没有任何方法。这种写法通常用于占位或后期动态添加属性,实际项目中很少用空类,但理解它有助于消除对__init__的误解。
__init__的可选参数决定了创建实例时“必须传哪些值”。例如:
class Student: def __init__(self, name: str): self.name = name stu = Student("Alice") # 正确 stu = Student() # 会报错,缺少 name 参数创建实例时传入的参数数量,必须与__init__方法参数匹配(self除外)。如果不匹配,Python 会抛出TypeError。这一点在后面排查报错时会用到。
5.2self的本质:谁调用方法,self 就是谁
self是 Python 实例方法中的一个约定名称。它在方法定义中代表“调用当前方法的实例对象本身”。
为什么必须写self?因为在 Python 中,方法本质上是通过类定义的普通函数。当通过实例调用方法时,Python 会把实例自动传给方法的第一个参数。
看一个能说明问题的等价写法:
class Student: def __init__(self, name): self.name = name def intro(self): return f"{self.name}" stu1 = Student("Alice") # 这两种调用方式等价 print(stu1.intro()) print(Student.intro(stu1))stu1.intro()这种写法会触发 Python 把stu1自动传给intro的self参数。而Student.intro(stu1)是直接通过类调用方法,必须手动传入实例。
这也解释了为什么方法定义时不写self会报错:
class Student: def __init__(self, name): self.name = name def intro(): # 缺少 self return f"{self.name}"当调用stu1.intro()时,Python 会尝试把stu1传给intro(),但intro()没有参数,于是抛出异常。
注意:
self只是一个约定名称,不是 Python 关键字。你可以把它改成this或obj,但强烈不建议这么做。整个 Python 社区都使用self,保持一致性能让代码更容易被他人阅读。
5.3 创建实例时传错参数,会立刻暴露问题
__init__带来的一个好处是“创建即校验”。如果忘记传参数,Python 会直接报错,而不是等到使用属性时才失败。
例如:
class Student: def __init__(self, name: str, age: int): self.name = name self.age = age stu = Student("Alice")运行结果:
TypeError: __init__() missing 1 required positional argument: 'age'这个报错清楚地说明了问题:缺少age参数。定位起来非常方便。
对比之前用字典的写法,如果字典少了一个键,不会立刻报错,只有当某段代码访问缺失的键时才可能出问题。因此,在需要固定结构的场景下,用类代替字典能提前暴露问题。
6. 实例属性、类属性与方法类型:区分清楚才能避免隐蔽 bug
6.1 实例属性与类属性的区别
在类内部,直接写在方法外面的变量叫类属性。在方法内部用self.变量名创建的属性叫实例属性。
看一个对比示例:
class Student: school = "No.1 Middle School" def __init__(self, name: str): self.name = name stu1 = Student("Alice") stu2 = Student("Bob") print(stu1.school) # No.1 Middle School print(stu2.school) # No.1 Middle School print(stu1.name) # Alice print(stu2.name) # Bobschool是类属性,定义在类内部、方法外部。所有实例共享同一个类属性。name是实例属性,每个实例都有自己的独立值。
访问属性时,Python 的查找顺序是:先找实例自己的属性,如果找不到,再去类中找类属性。
print(stu1.school) # 实例本身没有 school,就去类里找这段代码能输出No.1 Middle School,就是因为 Python 在stu1这个实例里没有找到school,于是到Student类里找到了类属性。
如果给实例动态添加一个同名属性,实例属性会“遮蔽”类属性:
stu1.school = "Another School" print(stu1.school) # Another School print(stu2.school) # No.1 Middle Schoolstu1.school变成了实例自己的属性,而stu2仍然访问类属性。这种遮蔽行为在初级代码中很容易造成困惑。
| 属性类型 | 定义位置 | 归属 | 访问方式 | 典型用途 |
|---|---|---|---|---|
| 实例属性 | __init__中用self.属性 | 每个实例独立 | 实例.属性 | 每个对象自己的状态 |
| 类属性 | 类内部、方法外部直接赋值 | 所有实例共享 | 实例.属性或类名.属性 | 所有对象共享的常量或默认配置 |
6.2 易混淆场景:修改类属性会影响所有实例吗
如果类属性是不可变类型,比如字符串或数字,直接在类中修改它是安全的。但如果类属性是可变类型,比如列表或字典,问题就会出现。
class Student: hobbies = [] def add_hobby(self, hobby): self.hobbies.append(hobby) stu1 = Student() stu2 = Student() stu1.add_hobby("reading") print(stu1.hobbies) # ['reading'] print(stu2.hobbies) # ['reading']stu1添加了"reading",结果stu2.hobbies也出现了"reading",因为hobbies是类属性,所有实例共享同一个列表。这通常不是期望行为。
正确的做法是把hobbies设置为实例属性,在__init__中初始化:
class Student: def __init__(self): self.hobbies = [] def add_hobby(self, hobby): self.hobbies.append(hobby)这样每个实例都有自己独立的列表。后面“常见坑”一节还会专门讲这类问题。
6.3 实例方法、类方法和静态方法
在类中定义的方法,默认是实例方法。它的第一个参数是self,操作的是实例数据。
除了实例方法,Python 还支持类方法和静态方法,用装饰器声明:
class Student: school = "No.1 Middle School" def __init__(self, name: str): self.name = name def intro(self): return f"{self.name} from {self.school}" @classmethod def get_school(cls): return cls.school @staticmethod def version(): return "1.0"intro是实例方法,第一个参数是self,可以访问实例属性和类属性。get_school是类方法,第一个参数是cls,表示当前类本身。它不访问实例属性,只访问类属性。version是静态方法,既不接收self也不接收cls,类似于一个放在类里的普通函数。
对于刚入门的学习者,只需掌握一个判断原则:如果方法需要访问具体实例的属性,就用实例方法;如果方法只和类相关、不需要实例数据,用类方法或静态方法都可以。
| 方法类型 | 第一个参数 | 何时使用 | 调用方式 |
|---|---|---|---|
| 实例方法 | self | 需要访问实例属性或修改实例状态 | 实例.方法() |
| 类方法 | cls | 需要访问类属性或执行与类相关的操作 | 类名.方法()或实例.方法() |
| 静态方法 | 无 | 与类和实例都无关,只是逻辑上放在类内 | 类名.方法()或实例.方法() |
7. 新手最容易踩的 5 个坑
7.1 实例方法忘记写 self
错误写法:
class Student: def __init__(self, name): self.name = name def intro(): return "hello"调用stu1.intro()时会报错:
TypeError: intro() takes 0 positional arguments but 1 was given原因是 Python 调用实例方法时,会自动把实例对象作为第一个参数传入。方法定义没有接收任何参数,于是报错。
正确写法是在intro中加self,即使不准备使用self也要写:
def intro(self): return "hello"7.2 可变默认参数导致所有实例共享同一个列表
错误写法:
class Student: def __init__(self, name, hobbies=[]): self.name = name self.hobbies = hobbies当两个实例都没有传hobbies时,它们会共享同一个默认列表。给其中一个实例添加兴趣爱好,另一个实例也会受影响。
正确写法是把可变对象放在__init__内部初始化:
class Student: def __init__(self, name, hobbies=None): self.name = name self.hobbies = hobbies if hobbies is not None else []这样每个实例在没有传入hobbies时,都会得到一个新的空列表。
7.3__init__拼写错误,导致属性没有初始化
Python 双下划线方法对拼写非常敏感。如果把__init__误写成_init_或__init,类仍然可以创建实例,但初始化方法不会执行。
错误写法:
class Student: def _init_(self, name): self.name = name stu = Student("Alice") print(stu.name)运行结果:
AttributeError: 'Student' object has no attribute 'name'这里没有立即报创建错误,因为Student("Alice")调用的并不是_init_,而是系统默认的普通方法。Python 中没有name属性,所以访问时抛出AttributeError。
排查方法是检查双下划线的数量和拼写:__init__是前后各两个下划线。也可以直接看错误提示中是否提到了__init__。
7.4 在__init__里修改类属性,导致所有实例都受影响
错误写法:
class Student: school = [] def __init__(self, name): self.name = name self.school.append(self.name)每次创建实例,school列表都会被追加名字。所有实例看到的school都是同一个列表,并且长度还会持续增长。
正确做法是确认这个属性到底属于类还是属于实例。如果每个学生有自己独立的提交记录,应该放在实例属性中;如果确实需要全类共享,则要明确这种累加行为是否符合业务需求。
7.5 直接创建实例后,以为可以无限使用未定义的属性
Python 允许给实例动态添加属性:
stu1 = Student("Alice") stu1.score = 90 print(stu1.score)这段代码能运行。但它违背了类的设计意图:score没有在__init__中定义,其他代码无法通过阅读类定义知道这个属性的存在。
实际项目中,属性应该在__init__中统一定义。一方面可以提高可读性,另一方面可以结合类型注解和检查工具提前发现拼写错误。不要依赖 Python 的动态特性随意添加属性。
8. 排查路径:类和实例相关报错从哪一层查起
类和实例相关的报错,看起来五花八门,但实际大多集中在几个固定环节。按下面的顺序排查,能快速定位问题。
| 问题现象 | 常见原因 | 检查点 |
|---|---|---|
TypeError: Student() takes no arguments | 类没有定义__init__,或__init__拼写错误 | 检查__init__是否存在且拼写正确 |
TypeError: __init__() missing 1 required positional argument | 创建实例时少传了参数 | 对比__init__的参数列表与调用语句 |
TypeError: intro() takes 0 positional arguments but 1 was given | 实例方法没有定义self参数 | 检查方法定义的第一参数是否为self |
AttributeError: 'Student' object has no attribute 'name' | 属性没有在__init__中赋值,或属性名拼写错误 | 检查__init__内部赋值语句 |
| 属性值所有实例都一样 | 修改了类属性,或在方法中直接修改了可变类属性 | 检查属性定义位置是类级别还是实例级别 |
排查时有一个建议顺序:
- 先看报错行数,确定是创建实例时出错,还是访问属性、调用方法时出错。
- 检查
__init__的参数名和赋值语句是否一致。self.name = name中,右侧的name是参数,左侧的self.name是属性,二者拼写都要核对。 - 如果是方法相关报错,优先检查方法定义是否写了
self。 - 如果是属性相关报错,优先检查
__init__是否执行了这个赋值。 - 如果涉及类属性,确认修改的是类属性还是实例属性。
养成“先看报错行,再对照类定义”的习惯,类和实例相关的 80% 问题都可以在几分钟内解决。
9. 从学习环境到生产环境的差异:现在不用做,但要了解方向
刚学习类和实例时,代码通常写在单个脚本文件里,运行完就结束。生产环境的 Python 项目中,类和实例的使用会复杂得多。
有四个方向需要提前了解但不能急于深挖:
第一个是类型检查和数据校验。上面提到的类型注解,配合mypy或pyright这类工具,可以在代码运行前发现类型不一致的问题。生产项目中,这比等到运行时报错要高效得多。
第二个是数据类的使用。Python 3.7 引入了@dataclass,它可以自动生成__init__、__repr__等样板代码。在类和实例的__init__模式掌握之后,可以逐步接触数据类,减少重复代码。
第三个是日志而不是print。学习阶段用print便于观察结果,但生产环境中需要记录日志,方便定位问题。日志不是print的简单替换,它还包含时间、级别、模块信息、异常堆栈等。
第四个是测试。类写完后,单元测试是验证行为最直接的方式。下面的简单测试思路能帮助你理解“验证如何做”:
def test_student_intro(): stu = Student("Alice", 18) assert stu.intro() == "Alice is 18 years old."如果后面要深入学习,可以把 pytest 作为第一个测试框架。先不要急着在生产环境里引入各种设计模式,扎实理解类和实例的基础语义才是关键。
10. 练习建议与下一步扩展路径
学完这篇教程后,动手练习比继续阅读更有价值。建议按从易到难的顺序完成下面三个练习。
第一个练习:定义一个Book类,包含title、author、price三个属性,并提供一个方法返回图书的完整信息。难度低,重点练习__init__和实例方法。
第二个练习:定义一个BankAccount类,包含owner和balance两个属性,提供deposit和withdraw两个方法。注意在withdraw中判断余额是否足够。这个练习可以检验你能否在方法中修改实例属性。
第三个练习:定义一个TodoList类,内部维护一个任务列表,提供add、remove、show三个方法。这个练习能帮助你理解可变实例属性的使用,并避开前面提到的“类属性共享可变对象”的坑。
完成这三个练习后,再学习下一个面向对象知识点会更顺畅。建议按下面的顺序扩展:
- 属性装饰器
@property,学习如何控制属性的读写。 - 继承与多态,理解子类如何复用父类代码。
@dataclass,学会减少样板代码。- 魔术方法,例如
__repr__、__eq__,让自定义类的行为更像内置类型。
类和实例是 Python 面向对象编程的地基,第一次接触时不需要把所有细节都理解透彻。先把“定义类、创建实例、使用属性、调用方法”这条主链路跑熟,再逐步扩展到更复杂的场景。当你能自然说出“这个属性应该属于类还是属于实例”“这个方法需不需要操作实例数据”时,面向对象的基本功就算到位了。