Python 面向对象入门:类、实例与封装
2026/7/27 8:04:33 网站建设 项目流程

【一】从过程到对象

程序本质上由数据和操作数据的规则组成。面向过程以“先做什么、后做什么”为中心,适合步骤明确的脚本、数据转换和算法实现;面向对象则以“谁拥有状态、谁负责行为”为中心,适合存在长期状态、多个协作实体或持续演化规则的业务。

两种方式并不互斥。一个项目可以用对象表达用户、订单等领域实体,再用普通函数组织导入、统计或批处理流程。选择的依据应是代码的复杂度和变化方向,而不是追求某种写法本身。

【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, ...)保证血量不会降为负数。随着规则增加,所有调用方都需要记住hppower等键名和计算方式,维护成本会逐步上升。

【2】对象如何改善组织方式

对象把相关状态和行为放到一起。例如“攻击”应由攻击者发起,并由目标承担扣血规则;外部代码只需要发出person.attack(dog),无需重复了解具体字段和计算细节。这种把内部状态变化收敛到方法中的做法称为封装。

classCharacter:def__init__(self,name:str,hp:int,power:int):self.name=name self.hp=hp self.power=powerdefattack(self,target:"Character")->None:target.take_damage(self.power)print(f"{self.name}攻击了{target.name},剩余血量:{target.hp}")deftake_damage(self,damage:int)->None:self.hp=max(0,self.hp-damage)person=Character("张三",hp=100,power=30)dog=Character("旺财",hp=100,power=40)person.attack(dog)

代码说明:Character是类,persondog是该类创建的实例。attack()只负责攻击流程,take_damage()集中处理扣血边界。今后需要增加护甲、暴击或死亡状态时,可优先修改相关方法,而不是在所有调用处复制计算逻辑。

【二】类与对象

类是创建对象的蓝图,描述同一类对象共享的结构和行为;对象是运行时创建出的具体实例。类中定义的方法由所有实例共享,实例属性则分别保存在每个对象中。

类名通常使用大驼峰命名,例如Student;函数、方法和变量名通常使用小写加下划线,例如show_profile。类中的普通方法称为实例方法,调用时 Python 会自动将当前对象作为第一个参数传入,惯例命名为self

classStudent:defgreet(self)->None:print("你好,我是一名学生")student=Student()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):ifage<0:raiseValueError("年龄不能为负数")self.name=name self.age=age self.gender=genderdefrun(self)->None:print(f"{self.name}正在跑步")defintroduce(self)->str:returnf"我是{self.name},今年{self.age}岁"student_one=Student("dream",18,"男")student_two=Student("opp",28,"女")print(student_one.introduce())print(student_two.introduce())student_one.run()

代码说明:每次创建Student都会执行__init__,因此student_onestudent_two各自拥有独立的nameagegender。直接写self.name = name比手动修改self.__dict__更清晰,且便于在初始化时加入校验和默认值。

实例属性通常可在对象创建后修改,但关键状态不应让外部代码任意改写。简单学习阶段可以通过student.age = 19赋值;业务规则复杂后,应通过方法或property集中校验。

classAccount:def__init__(self,balance:int=0):self._balance=balance@propertydefbalance(self)->int:returnself._balancedefdeposit(self,amount:int)->None:ifamount<=0:raiseValueError("存入金额必须大于零")self._balance+=amount account=Account()account.deposit(100)print(account.balance)

代码说明:_balance表示内部使用的属性约定,balance通过@property以只读形式暴露。deposit()集中验证金额并更新余额,避免不同调用方各自修改余额而破坏规则。

【三】类属性与实例属性

类属性属于类本身,适合存放所有实例共享的值,例如默认学校名称;实例属性属于某个对象,例如姓名和年龄。读取student.school时,Python 会先找实例属性,再找类属性。若给某个实例赋同名属性,就会遮蔽类属性,而不会修改其他实例。

classStudent:school="清华大学"def__init__(self,name:str,age:int):self.name=name self.age=age dream=Student("dream",18)opp=Student("opp",28)print(dream.school,opp.school)dream.school="北京大学"print(dream.school,opp.school,Student.school)

代码说明:第一次读取时,两个对象都从类中获得school。执行dream.school = "北京大学"后,dream的实例命名空间中新增同名属性,只影响该对象;opp.schoolStudent.school仍为原值。

不要把每个实例独有的可变数据直接写成类属性,否则所有对象会共享同一个列表或字典。

classBadStudent:hobbies=[]classGoodStudent:def__init__(self):self.hobbies=[]first=BadStudent()second=BadStudent()first.hobbies.append("music")print(second.hobbies)third=GoodStudent()fourth=GoodStudent()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.name=name self.breed=breeddefbark(self)->str:returnf"{self.name}正在叫"dog=Dog("旺财","哈士奇")print(dog.__dict__)print(Dog.__dict__["species"])print(dog.bark())

代码说明:dog.__dict__显示当前实例独有的namebreed,不会包含类属性species或方法bark。属性查找会先访问实例,再访问类。直接修改__dict__虽然可行,但会绕过构造参数、校验与重构工具的检查,应只用于调试和机制学习。

【五】面向过程与面向对象的取舍

面向过程的优势是执行流程直观,适合一次性任务和数据处理;面向对象的优势是把状态和行为封装在明确边界内,适合存在多个实体、规则会不断变化的业务。对象并不是“容器越大越好”:类职责过多、继承层级太深或把无关逻辑都塞入方法,都会降低可读性。

设计类时可先回答三个问题:它代表什么实体?它维护哪些状态?它负责哪些行为?如果一个方法既处理网络请求、又做数据库写入、又修改领域状态,通常应拆分职责。简单数据对象可以使用dataclasses.dataclass减少样板代码。

fromdataclassesimportdataclass@dataclassclassPoint:x:inty:intdefdistance_from_origin(self)->float:return(self.x**2+self.y**2)**0.5point=Point(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__是直接父类元组。它们适合观察和调试;业务逻辑不应过度依赖这些内部元数据。

【七】本章小结

  1. 面向过程围绕执行步骤组织,面向对象围绕实体状态和行为组织,两者可在同一项目中配合使用。
  2. 类是对象的蓝图,实例是具体对象;self表示正在调用方法的实例。
  3. __init__用于初始化实例属性,应通过明确赋值建立对象的有效状态。
  4. 类属性由所有实例共享,实例属性属于单个对象;可变的实例数据不要直接作为类属性。
  5. 封装应把状态变化集中到合适方法中,减少外部代码对内部字段与规则的依赖。
  6. 先保证类职责清晰,再考虑property、数据类、组合或继承等抽象工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询