ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python面向对象编程入门:类、对象与封装继承多态实战

Python面向对象编程入门:类、对象与封装继承多态实战 1.1 用函数写业务走到哪一步会卡住很多人学Python的顺序是这样的先学会print再学会变量然后循环、判断、函数一路过关斩将觉得“我也能写点东西了”。但等到想做稍微像样一点的项目时比如管理一个班级的成绩、写一个小游戏、做一套简单的订单系统突然发现自己写的函数越来越乱参数越来越多数据到处传改一个地方崩三个地方。这时候你大概率会听别人说一句话“你应该用面向对象了。”很多人对这句话的抵触本质上是心里有个疑问函数明明也能写为什么非得用类我可以直接给你一个场景假设你要管理三个学生的姓名和成绩用函数写数据大概率长这样。student1_name 小明 student1_score 85 student2_name 小红 student2_score 92 student3_name 小刚 student3_score 78如果你写一个计算平均分的函数就得把这些参数全传进去def get_average(s1_name, s1_score, s2_name, s2_score, s3_name, s3_score): return (s1_score s2_score s3_score) / 3这还只是三个学生。如果是三十个呢这时代码可读性会断崖式下跌因为你每次调用函数都要记住“第几个参数代表谁的成绩”这种情况在编程里有一个专门的叫法——数据与操作数据的行为是割裂的。而面向对象做的事情翻译成大白话很简单把相关的数据和操作这些数据的函数捆绑到一个容器里这个容器就是对象。1.2 类和对象一张图纸盖出一千栋房子先别急着写代码把这个概念彻底想明白后面能少走很多弯路。假设你要批量盖100栋一模一样的房子你肯定不会一砖一瓦盖100遍而是先找设计师画一张图纸。图纸上标注了房子的面积、格局、门的位置、窗户的数量。但图纸不是房子它不能住人。工人按照图纸一栋一栋把房子盖出来每一栋都是独立的、能住人的实体。在Python里类就是那张图纸对象就是按照图纸盖出来的房子。拿刚开始那个学生例子来说你写的不是一个一个的学生变量而是先定义一张“学生图纸”class Student: def __init__(self, name, score): self.name name self.score score然后再按图纸批量“盖房子”s1 Student(小明, 85) s2 Student(小红, 92) s3 Student(小刚, 78)s1、s2、s3都是Student这个类创建出来的对象也叫实例。它们各自拥有自己的name和score互不干扰。这背后最大的好处不是代码变短了而是代码结构变了你不再关心第几个参数是谁你只需要说s1.name、s2.score一切都清晰直白。这套“图纸–房子”的思维方式是整个面向对象的基石。后面所有看似复杂的概念什么封装、继承、多态本质上都是在想办法让图纸更合理、让盖房过程更省事。所以这一节值得你多读两遍把“类模板、对象实例”这个对应关系刻在脑子里再往下走就顺了。2. 从零写出第一个类把“数据”和“行为”绑在一起2.1 class、def、self这三个词到底连起来做了什么现在动手写一个最简单的类。我建议你直接跟着敲一遍不要只是复制粘贴因为手敲的过程本身就是在建立肌肉记忆。假设我们要设计一个游戏里的小兵它有名字、血量、攻击力三个属性还能释放一个攻击动作。用class写出来长这样class Soldier: def __init__(self, name, hp, attack_power): self.name name self.hp hp self.attack_power attack_power def attack(self, target): target.hp - self.attack_power print(f{self.name} 攻击了 {target.name}造成 {self.attack_power} 点伤害{target.name} 剩余 {target.hp} 点血量)然后这样调用soldier_a Soldier(步兵甲, 100, 15) soldier_b Soldier(步兵乙, 80, 20) soldier_a.attack(soldier_b)运行结果步兵甲 攻击了 步兵乙造成 15 点伤害步兵乙 剩余 65 点血量我来拆解一下这段代码。class Soldier是定义一张名叫Soldier的图纸。def __init__是图纸里的一个特殊函数名字叫构造方法Python在创建对象时自动调用它参数name、hp、attack_power就是盖房子时你告诉工人的“这栋房子窗户多大、门朝哪开”。self是这一节最容易懵的点我直接给你一个最朴素的解释self代表“这栋被盖出来的房子自己”。当soldier_a.attack(soldier_b)被调用时Python内部偷偷做了翻译把第一参数换成了调用者本身等价于Soldier.attack(soldier_a, soldier_b)。所以self.name拿到的是soldier_a自己的nameself.hp是soldier_a自己的hp。没有self函数就不知道该操作哪个对象的“房子”代码就没法区分是谁在攻击、谁在挨打。提示新手最常犯的错误之一就是定义方法时忘记写self。一旦你调用对象的方法Python会报“TypeError: attack() takes 2 positional arguments but 3 were given”。看到这个错误不用慌第一反应永远是去检查方法定义里有没有抠掉self。有了这个类之后你再创建一百个小兵每个对象都有自己的血量、名字和攻击力彼此独立。你想给一个小兵回血也不会影响其他小兵。这种“数据隔离”是函数式写法非常难做到的但面向对象天生就支持。2.2 __init__构造方法对象出生时自动执行的第一步__init__这个魔法方法值得单独讲透因为很多人虽然会用但并不知道它精确的执行时机。创建一个对象经历了两个阶段第一阶段调用__new__方法分配一块内存空间相当于给你圈了一块空地第二阶段调用__init__方法在空地上做初始化装修给对象填上初始数据。日常开发中几乎不需要碰__new__但__init__是每天都在用的。注意__init__的第一个参数依然必须是self因为这步“装修”是对某个具体实例做的不是给整个类做的。还有一点容易被忽略__init__方法可以有默认参数也可以没有参数。比如class Config: def __init__(self, debugFalse, levelINFO): self.debug debug self.level level config1 Config() config2 Config(debugTrue, levelDEBUG)这种写法对于“配置项”类的对象特别实用因为大部分配置项都有默认值你只需要在特殊场景下覆盖默认值。另外__init__并不是类的必须组成部分。如果你只是需要一个简单的数据容器不初始化任何特殊数据完全可以不写__init__Python会调用一个默认的空构造方法。但绝大多数类都需要初始化自己的数据所以这个魔法方法几乎成了每个类的标配。实操心得我见过不少初学朋友在__init__里放一堆复杂的业务逻辑比如连数据库、发网络请求。这不是不行但我建议初始化函数只做数据准备的活。原因只有一个调试体验极差。你每次创建一个对象都要承担那些副作用一旦中间某一步失败你根本分不清是数据有问题还是初始化流程有问题。3. 封装别让外部随便动我的数据3.1 为什么需要封装面向对象三大特性封装、继承、多态。先讲封装因为它是你从“会用类”到“会设计类”的第一道分水岭。封装这个词听起来抽象我用一个生活场景解释你去银行取钱背后的账目怎么计算、系统怎么校验、钞票怎么清点你完全不需要知道。你只需要走到柜台或ATM前告诉它“我要取500块”。你不可能直接跑进金库自己拿钱那样整个系统就崩了。封装做的是同一件事把内部数据藏起来对外只暴露几个操作入口。还是用银行例子写代码。设想一个简单的银行账户类class BankAccount: def __init__(self, owner, balance): self.owner owner self.balance balance这样写有什么问题问题大了。外部代码可以直接这样改account BankAccount(小明, 1000) account.balance 99999999一个余额明明只有1000块的账户被外部一只手强行改成接近一个亿这在真实业务里是不可接受的。问题不在于Python运行出错而在于它允许你做逻辑上违规的事。封装要解决的就是这个数据不应当被外部随意修改所有改动必须经过行为方法校验。改造后的设计应该是class BankAccount: def __init__(self, owner, balance): self.owner owner self.__balance balance def deposit(self, amount): if amount 0: print(存款金额必须大于0) return self.__balance amount print(f存入 {amount} 元余额 {self.__balance} 元) def withdraw(self, amount): if amount self.__balance: print(余额不足) return self.__balance - amount print(f取出 {amount} 元余额 {self.__balance} 元) def get_balance(self): return self.__balance现在余额被藏在__balance这个私有属性里外部代码再也无法用一行赋值直接改数据。你想存钱走deposit想取钱走withdraw。这两步操作里都有规则校验金额不能乱填余额不足不会扣钱。3.2 下划线的“潜规则”和property装饰器在上面的代码里我用了双下划线__balance。你要清楚一个事实Python并没有真正意义上“私有”的概念双下划线只是一种约定Python解释器会自动把属性名“改名”成_BankAccount__balance。这叫做名称修饰目的是防止不小心从外部直接访问到它。你依然可以用account._BankAccount__balance强行拿到它但没人会这么干大家都会遵守约定。如果你不想让类外部直接随意修改数据但允许外部读取数据应该怎么做传统做法是写get_balance方法但每次都要调用方法稍显繁琐。Python更优雅的做法是用property装饰器把方法伪装成属性。class BankAccount: def __init__(self, owner, balance): self.owner owner self.__balance balance property def balance(self): return self.__balance这样外部代码可以这样写account BankAccount(小明, 1000) print(account.balance) # 1000像读取属性一样自然 account.balance 500 # 直接报错AttributeError属性只读这个设计的好处在于未来如果你想调整余额的计算逻辑比如把单位从元变为分你不必改动任何外部调用代码只需要在property方法内部做换算即可。这就是封装带来的最大收益内部怎么变外部无感知。注意很多教程会把“私有属性”写成单个下划线_balance这只是一种程序员之间的默契表示“这是内部数据外部别碰”解释器不会做任何强制处理。双下划线则是有语法层面改名的。两者都算不上真正的私有重点是你和你的团队是否遵守约定。4. 继承和多态让代码像乐高一样可组合4.1 继承把公共逻辑抽到父类学会封装之后你会发现不同类之间经常有重复的代码。比如游戏里战士有名字、血量、攻击力法师也有相同的属性战士能攻击法师也能攻击。如果两个类各写一遍代码冗余不说将来要加一个“暴击”功能得改两个地方很不方便。继承的解决思路是把公共的属性和方法抽到一个父类里然后子类通过关键字class后面的括号继承父类。class Character: def __init__(self, name, hp, attack_power): self.name name self.hp hp self.attack_power attack_power def attack(self, target): target.hp - self.attack_power print(f{self.name} 攻击了 {target.name}造成 {self.attack_power} 点伤害{target.name} 剩余 {target.hp} 点血量) class Warrior(Character): def shout(self): print(f{self.name} 发出一声震天怒吼所有敌人攻击力下降) class Mage(Character): def cast_spell(self, target): target.hp - self.attack_power * 2 print(f{self.name} 施放法术对 {target.name} 造成 {self.attack_power * 2} 点伤害)现在Warrior和Mage都自动拥有了name、hp、attack_power以及attack方法。代码少写了功能还齐了。更妙的是如果后续想给所有角色增加一个逃跑方法只需要在Character里加一个run方法所有子类立刻可用。继承还有一层问题是很多新手会踩的子类的构造方法。如果你在子类里定义了__init__父类的__init__不会自动执行。你需要在子类里调用super().__init__(...)来把公共属性初始化好。class Warrior(Character): def __init__(self, name, hp, attack_power, armor): super().__init__(name, hp, attack_power) self.armor armor第一个参数armor是战士特有的护甲值而name、hp、attack_power这些公共属性就交给父类的__init__去处理。这样既避免了重复代码又保证了每个类都能扩展自己的专属数据。实操心得我见过很多初学者为了用继承而继承比如“猫是一个动物所以应该继承动物汽车是一个交通工具所以应该继承交通工具”。但有一段话我至今很受用继承的正确动机是“代码复用”和“行为一致性”。如果你只是想让子类恰好有几个一样的属性你可以用组合把另一个类的实例作为自己的属性如果你希望多个类共享一套逻辑并对同一方法产生不同表现再考虑继承。4.2 多态同一句话每个对象有自己说法多态本来是面向对象里比较难讲透的概念但它其实可以浓缩成一句话同一个动作不同的对象执行时表现出不同的行为。还是用游戏里的角色。Animal类定义一个make_sound方法Dog和Cat各自继承并覆写这个方法的实现class Animal: def __init__(self, name): self.name name def make_sound(self): raise NotImplementedError(子类必须实现 make_sound 方法) class Dog(Animal): def make_sound(self): return f{self.name} 叫了一声汪汪 class Cat(Animal): def make_sound(self): return f{self.name} 叫了一声喵喵然后写一个函数它不管传入的对象具体是狗还是猫只关心这个对象有没有make_sound方法def animal_concert(animals): for animal in animals: print(animal.make_sound()) animals [Dog(旺财), Cat(咪咪), Dog(大黄)] animal_concert(animals)运行结果旺财 叫了一声汪汪 咪咪 叫了一声喵喵 大黄 叫了一声汪汪调用方完全不知道也不关心每个对象是Dog还是Cat它只知道“你既然出现在这个列表里就应该具备make_sound方法”。这就是多态的价值它让代码的可扩展性大大提高。未来你再加入一个Sheep类只要实现了make_sound方法animal_concert函数一行都不用改新功能就自动接入了。这也是为什么面向对象在有大量交互逻辑的项目中那么重要因为它让上层逻辑和具体实现解耦了。上层只依赖“接口”或“约定”不依赖具体类改起来自然就不慌。5. 一个完整的小项目图书借阅系统里的三个类5.1 拆解场景哪些东西适合做成类前面讲了概念现在把前面所有东西串起来做一个完整、能跑的小项目。我们做一个简化版的图书借阅系统需求只有三条图书可以新增、成员可以借书、成员可以还书。拿到需求别急着写代码先问自己场景里有哪些“名词”图书、成员这是两个肯定要变成类的对象。借书、还书是行为行为属于谁借书这个动作涉及两个对象图书要检查“我在不在”成员要记录“我借了哪些书”所以把借书行为放在Member里调用Book对象的方法来协作。我的类设计如下Book类书名、作者、是否被借出、当前借阅者Member类姓名、借书卡号、已经借走的书列表Member调用Book的borrow和return_book方法完成交互5.2 完整代码与运行演示class Book: def __init__(self, title, author): self.title title self.author author self.is_available True self.borrower None def borrow(self, member): if not self.is_available: print(f《{self.title}》已被借走暂时不能借) return False self.is_available False self.borrower member.name print(f{member.name} 成功借走《{self.title}》) return True def return_book(self): if self.is_available: print(f《{self.title}》没有被借出无需归还) return False print(f{self.borrower} 归还了《{self.title}》) self.is_available True self.borrower None return True class Member: def __init__(self, name, card_id): self.name name self.card_id card_id self.borrowed_books [] def borrow_book(self, book): if len(self.borrowed_books) 2: print(f{self.name} 已借满2本不能再借) return if book.borrow(self): self.borrowed_books.append(book) def return_book(self, book): if book in self.borrowed_books: book.return_book() self.borrowed_books.remove(book) else: print(f{self.name} 没有借过《{book.title}》不能归还)测试代码book1 Book(三体, 刘慈欣) book2 Book(活着, 余华) member1 Member(小明, M001) member2 Member(小红, M002) member1.borrow_book(book1) # 小明借三体 member1.borrow_book(book2) # 小明借活着 member2.borrow_book(book1) # 小红想借三体失败 member1.return_book(book1) # 小明还三体 member2.borrow_book(book1) # 小红借三体成功把这段代码跑起来输出应该是小明 成功借走《三体》 小明 成功借走《活着》 《三体》已被借走暂时不能借 小明 归还了《三体》 小红 成功借走《三体》仔细读一遍这段代码你会发现几个之前学的知识点全部被用上了__init__在初始化数据self贯穿所有方法Book类的borrow方法内部做了状态判断来实现数据保护两个类之间通过方法调用协作完成业务链路。这才是面向对象真正发挥作用的样子不是花哨的语法而是让复杂的业务规则被清楚地拆分到各自应该待的地方。提示这个项目里有个细节值得留意borrow_book返回之前我把book.append到成员列表之前先判断了图书是否可借。很多新手会把顺序写反先append再调book.borrow。如果borrow失败列表里就多了一本没有借成功的书状态就乱了。正确的思路是先确认核心操作成功再更新依赖它的数据。6. 面向对象新手避坑手册与学习建议6.1 高频报错定位与解决方案写面向对象代码前几个月我几乎每天都要和下面几个错误打交道这里整理成一张速查表你碰到时直接对照查找。报错信息原因解决方案TypeError: __init__() missing 1 required positional argument创建对象时少传了一个参数检查类构造方法定义和创建时的参数个数TypeError: xxx() takes 1 positional argument but 2 were given定义方法时忘了写self方法定义第一参数必须加selfAttributeError: Student object has no attribute name对象没有这个属性大概率构造函数里没有定义检查__init__里是否初始化了该属性NameError: name self is not defined定义方法时漏掉了self参数或者在方法外用self给方法补上self参数类属性被某个实例修改后其他实例也跟着变了把可变数据定义成了类属性确认数据属于实例在__init__里用self定义最后这一行是隐藏大坑值得展开说。变量分为类属性和实例属性两种。如果这样写class Dog: tricks []那么所有Dog对象共享同一个列表任何一只狗往里面加技能所有狗的技能列表都会跟着变。因为这行代码把列表绑在了类上而不是每个实例上。正确的做法是把tricks定义在__init__里class Dog: def __init__(self): self.tricks []这样每只狗都有自己的技能列表互不干扰。判断原则很简单凡是和数据特征相关、每个对象可能不同的东西一律定义在实例上。6.2 什么情况真的需要写类什么情况不用最后聊一个很多人想问但不好意思问的问题学完面向对象之后是不是所有代码都要往类上凑当然不是。判断标准很朴素如果你的代码只是把几个函数组合起来处理一份数据全程只用一次那用函数完全够。如果你发现同一组数据要在多个函数里传来传去或者你要创建的对象的“模板”有很多份比如多个学生、多个订单、多个玩家这时候把数据和操作绑成一个类收益才开始变大。我个人的经验是新手阶段不要为了面向对象而面向对象。你完全可以在一个小脚本里继续用函数但在每一个项目练习中尝试把一个场景用类实现一遍感受组织方式的变化。今天这个图书借阅系统就是一个很好的练手项目你可以给它加一个Library类统一管理所有藏书再加一个统计热门图书的功能。一步步把小的模块拼成大的系统面向对象的价值会在这个过程中越来越明显。实操心得我最初学面向对象的时候也卡在“类里访问另一个类的数据”这个概念上很长时间。后来我想了一个笨办法把每个类都想象成一家独立的小店类和类之间要互相调用你得通过“店门口”的方法进去而不是直接翻窗户拿东西。这种比喻虽然不严谨但帮我度过了最难熬的抽象阶段。编程的天花板很多时候不是语法而是你脑子里能不能建立起一套“模块如何协作”的图景。希望这篇文章就是你的那块垫脚石。
返回列表