1. 面向对象的思维转变为什么代码会从函数走向类我最早带新人学编程的时候发现一个很有意思的分水岭循环、条件、函数这些语法大部分人啃两周就能上手但真到了要写一个稍微像样的项目——比如做一个联系人管理工具、模拟一个游戏角色、统计全班成绩——代码就开始乱套了。问题的根源往往不是语法不会而是思维模型还停留在面向过程一股脑地把所有逻辑拆成函数然后靠一堆参数在函数之间来回传递数据。传着传着你根本分不清哪个函数改了哪份数据改一个数据结构就得牵连七八个函数。这时候就是时候认真学类和对象了。这一节内容是给已经掌握基本语法、想跨入面向对象编程OOP大门的读者准备的。我会把上部分聚焦在最核心的地基上类是什么、对象是什么、它们之间的关系是什么、怎么用代码创建和操作它们。继承、多态、魔法方法这些进阶内容这一节不碰下一节再展开。把地基打牢了后面都是水到渠成的事这也是我说上必须单独讲清楚的原因。面向对象编程的理念几乎渗透在所有现代主流语言里Python、Java、C#、JavaScript、Go甚至前端框架的组件化设计底层都在用这套思路。你可以不写一个类就完成很多脚本但一旦项目超过几百行、需要多人协作、需要反复迭代需求类与对象的封装能力就会体现出碾压性的优势。理解这一节就等于拿到了阅读大型项目源代码的第一把钥匙。1.1 从过程拆解到对象划分的视角转换先看一个典型的面向过程写法。假设我们要模拟一个银行账户系统最简单的场景是存取款和查余额。面向过程的思路是这样先把所有账户数据塞进一个字典然后定义一堆操作函数每个函数都接收这个字典作为参数。def create_account(owner, balance0): return {owner: owner, balance: balance} def deposit(account, amount): if amount 0: print(存款金额必须大于0) return account account[balance] amount return account def withdraw(account, amount): if amount account[balance]: print(余额不足) return account account[balance] - amount return account def get_balance(account): return account[balance]这么写功能上完全没问题小脚本里甚至还挺清晰。但问题在于数据和操作是分离的。数据放在字典里而操作数据的函数散落在外面。一旦需求变化——比如账户要增加冻结状态、要记录每笔交易流水、要区分活期和定期——你会发现所有函数都要跟着改还非常容易漏改。更麻烦的是如果哪天数据结构从字典换成别的形式所有函数的参数和内部逻辑全部要重写风险极高。面向对象的思路则完全不同我们把账户看作一个整体把数据余额、户主和操作存款、取款、查余额打包在一起形成一个自治的单元。外部想操作账户不再直接摆弄数据结构而是调用这个单元提供的接口。账户内部的细节对外部屏蔽外部只需要关心我调存款方法、传金额进去就行了。这就是封装的基本思想也是面向对象最核心的价值之一。1.2 数据操作绑定到底好在哪你可能觉得上面那点区别不算大多写几行函数一样能解决。确实在几十行的脚本里面向过程完全够用。但我们可以做一个思想实验假设账户系统要加一个功能——打印账户的完整交易流水。面向过程的写法里你得新写一个函数参数里再带上交易记录如果交易记录的数据结构没设计好还得回头改字典。而在面向对象的写法里你只需要在账户类内部新增一个方法外面调用的地方几乎不用动因为账户自己知道自己有哪些数据。我再举个例子帮不熟悉这个概念的读者理解。你买一台洗衣机面向过程的思路是你得自己了解洗衣机的电机、水管、电路结构然后写一套操作流程先注水、再放洗衣粉、再启动电机……面向对象的思路是洗衣机封装成一个整体它提供洗衣服这个接口你只管把衣服塞进去、按启动键内部怎么做是它自己的事。洗衣机内部哪怕从机械结构升级成了全电子控制你按启动键的习惯一点不会变。这就是封装带来的抗变化能力。代码规模越大、维护周期越长这种优势就越明显。2. 类与对象的关系类是一张图纸对象是造出来的房子类和对象这个组合术语很多初学者理解起来费劲是因为它们在中文日常语义里太接近了。我得先给它们一个非常清晰的角色划分类是模板对象是模板生产出来的真实实体。类是图纸,对象是按图纸造出来的房子。图纸定义了房子要有几间房、门窗位置、层高但图纸本身不能住人只有按照图纸实际施工造出来的那一栋栋房子才能住人。每一栋房子都对应着同一张图纸但每一栋房子的实际状态可能完全不同——门牌号不同、装修不同、住的人不同。以程序员的口吻来说类是一种自定义的数据类型对象是该类型的一个实例。这里有几个名词你必须牢牢记住类class、对象object、实例instance、实例化instantiation。对象和实例在绝大多数场景下指同一个东西只是侧重点稍有不同对象强调它是一个独立存在的实体实例强调它是某个类的具体产物。实例化则是创建对象的过程。2.1 一张图纸可以造出无数栋房子还是用Python代码来说。定义一个最简单的类class Student: pass这里的pass表示类的内部暂时什么都不写只是为了先把语法跑通。然后实例化stu1 Student() stu2 Student() print(stu1) # __main__.Student object at 0x... print(stu2) # __main__.Student object at 0x...stu1和stu2就是两个不同的对象。它们都是通过Student这个类创建出来的共享同一套设计图纸但在内存里是两个完全不同、互不干扰的实体。你打印它们时Python会输出两行不同的内存地址。这就好比同一张户型图造出来的两套房子位置完全不同谁也不会影响谁。这里有个容易忽略的细节类名后面的括号Student()是在调用类的构造过程。对于自定义类即使我们没有写任何构造函数Python也会自动帮你完成最基本的对象创建。你看到的pass类其实已经是一个完整的类了只是它不携带任何自定义的数据和行为。2.2 对象身上可以动态挂属性Python有一种比较随性的特性即使类里没有定义任何属性你也能在对象创建之后临时给对象加上属性。比如stu1.name 小明 stu1.age 18 stu2.name 小红这样stu1就有了name和age两个属性而stu2只有name没有age。这在语法上完全合法也是Python灵活性的体现。但我不建议新手依赖这种写法——它能帮你理解对象是属性的容器这个概念却很容易导致代码里属性定义散落各处今天给这个对象加一个属性明天给那个对象加一个项目稍微大一点就再也管不住了。所以正规做法是在类内部定义好需要哪些属性让每个对象从出生那一刻起就拥有完整的属性结构。这个出生时刻就是接下来要说的构造函数。3. 实操用Python定义并创建你的第一个类概念说得再多不如亲手敲一遍。这一章我会带你从零写一个完整的类同时把每一步背后的原因讲透。我们以学生为例一步步从最简单的类扩展成包含属性、方法、初始化的完整版本。3.1 从空白类开始逐步添加属性与方法第一步先定义类名和最基本的属性。按照Python社区的习惯类名使用大驼峰命名法每个单词首字母大写比如Student、BankAccount、GameCharacter函数和变量名则使用小写加下划线比如student_name、get_balance。这个命名规范不是硬性语法要求而是所有Python开发者约定俗成的默契遵守它你的代码别人扫一眼就知道哪些是类、哪些是函数。class Student: def __init__(self, name, age): self.name name self.age age def introduce(self): print(f大家好我是{self.name}今年{self.age}岁)这个类做了两件事定义了一个初始化方法__init__定义了每个对象都应该有的name和age属性定义了一个普通方法introduce对象可以用来打招呼。写完后实例化并调用stu1 Student(王小明, 18) stu2 Student(李小红, 19) stu1.introduce() # 大家好我是王小明今年18岁 stu2.introduce() # 大家好我是李小红今年19岁你会发现创建对象时传入的参数自动变成了对象自己的属性调用introduce方法时它打印出的名字是各自对象的属性互不干扰。这是类与对象最直观的体验同一个模板不同的个体。3.2 为什么代码里到处都是self新手第一次接触类最常见的疑惑就是为什么每个方法定义里的第一个参数都写self为什么调用的时候又不需要传这个参数这其实是Python的一种设计约定。self指代的是当前这个对象本身。当你调用stu1.introduce()时Python背后做了两件事一是找到stu1这个名字对应的对象二是调用Student类里定义好的introduce方法并且把stu1这个对象本身作为第一个参数传进去。所以在方法内部写上self.name意思就是当前对象的name属性。换个对象调用self就指向那个对象自然取到的是那个对象自己的属性。用生活类比类方法是印在洗衣机说明书上的一条操作步骤按下启动键等待程序结束。说明书是通用的但每次有人操作时实际操作的是某一台具体的洗衣机。self就相当于在方法内部告诉代码你现在操作的是这台具体洗衣机不是墙上的说明书。 理解了这一点你就理解了类方法设计的核心逻辑。3.3 一个常见错误方法里忘了接收参数我刚学类的时候犯过一个至今记忆犹新的错误在类里写了一个方法定义时没加self结果一调用就报错missing 1 required positional argument。原因是Python认为类的方法在调用时第一个位置参数永远会被自动传入对象本身。如果定义时只写了def introduce():那么实际调用stu1.introduce()时Python会把stu1当作实参传进去而方法定义却连一个参数都不打算接收自然就报错了。正确的写法永远是def 方法名(self, 其他参数...)。哪怕方法内部完全不需要访问对象的属性也要写self占住第一个位置——这是一个约定也是一个强制性要求。记住这句话你就能避免初学者时期最频繁踩到的第一类报错。4.init和 self对象的出生仪式与自我认知上一节我们其实已经用到了__init__但只是介绍了用法没有解释它的本质。这一节我把它单独拎出来讲因为它是类和对象上这一部分里最重要的两个机制之一__init__是对象出生的那一刻自动执行的初始化函数而self是对象对我是谁的认知方式。4.1init为什么叫初始化而不是构造函数在Python里__init__的官方名称是初始化方法而不是构造函数。这个区别值得较真真正负责在内存里凭空创建一个对象的是__new__方法但几乎所有的日常编程里你都不会去碰它__init__是在对象已经被创建出来之后负责给这个对象填上初始状态的。你可以这样理解__new__是工厂把毛坯房盖好了__init__是装修队进去把水电、家具、墙漆都弄好最后交付给你一套可以拎包入住的房子。作为普通开发者你只需要关心装修也就是在__init__里给属性赋初值。每次执行Student(王小明, 18)时Python会依次做这样几件事在内存里开辟一块空间准备装一个新的Student对象自动调用Student类里定义的__init__方法把王小明传给参数name把18传给参数age在__init__内部self指向刚才开辟的那块空间所以self.name 王小明就是把王小明这个值存进这块空间的name属性里初始化完成返回一个完整可用的对象赋值给stu1。所以每当你看到类名(参数)这种写法你其实不只是在调用类而是在完整走一遍创建对象初始化属性的流程。4.2init里的三条铁律第一__init__这个方法的名称是固定的前后各有两个下划线一个都不能少。写成_init_或者init_Python都只会把它当成一个普通方法创建对象时不会自动调用结果对象创建出来之后一片空白属性全都没定义。这个拼写错误非常隐蔽因为不报错但行为完全不对排查起来很耗时间。第二__init__内部不能有return语句返回值。初始化方法的作用是设置状态不是返回计算结果。如果你在__init__里写return 某个值Python会直接抛TypeError异常。不过你可以在里面写return加一个空语句用于提前结束初始化但这属于比较少见的用法新手阶段不建议玩。第三__init__里定义的属性才是每个对象真正独立拥有的属性。比如self.name name每一个学生对象都会复制一份属于自己的name。这一点在后文讲类属性的时候会形成鲜明对比到那时候你再回头看这条会有更深的体会。4.3 带默认参数的初始化方法初始化方法和其他函数一样也可以给参数设置默认值。这样创建对象时有些信息可以不传class Student: def __init__(self, name, age18, city未知): self.name name self.age age self.city city stu1 Student(王小明) # age 默认18city 默认未知 stu2 Student(李小红, 20, 北京) # 全部显式传入默认参数让类的使用变得灵活有的场景你需要精细控制每个字段有的场景只需要传最关键的几个。但这引出一个更深层的话题什么时候该把数据定义在__init__里什么时候该定义在类里这就到了下一章的范畴——类属性与实例属性的区别。5. 类属性与实例属性共享与独立的边界这是很多学了半年Python的人依然会混淆的知识点也恰恰是理解类和对象上这部分的关键分水岭。我先给结论在__init__方法内部通过self.属性名定义的是实例属性每个对象各自持有一份互不影响直接写在类内部、方法外部的变量是类属性它属于类本身所有对象共享同一份。5.1 两种属性的定义位置与访问方式看代码class Student: school 某市第一中学 # 类属性所有对象共享 def __init__(self, name, age): self.name name # 实例属性 self.age age # 实例属性这里school是类属性name和age是实例属性。访问它们时语法上几乎一样stu1 Student(王小明, 18) print(stu1.school) # 某市第一中学 print(stu1.name) # 王小明 print(Student.school) # 也可以通过类名访问类属性你既可以stu1.school,也可以Student.school。但请注意实例属性是不能通过类名直接访问的比如Student.name会报错因为name只存在于具体的对象身上类本身并没有这个属性。5.2 共享与独立真实场景下的差异用一个容易理解的例子假设学校改名了要把school从某市第一中学改成某市实验中学。如果你改的是类属性那么所有已经创建出来的学生对象的school都会同步变化因为大家访问的是同一份数据Student.school 某市实验中学 print(stu1.school) # 某市实验中学跟着变了但如果某个学生因为转学要单独修改自己的name这不影响其他任何人因为name是每份独立的实例属性stu1.name 王小明改名 print(stu2.name) # 李小红完全不受影响这个例子直观体现了设计层面的取舍凡是所有对象都应该保持一致的信息适合放类属性凡是每个对象各自不同、需要独立管理的信息必须放实例属性。如果你把一个本该独立的属性误放成类属性就会出现改一个学生全班跟着变的幽灵问题。我见过不止一个初学者在这个坑里困惑很久——明明只改了一个人的信息打印出来其他人也变了——最终发现那个人把姓名这种字段写成了类属性。5.3 一个经典陷阱通过对象给类属性赋值这里还有一个非常容易踩的坑。假设你写了这样一段代码class Student: school 某市第一中学 stu1 Student() stu1.school 某市实验中学 # 你以为是修改类属性 print(Student.school) # 某市第一中学没变 print(stu1.school) # 某市实验中学发现了吗stu1.school 某市实验中学并没有修改类属性而是给stu1这个对象新建了一个名为school的实例属性从此stu1.school优先读取自己的实例属性而类属性的值某市第一中学原封不动。其他对象的school依然指向类属性。这个坑特别隐蔽因为它不报错、行为又看似合理只有当你认真输出类属性的值时才会发现问题。记住这个原则给类属性赋值一定通过类名Student.school ...不要通过对象去改。如果你想区分一个属性到底是类属性还是实例属性用__class__或type()也可以查看但对新手来说记住上面这条原则就够了。6. 综合示例用类来封装一个成绩管理模型理论说了一大堆不变成一个真正能跑的项目就还是纸上谈兵。这一节我带你用类与对象做一个成绩管理系统的核心模块过程中你会看到类和对象在实际项目中是如何配合的也会看到为什么类设计得好后续扩展能省多少事。6.1 设计一个Student类让它自己管理成绩需求设定每个学生有名字、学号可以添加课程成绩可以查看自己的平均分和最高分。第一版类设计如下class Student: def __init__(self, name, student_id): self.name name self.student_id student_id self.scores [] # 用来存放所有课程成绩的列表 def add_score(self, course, score): self.scores.append({course: course, score: score}) def average_score(self): if not self.scores: return 0 total sum(item[score] for item in self.scores) return total / len(self.scores) def max_score(self): if not self.scores: return None return max(item[score] for item in self.scores) def show_info(self): print(f学生{self.name}学号{self.student_id}) print(f平均分{self.average_score():.1f}最高分{self.max_score()})使用起来非常直观stu1 Student(王小明, 2025001) stu1.add_score(语文, 92) stu1.add_score(数学, 88) stu1.add_score(英语, 95) stu1.show_info()你会发现整个过程里你不需要关心scores列表具体怎么存的也不需要手动维护平均分的计算逻辑——所有数据和行为都在Student这个类里自洽。外部只需要发起请求调用方法类自己把细节处理好。这就是封装最典型的效果。6.2 再加一个ClassRoom类看类之间如何协作单有学生还不够我们再加一个班级类负责维护多位学生并统计班级层面的指标class ClassRoom: def __init__(self, class_name): self.class_name class_name self.students [] def add_student(self, student): self.students.append(student) def class_average(self): if not self.students: return 0 total sum(stu.average_score() for stu in self.students) return total / len(self.students) def top_student(self): if not self.students: return None return max(self.students, keylambda stu: stu.average_score())两个类协作起来代码的层次感就出来了classroom ClassRoom(高一3班) classroom.add_student(stu1) classroom.add_student(Student(李小红, 2025002)) classroom.add_student(Student(赵小磊, 2025003)) print(f班级平均分{classroom.class_average():.1f}) top classroom.top_student() print(f班级第一名{top.name}平均分{top.average_score():.1f})注意一个细节ClassRoom里直接调用了Student对象的方法比如stu.average_score()。这就是类与类协作的常见形态——一个类持有另一个类的对象作为自己的数据成员并通过调用对方的方法来获取信息。面向对象编程到了后期你写的其实就是一个由类相互连接组成的网络每个类像一个小型工作站各司其职再通过稳定的接口互相配合。这个设计还有一个非常大的好处当需求变化时改动的范围被限制在个别类里。比如要把平均分改成去掉一个最高分、一个最低分后的平均分你只需要修改Student.average_score这一个方法ClassRoom里统计班级平均分的代码一行都不需要动。这就是高内聚、低耦合的朴素体现也是面向对象设计追求的核心目标之一。7. 新手最容易踩的坑以及课后练习建议最后这一章我把我这几年被问频率最高、也最典型的问题集中列出来。这些问题每一个都是我亲眼见过实打实的报错或者诡异行为而不是从文档里抄出来的“注意事项”。7.1 高频错误清单与排查方向现象根本原因解决办法调用方法时报 missing 1 required positional argument方法定义时漏了self第一个参数写上self创建对象后访问属性报 AttributeError__init__里忘记给属性赋值或属性名拼写不一致检查__init__中的赋值语句确认所有需要初始化的属性都已定义__init__没执行但代码不报错把__init__拼写成了_init_、init_等确认方法名是前后各两个下划线__init__修改一个对象的数据其他对象跟着变把实例属性误写成了类属性按前面第5章的方式检查属性定义的位置通过对象给类属性赋值类属性没变对象被新建了一个同名的实例属性覆盖了类属性通过类名.类属性赋值来修改类属性在__init__里写了return 值报 TypeError初始化方法不允许返回值删除return语句或者只写return表示提前结束这张表建议你收藏起来等实际写代码遇到报错时再对照着看。很多问题单看文字觉得简单但真正发生在你自己代码里时排查一两个小时都属于正常现象因为报错信息往往指向的不是问题根源。7.2 三个可以立刻上手练的类光看不练等于没学。我建议你从下面三个练习里挑一个去做做完一个就基本掌握这一节的内容了银行账户类定义BankAccount类属性有account_name、balance方法有deposit(amount)、withdraw(amount)、show_balance()。注意存款和取款时都要做金额合法性判断比如不能取超过余额。做完这个你就能体会到数据行为绑定后的整洁感。宠物类定义Pet类属性有name、species、energy方法有eat()吃东西增加体力、play()玩耍减少体力、status()输出当前状态。你可以模拟喂食、玩耍几次观察能量变化。这个练习能帮你熟悉多个方法之间的状态联动。简单图书管理类定义Book类属性有title、author、is_borrowed方法有borrow()、return_book()、display()。再把Library类也定义出来内部维护一个书籍列表支持添加书籍和查找可借书籍。这个练习稍微综合一些做完后类与类协作的感觉会非常强烈。每个练习做完后我建议你再问自己三个问题这个类的属性有哪些是每个对象独立的有哪些可以是所有对象共享的如果把某个方法改一个名字外部调用方需要跟着改吗想清楚这三个问题你对类和对象的理解就真正到位了。下一节我们会在这个基础上谈继承和多态——到时候你会发现有了这两个概念写代码的灵活度又会上升一个层次。
阅读完成 · 觉得有帮助?