13_Python面向对象入门:类、对象、属性与方法
Python面向对象入门:类、对象、属性与方法
前面 12 篇我们一直在用"面向过程"的方式写代码——数据是数据,函数是函数,分开处理。对于 50 行的程序,这完全够用。但当你的程序变得复杂——一个学生管理系统有几十种数据和操作——你需要一种能把"数据"和"操作数据的方法"绑在一起的方式。这就是面向对象编程(OOP)的核心思想。
不要被"面向对象"这个名字吓到。它的底层逻辑和你日常生活中的思维方式非常接近——你看到一辆车,它是一个"对象";它有"属性"(颜色、速度、油量);它能执行"方法"(加速、刹车、转向)。在 Python 里定义一个"车"类型,和你在脑子里定义"车"这个概念的思维方式,本质上是一样的。
这篇从零开始,把类、对象、属性、方法这些概念用最直观的方式讲清楚。
一、面向对象基础
从一段没有使用类的代码开始
# 用字典表示学生
student1 = {"name": "小明", "age": 20, "score": 85}
student2 = {"name": "小红", "age": 21, "score": 92}
def get_level(student):
s = student["score"]
if s >= 90: return "A"
elif s >= 80: return "B"
elif s >= 70: return "C"
elif s >= 60: return "D"
return "F"
def print_student(student):
print(f"{student['name']},{student['age']}岁,{student['score']}分,等级{get_level(student)}")
print_student(student1)
print_student(student2)
这种写法能用,但数据和操作它们的函数是分离的——student1 这个字典和 get_level 这个函数之间没有语法层面的联系。如果你不小心给 get_level 传了一个不是学生的字典(比如 {"product": "苹果", "price": 5}),只有在运行时才会报错。随着数据种类增多,这种"数据漫游——函数处理"的模式会越来越难以维护。
类:自定义的数据类型
class Student:
"""学生类:描述一个学生"""
def __init__(self, name, age, score):
self.name = name # 属性:姓名
self.age = age # 属性:年龄
self.score = score # 属性:成绩
def get_level(self):
"""根据成绩返回等级"""
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 70:
return "C"
elif self.score >= 60:
return "D"
return "F"
def introduce(self):
"""自我介绍"""
return f"{self.name},{self.age}岁,{self.score}分,等级{self.get_level()}"
# 创建对象(实例化)
s1 = Student("小明", 20, 85)
s2 = Student("小红", 21, 92)
print(s1.introduce()) # 小明,20岁,85分,等级B
print(s2.introduce()) # 小红,21岁,92分,等级A
现在数据(name、age、score)和操作(get_level()、introduce())被封装在同一个 Student 类型里。你创建 s1 = Student("小明", 20, 85) 时,Python 自动调用 __init__ 方法初始化这个学生的所有属性。之后调用 s1.get_level() 时,self 自动指向 s1,用的是 s1 自己的数据。
理解类和对象的关系
类是模板——它定义了"一个学生应该有哪些属性、能做什么"。对象是用模板造出来的具体实例——小明和小红是两个不同的学生对象,他们共享同一套方法,但有自己的独立数据。
用生活场景来理解:类就像饼干模具,对象就是用模具压出来的饼干。你只有一个模具(类),但可以压出很多块饼干(对象)。每块饼干的形状一样(结构相同),但上面的糖霜不同(数据不同)。
# 类 = 模板
class Student:
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
# 对象 = 用模板创建的实例
s1 = Student("小明", 20, 85) # s1 是 Student 的一个实例
s2 = Student("小红", 21, 92) # s2 是另一个实例
print(type(s1)) # <class '__main__.Student'>
print(isinstance(s1, Student)) # True
print(s1 is s2) # False —— 两个不同的对象
init 方法:对象的初始化器
__init__ 是 Python 类里最特殊的方法。它不是在"创建"对象(创建由 __new__ 负责),而是在对象创建好之后初始化属性。你不用显式调用它——当你写 Student("小明", 20, 85) 时,Python 自动调用了 __init__:
# 你看到的
s1 = Student("小明", 20, 85)
# Python 内部做的(简化版)
# 1. 创建一个空的 Student 对象
# 2. 调用 Student.__init__(这个空对象, "小明", 20, 85)
# 3. __init__ 给空对象设置 name、age、score 属性
# 4. 返回设置好的对象
self 就是那个"正在被初始化的对象"。在 __init__ 函数里,self.name = name 这行代码把传入的参数 name(“小明”)设成对象的 name 属性。self 这个名字不是 Python 的关键字——写成 this、me 都可以,但 Python 社区约定俗成用 self。
类属性和实例属性
class Student:
school = "第一中学" # 类属性:所有实例共享
def __init__(self, name, score):
self.name = name # 实例属性:每个实例独立
self.score = score
s1 = Student("小明", 85)
s2 = Student("小红", 92)
print(s1.school) # 第一中学 —— 通过实例访问类属性
print(s2.school) # 第一中学 —— 同一个值
Student.school = "第二中学" # 修改类属性
print(s1.school) # 第二中学 —— 所有实例都看到变化
print(s2.school) # 第二中学
类属性适合存放"所有实例共享"的数据——比如数据库连接、配置常量、计数器。实例属性适合存放"每个实例独有"的数据——姓名、成绩、年龄。初学时的一个好习惯是:凡是不确定该不该共享的数据,都放在 __init__ 里做成实例属性。

图13-1 类与对象关系:类是模具,对象是用模具创建的具体实例。每个对象有独立的属性值,共享类的方法定义。
方法的本质
类里的函数叫方法。它们和普通函数的唯一区别是第一个参数永远是 self——它告诉方法"你在处理哪个对象的数据":
class Dog:
def __init__(self, name):
self.name = name
def bark(self, count):
"""叫 count 声"""
for _ in range(count):
print(f"{self.name}:汪汪!")
dog1 = Dog("旺财")
dog2 = Dog("来福")
dog1.bark(2) # 旺财叫 2 声 —— self 就是 dog1
dog2.bark(1) # 来福叫 1 声 —— self 就是 dog2
# 等价于(实际上 Python 内部就是这么做的)
Dog.bark(dog1, 2) # 类.方法(实例, 参数...)
Python 在调用 dog1.bark(2) 时,自动把 dog1 作为第一个参数传给 bark 方法。这就是为什么你在定义方法时不用写 self 这个参数在调用时的值——Python 会自动补上。
封装:用属性访问控制保护数据
面向对象的一个核心原则是封装——对象的内部数据不应该被随意修改。在 Python 里,用下划线约定来表示"这个属性是内部的,请勿直接访问":
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # 单下划线:约定为"内部属性"
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须大于 0")
self._balance += amount
print(f"存入 {amount} 元,余额 {self._balance} 元")
def withdraw(self, amount):
if amount <= 0:
raise ValueError("取款金额必须大于 0")
if amount > self._balance:
raise ValueError("余额不足")
self._balance -= amount
print(f"取出 {amount} 元,余额 {self._balance} 元")
@property
def balance(self):
"""余额只读属性"""
return self._balance
account = BankAccount("小明", 1000)
account.deposit(500) # 存入 500 元,余额 1500 元
account.withdraw(300) # 取出 300 元,余额 1200 元
print(account.balance) # 1200 ——可以读
# account.balance = 9999 # AttributeError ——不能直接改
_balance 前面的单下划线是一种约定——“我是内部变量,不要直接碰我”。Python 不会真的阻止你写 account._balance = 9999,但如果你这么做,就等于在跟写这个类的人说"我知道不应该,但我就是要冒险"。@property 装饰器把 balance 变成了一个只读属性——读起来像属性,背后调的是方法。

图13-2 self 指向当前对象:调用 stu.show() 时 Python 自动将 stu 传给 show(self) 的第一个参数。
二、进阶技巧
特殊方法:让你的对象更像 Python 内置类型
以双下划线开头和结尾的方法叫特殊方法(也叫魔法方法)。它们让自定义对象能配合 Python 内置函数使用:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __str__(self):
"""print() 和 str() 调用时显示的字符串"""
return f"Student({self.name}, {self.score}分)"
def __repr__(self):
"""在交互模式和调试时显示的字符串"""
return f"Student('{self.name}', {self.score})"
def __lt__(self, other):
"""定义 < 比较——让两个学生可以按成绩比较"""
return self.score < other.score
def __eq__(self, other):
"""定义 == 比较"""
return self.name == other.name and self.score == other.score
s1 = Student("小明", 85)
s2 = Student("小红", 92)
print(s1) # Student(小明, 85分) ——调用了 __str__
print(s1 < s2) # True ——调用了 __lt__
print(s1 == s2) # False ——调用了 __eq__
有了 __lt__,你可以直接用 sorted() 对学生列表排序。有了 __str__,print() 输出的是有意义的信息而不是 <__main__.Student object at 0x...> 这样的内存地址。这些特殊方法让你的自定义类看起来、用起来都像 Python 的原生类型。
dataclass:用最少的代码写数据类
如果一个类主要是用来存数据的(像上面的 Student,属性多、方法少),Python 3.7 引入了 dataclass 让你省去大量样板代码:
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
score: float
def get_level(self):
if self.score >= 90: return "A"
elif self.score >= 80: return "B"
elif self.score >= 70: return "C"
elif self.score >= 60: return "D"
return "F"
s1 = Student("小明", 20, 85)
s2 = Student("小红", 21, 92)
# 自动生成了 __init__、__repr__、__eq__ 等方法
print(s1) # Student(name='小明', age=20, score=85)
print(s1 == s2) # False
print(s1 == Student("小明", 20, 85)) # True ——自动比较所有字段
三行类型注解 name: str、age: int、score: float 省去了你手写 __init__、__repr__、__eq__ 的十几行代码。对于以数据为中心的类,dataclass 是首选——代码少、自动生成的逻辑靠谱、可读性高。
图13-3 从类到对象调用全流程:class 定义 → 调用 Student() 触发 init → 创建实例 → 调用实例方法,self 自动指向当前对象。
什么时候应该用类而不是字典
这是一个很多人困惑的问题。一个简单的判断标准:当你的数据结构上面有固定的操作时,用类;如果只是单纯地存放数据,字典和 dataclass 都可以。
# 适合用字典:纯数据,没有专门的操作
config = {"host": "localhost", "port": 8080, "debug": True}
# 适合用类:数据结构附带专门的操作
class DatabaseConfig:
def __init__(self, host, port):
self.host = host
self.port = port
def get_connection_string(self):
return f"mysql://{self.host}:{self.port}"
def validate(self):
if not self.host:
raise ValueError("host 不能为空")
当你发现自己在写很多"接收同一个字典参数、对这个字典做操作"的函数时,就是把这些函数和字典合并成一个类的时机。类不强求——如果你觉得字典够用,就用字典。面向对象是工具,不是义务。
类和字典——什么时候该用哪个
这个问题值得单独拿出来讲,因为它在实际编码中每天都会遇到。一个简单的判断:如果你的数据只是一堆键值对,没有专属的操作逻辑——用字典或 dataclass。如果你的数据带着固定的、专属的操作——用类。
比如一个 API 返回的 JSON 数据:{"name": "小明", "score": 85}。你只是把它从 API 传递到数据库,不需要对"小明"做什么操作——字典就够了。但如果你的系统里到处都在计算"这个学生及格了吗"“这个学生排名第几”,而且计算逻辑在十几个地方重复——这时候就该封装成 Student 类了。
一个实用经验:先用字典写,当发现相同的处理逻辑在三个以上地方重复出现时,重构为类。过早地引入类会让代码过度工程化——100 行的程序被拆成了七八个类,每个类只有两行逻辑。但过晚地引入类会让代码难以维护——到处是复制粘贴的相似函数。掌握"刚刚好"的时机需要经验,而经验来自于不断地写代码、不断地感受痛点。
为什么 init 不是构造函数
很多资料把 __init__ 称为"构造函数",这在技术上是不准确的。Python 真正创建对象的方法叫 __new__——它在 __init__ 之前被调用,负责创建并返回一个新的空对象。__init__ 接收这个空对象(即 self),给它装填属性。由于 __new__ 在 99.9% 的场景下不需要你手动干预,日常说"构造"的时候指的都是 __init__。
了解这个区别在实际编码中很少用到,但它解释了一个现象:为什么你不能在 __init__ 里 return 一个不同的对象。因为对象的创建已经由 __new__ 完成了,__init__ 只能给这个对象添加属性,不能"狸猫换太子"。
三、实战与思维
实战:用类重写学生管理系统
from dataclasses import dataclass
from typing import List
@dataclass
class Student:
student_id: str
name: str
scores: List[float]
def average(self):
if not self.scores:
return 0
return sum(self.scores) / len(self.scores)
def get_level(self):
avg = self.average()
if avg >= 90: return "A"
elif avg >= 80: return "B"
elif avg >= 70: return "C"
elif avg >= 60: return "D"
return "F"
class Classroom:
def __init__(self, name):
self.name = name
self.students = []
def add(self, student):
self.students.append(student)
def find(self, student_id):
for s in self.students:
if s.student_id == student_id:
return s
return None
def remove(self, student_id):
s = self.find(student_id)
if s:
self.students.remove(s)
return True
return False
def get_top_students(self, n=3):
return sorted(self.students, key=lambda s: s.average(), reverse=True)[:n]
def report(self):
print(f"===== {self.name} 班级报告 =====")
print(f"总人数:{len(self.students)}")
if not self.students:
return
all_scores = [s.average() for s in self.students]
print(f"班级平均分:{sum(all_scores)/len(all_scores):.1f}")
print(f"最高分:{max(all_scores):.1f}")
print(f"前三名:")
for rank, s in enumerate(self.get_top_students(), 1):
print(f" {rank}. {s.name} (均分 {s.average():.1f}, {s.get_level()})")
# 使用
classroom = Classroom("一年级一班")
classroom.add(Student("001", "小明", [85, 92, 78]))
classroom.add(Student("002", "小红", [92, 88, 95]))
classroom.add(Student("003", "小刚", [78, 82, 80]))
classroom.add(Student("004", "小美", [95, 98, 91]))
classroom.report()
看,Student 负责描述一个学生,Classroom 负责管理一群学生。每个类的职责单一清晰。这就是面向对象在实际项目中的应用方式——不是炫技,而是用更贴近人类思维的方式组织代码。
学会区分类方法和静态方法——@classmethod 和 @staticmethod
类里的方法不只有一种。除了以 self 为第一个参数的普通方法,还有两种特殊方法值得了解:
class Student:
school = "第一中学"
def __init__(self, name, score):
self.name = name
self.score = score
@classmethod
def from_string(cls, data_str):
"""工厂方法:从字符串创建对象,如 "小明,85" """
name, score = data_str.split(",")
return cls(name, int(score))
@classmethod
def change_school(cls, new_name):
"""修改类属性"""
cls.school = new_name
@staticmethod
def is_valid_score(score):
"""不依赖类或实例的独立工具函数"""
return 0 <= score <= 100
# 类方法的使用
s = Student.from_string("小明,85")
print(s.name, s.score) # 小明 85
# 静态方法的使用
print(Student.is_valid_score(95)) # True
print(Student.is_valid_score(150)) # False
@classmethod 的第一个参数是 cls(类本身,而非实例),常用于"工厂方法"——用不同的输入方式创建对象。@staticmethod 不需要 self 或 cls,就是一个放在类里的普通函数——放在类里只是因为它和这个类在逻辑上相关。这两种方法在初学阶段用得不多,但当你在框架源码或别人的设计中看到它们时,知道它们是什么就不会困惑了。
动手练习
-
银行账户类:完善
BankAccount类,添加交易历史记录(每次存取款都记录下来),添加print_history()方法打印交易明细。 -
图书类:创建
Book类(属性:书名、作者、ISBN、是否在馆),创建Library类(管理书籍、支持借书和还书、支持按书名/作者搜索)。 -
dataclass 实战:用 dataclass 定义
Product(商品名、单价、库存),写一个ShoppingCart类管理购物车(添加商品、移除商品、计算总价、应用折扣)。 -
用类改写一段面向过程代码:找一段你之前写的、超过 30 行的面向过程代码(比如第 6 篇的成绩统计程序),用类和对象的思想重写。体验一下"把数据和相关函数绑在一起"和"让它们各自飘在外面"在代码组织上的区别。做完之后问自己:这个程序用面向对象写是不是更好?如果答案是"好像也没什么区别"——那说明这个程序确实不需要面向对象。能够理性地选择"不用 OOP",比盲目地"到处都是类"更成熟。OOP 不是目的——写出清晰、可维护的代码才是目的。有些问题用函数式风格写出来更简洁,有些问题用 OOP 写出来更清晰。随着经验的积累,你会逐渐形成对"这道题该用哪种范式"的直觉。在那之前,一个简单的参考标准是:如果你发现自己在函数间反复传递同一组相关的数据,而且这些数据上面有固定的操作逻辑——那就是面向对象的信号。如果数据只是数据,操作只是操作,两者没有内在的绑定关系——那就不是必须用类。
-
用
__repr__改进调试体验:给你之前写的 Student 类添加__repr__方法,使得在交互模式中直接输入s就能看到Student(name='小明', score=85)而不是<__main__.Student object at 0x7f...>。对比有无__repr__时在交互模式下操作一堆对象的便利性差异——这个区别在调试复杂数据结构时会放大十倍。 -
设计一个"停车场"系统(选做):有几种不同类型的车辆(小汽车、卡车、摩托车),每种车的停车费计算方式不同。停车场有固定容量,满了之后不允许进入。设计一个包含
ParkingLot、Vehicle及其子类的系统,支持车辆进入、离开、查询当前空位数。这个练习会逼你同时运用面向对象的四个核心概念:继承(不同车辆类型)、多态(统一计算停车费)、封装(保护停车场内部状态)、组合(停车场包含车辆列表)。做完之后你会对 OOP 的设计感觉有质的飞跃。:有几种不同类型的车辆(小汽车、卡车、摩托车),每种车的停车费计算方式不同。停车场有固定容量,满了之后不允许进入。设计一个包含ParkingLot、Vehicle及其子类的系统,支持车辆进入、离开、查询当前空位数。这是现实生活中 OOP 继承和多态的经典场景。
面向对象的思维转变——从"这件事怎么做"到"这件事该谁做"
面向对象不仅仅是语法——它在你学了 class 之后不会自动到来。它是一种思维方式的转变。面向过程编程的思路是"第一步做什么,第二步做什么,第三步做什么",像一个操作手册。面向对象编程的思路是"这个系统里有几种角色,每种角色有什么属性和能力"。
用订餐系统来打比方。面向过程的思路:获取用户输入 → 查找餐厅 → 生成菜单 → 计算价格 → 下单 → 支付。面向对象的思路:有一个 User 类(有姓名、地址、余额),有一个 Restaurant 类(有名称、菜单、评分),有一个 Order 类(关联用户和餐厅、包含菜品列表、能计算总价)。对象之间通过方法来协作——user.place_order(restaurant, items) → restaurant.confirm(order) → user.pay(order)。
没有哪种思维方式天生更优。但在处理复杂系统时,面向对象的思想往往更接近人类的直觉——我们本来就是用"谁有什么、谁能做什么"的方式来理解世界的。学了类之后,不是每段代码都必须用类来写,而是你在遇到"数据带着固定操作"的场景时,多了一个强大的工具来选择。
要点回顾
- 类是模板,对象是用模板创建的具体实例
__init__初始化对象属性,self指向当前实例- 类属性所有实例共享,实例属性每个对象独有
- 用单下划线约定"内部属性",
@property创建只读属性 - 特殊方法(
__str__、__lt__等)让自定义对象像内置类型一样工作 - dataclass 帮你省去写
__init__等样板代码——以数据为中心的类的首选
下篇预告
下一篇我们进入面向对象进阶——继承、多态、封装和更多的魔法方法。
更多推荐


所有评论(0)