Python 面向对象入门:类、实例与封装
【一】从过程到对象程序本质上由数据和操作数据的规则组成。面向过程以“先做什么、后做什么”为中心适合步骤明确的脚本、数据转换和算法实现面向对象则以“谁拥有状态、谁负责行为”为中心适合存在长期状态、多个协作实体或持续演化规则的业务。两种方式并不互斥。一个项目可以用对象表达用户、订单等领域实体再用普通函数组织导入、统计或批处理流程。选择的依据应是代码的复杂度和变化方向而不是追求某种写法本身。【1】字典与函数的局限在最简单的“人狗大战”示例中可以用字典保存角色状态用函数修改状态。这样的写法容易上手但当角色增多、攻击规则变复杂时字段名和修改逻辑会分散在多个位置调用方必须同时知道字典结构和函数使用顺序。person{name:张三,hp:100,power:30}dog{name:旺财,hp:100,power:40}defperson_attack_dog(attacker:dict,target:dict)-None:target[hp]max(0,target[hp]-attacker[power])print(f{attacker[name]}攻击了{target[name]}剩余血量{target[hp]})person_attack_dog(person,dog)代码说明字典保存数据外部函数负责行为。max(0, ...)保证血量不会降为负数。随着规则增加所有调用方都需要记住hp、power等键名和计算方式维护成本会逐步上升。【2】对象如何改善组织方式对象把相关状态和行为放到一起。例如“攻击”应由攻击者发起并由目标承担扣血规则外部代码只需要发出person.attack(dog)无需重复了解具体字段和计算细节。这种把内部状态变化收敛到方法中的做法称为封装。classCharacter:def__init__(self,name:str,hp:int,power:int):self.namename self.hphp self.powerpowerdefattack(self,target:Character)-None:target.take_damage(self.power)print(f{self.name}攻击了{target.name}剩余血量{target.hp})deftake_damage(self,damage:int)-None:self.hpmax(0,self.hp-damage)personCharacter(张三,hp100,power30)dogCharacter(旺财,hp100,power40)person.attack(dog)代码说明Character是类person和dog是该类创建的实例。attack()只负责攻击流程take_damage()集中处理扣血边界。今后需要增加护甲、暴击或死亡状态时可优先修改相关方法而不是在所有调用处复制计算逻辑。【二】类与对象类是创建对象的蓝图描述同一类对象共享的结构和行为对象是运行时创建出的具体实例。类中定义的方法由所有实例共享实例属性则分别保存在每个对象中。类名通常使用大驼峰命名例如Student函数、方法和变量名通常使用小写加下划线例如show_profile。类中的普通方法称为实例方法调用时 Python 会自动将当前对象作为第一个参数传入惯例命名为self。classStudent:defgreet(self)-None:print(你好我是一名学生)studentStudent()student.greet()print(isinstance(student,Student))代码说明执行Student()才会创建对象。调用student.greet()时Python 等价于调用Student.greet(student)因此方法定义中需要self调用处却不必手动传入它。isinstance()用于判断对象是否属于某个类或其子类。【1】实例属性与init不同学生的姓名、年龄、性别等数据属于实例属性。__init__是对象创建后自动调用的初始化方法适合接收构造参数并建立对象的初始有效状态。它不负责真正分配对象也不应该返回其他对象。classStudent:def__init__(self,name:str,age:int,gender:str):ifage0:raiseValueError(年龄不能为负数)self.namename self.ageage self.gendergenderdefrun(self)-None:print(f{self.name}正在跑步)defintroduce(self)-str:returnf我是{self.name}今年{self.age}岁student_oneStudent(dream,18,男)student_twoStudent(opp,28,女)print(student_one.introduce())print(student_two.introduce())student_one.run()代码说明每次创建Student都会执行__init__因此student_one与student_two各自拥有独立的name、age、gender。直接写self.name name比手动修改self.__dict__更清晰且便于在初始化时加入校验和默认值。实例属性通常可在对象创建后修改但关键状态不应让外部代码任意改写。简单学习阶段可以通过student.age 19赋值业务规则复杂后应通过方法或property集中校验。classAccount:def__init__(self,balance:int0):self._balancebalancepropertydefbalance(self)-int:returnself._balancedefdeposit(self,amount:int)-None:ifamount0:raiseValueError(存入金额必须大于零)self._balanceamount accountAccount()account.deposit(100)print(account.balance)代码说明_balance表示内部使用的属性约定balance通过property以只读形式暴露。deposit()集中验证金额并更新余额避免不同调用方各自修改余额而破坏规则。【三】类属性与实例属性类属性属于类本身适合存放所有实例共享的值例如默认学校名称实例属性属于某个对象例如姓名和年龄。读取student.school时Python 会先找实例属性再找类属性。若给某个实例赋同名属性就会遮蔽类属性而不会修改其他实例。classStudent:school清华大学def__init__(self,name:str,age:int):self.namename self.ageage dreamStudent(dream,18)oppStudent(opp,28)print(dream.school,opp.school)dream.school北京大学print(dream.school,opp.school,Student.school)代码说明第一次读取时两个对象都从类中获得school。执行dream.school 北京大学后dream的实例命名空间中新增同名属性只影响该对象opp.school和Student.school仍为原值。不要把每个实例独有的可变数据直接写成类属性否则所有对象会共享同一个列表或字典。classBadStudent:hobbies[]classGoodStudent:def__init__(self):self.hobbies[]firstBadStudent()secondBadStudent()first.hobbies.append(music)print(second.hobbies)thirdGoodStudent()fourthGoodStudent()third.hobbies.append(reading)print(fourth.hobbies)代码说明BadStudent.hobbies只创建了一次因此两个对象共享同一列表GoodStudent在每次初始化时创建新列表实例之间互不影响。这是类属性与实例属性最常见的误用之一。【四】方法、self 与对象命名空间实例方法通过self访问当前对象。self不是关键字但应遵循该命名约定它使读者清楚某个属性或方法属于当前实例。对象属性通常保存在obj.__dict__中类属性和方法通常保存在Class.__dict__中这些字典适合学习和调试不应作为日常的初始化或业务修改接口。classDog:species犬类def__init__(self,name:str,breed:str):self.namename self.breedbreeddefbark(self)-str:returnf{self.name}正在叫dogDog(旺财,哈士奇)print(dog.__dict__)print(Dog.__dict__[species])print(dog.bark())代码说明dog.__dict__显示当前实例独有的name与breed不会包含类属性species或方法bark。属性查找会先访问实例再访问类。直接修改__dict__虽然可行但会绕过构造参数、校验与重构工具的检查应只用于调试和机制学习。【五】面向过程与面向对象的取舍面向过程的优势是执行流程直观适合一次性任务和数据处理面向对象的优势是把状态和行为封装在明确边界内适合存在多个实体、规则会不断变化的业务。对象并不是“容器越大越好”类职责过多、继承层级太深或把无关逻辑都塞入方法都会降低可读性。设计类时可先回答三个问题它代表什么实体它维护哪些状态它负责哪些行为如果一个方法既处理网络请求、又做数据库写入、又修改领域状态通常应拆分职责。简单数据对象可以使用dataclasses.dataclass减少样板代码。fromdataclassesimportdataclassdataclassclassPoint:x:inty:intdefdistance_from_origin(self)-float:return(self.x**2self.y**2)**0.5pointPoint(3,4)print(point)print(point.distance_from_origin())代码说明dataclass会自动生成常用的__init__、__repr__和比较方法适合主要用于存放数据的类。领域行为仍可像普通类一样定义当初始化、验证或继承逻辑很复杂时普通类往往更清晰。【六】类的元数据与实践建议__name__、__module__、__doc__、__bases__等属性能帮助观察类的名称、定义位置、文档和父类关系常用于调试、学习与框架开发。日常业务代码判断类型时优先使用isinstance()、issubclass()理解接口时优先阅读文档、类型标注和测试。classStudent:表示一名学生。print(Student.__name__)print(Student.__module__)print(Student.__doc__)print(Student.__bases__)print(issubclass(Student,object))代码说明__name__是类名__module__是定义类的模块__doc__是文档字符串__bases__是直接父类元组。它们适合观察和调试业务逻辑不应过度依赖这些内部元数据。【七】本章小结面向过程围绕执行步骤组织面向对象围绕实体状态和行为组织两者可在同一项目中配合使用。类是对象的蓝图实例是具体对象self表示正在调用方法的实例。__init__用于初始化实例属性应通过明确赋值建立对象的有效状态。类属性由所有实例共享实例属性属于单个对象可变的实例数据不要直接作为类属性。封装应把状态变化集中到合适方法中减少外部代码对内部字段与规则的依赖。先保证类职责清晰再考虑property、数据类、组合或继承等抽象工具。