news 2026/10/16 11:08:28

Python类与对象精讲:从实例化到类属性,避开新手常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python类与对象精讲:从实例化到类属性,避开新手常见坑

1. 从一张会员卡说起:为什么编程里要有类和对象

如果你刚接触编程不久,大概率会在某个学习阶段被"类和对象"这个概念卡住。我也一样,最初看教程的时候,书上写着"类是对对象的抽象,对象是类的实例",我反复读了三遍,脑子里只有两个大字:啥啊。

后来真正让我理解类和对象区别的,反而是一次写会员积分系统的经历。当时我想实现一个简单的功能:每个会员有名字、手机号、积分,可以消费、可以累积积分。我最初用几个列表和字典硬写——一个列表存名字,一个列表存积分,一个嵌套字典按手机号索引……代码写了一半就自己都绕晕了,修改一个人的积分,得同时维护三四个地方,稍不留神就数据错乱。

那次之后我才意识到,问题的根源在于:我把"会员"这个真实世界里的实体,拆成了一堆互相没有明确归属的散装数据。而类和对象这套机制,就是帮你把"会员"应该长什么样(有哪些属性)和能做哪些事(有哪些方法)聚合在一起的东西。

简单一句话:**类是模板,对象是模板造出来的具体实例。**你不需要一开始就完全理解这句话,跟着下面的例子走一遍,你不仅能分清两者,还能知道它们在实际项目中到底承担什么职责。

2. 类和对象的本质区别:图纸与实物的距离

2.1 最直白的类比:户型图、模具与菜谱

我给学生讲类和对象的时候,最常用的类比有三个,哪个都好用,大家选自己最容易接受的。

第一个是户型图和房子的关系。户型图画好了,规定有三室两厅、有一个阳台,户型图本身不能住人,它只是描述。真正能住进去的,是照着这张图纸盖出来的那一栋栋房子。同一张户型图可以盖出无数栋房子,它们在布局上完全一样,但地址、住户、装修都不相同。这里,户型图是类,每一栋具体的房子就是对象。

第二个是模具和产品的关系。比如做一个塑料杯的模具,模具本身不是杯子,但它定义了杯子的一切特征:高度、容积、材质。工厂可以拿同一个模具批量生产几万个杯子,每个杯子的形状一样,但它们是彼此独立的个体,一个杯子被摔碎了不影响其他杯子。模具是类,每个生产出来的杯子是对象。

第三个是做菜的菜谱。鱼香肉丝菜谱上写着需要什么食材、什么调料、步骤是什么,菜谱不能吃,但照着菜谱可以做出很多盘鱼香肉丝。菜谱是类,每一盘端上桌的鱼香肉丝是对象。

这三个类比的共同点是:类负责"定义"和"描述",对象负责"存在"和"是具体的一坨东西"。类提前约定好规则,对象在运行时真正占用内存、保存数据、执行逻辑。

2.2 类的名字叫"类",但你要把它当成"模具"而不是"名单"

新手最容易在字面上被绕晕。看到"类"这个字,容易联想到"类别"或"种类",比如鸟类、鱼类,好像类是一个集合概念。这么理解也不算全错,但编程里的"类"核心不在于"分类归纳",而在于"规范制造"。

打个比方:动物园里的"鸟类"是一个分类概念,指代所有长羽毛、有翅膀的动物,它不是模板。编程里的"鸟类"类,是告诉你"如果你想制造一个鸟对象,必须具备羽毛、翅膀,并且能飞",它是一种可复制的制造规范。你可以new出好几只鸟对象,每只鸟有自己独立的体重、毛色、飞行速度。

这一点想通了,整个面向对象思想的入门就顺畅多了。对象是活的、有状态、有具体数值的;类是死的、是规则、是定义。你把规则写一次,然后可以按规则生成任意多个"活物"。

3. 动手写一写:实例化的完整过程

3.1 定义一个最简单的类

说再多都不如跑一段代码。下面我用一个会员类做例子,这是我在那次积分系统里沉淀下来的版本,已经简化过了:

class Member: """会员类:定义会员长什么样、能做什么""" def __init__(self, name, phone): self.name = name self.phone = phone self.points = 0 def add_points(self, amount): self.points += amount def consume(self, amount): if self.points >= amount: self.points -= amount return True return False

然后创建对象:

member_a = Member("张三", "13800001111") member_b = Member("李四", "13900002222") member_a.add_points(100) print(member_b.points) # 输出 0

这一小段代码里,Member是类,member_a和member_b是对象。member_a加了100积分,member_b完全不受影响。两个对象是由同一个类造出来的,但它们是独立的内存实体,各存各的数据。

3.2__init__到底做了什么,为什么必须有self

很多新手第一次看到__init__和self会懵,这两个东西几乎每个教程都出现,但真正讲清楚的不多。

__init__是Python类的构造函数(严格说是初始化方法),它负责在"对象被创建的那一刻"完成数据的初始化。member_a = Member("张三", "13800001111")这一行代码执行时,Python暗中做了几件事:先分配一块内存给新对象,然后调用__init__方法,把传入的参数绑定到对象上。

self表示"调用这个方法的那个对象本身"。member_a.add_points(100)执行时,Python会自动把这个member_a传进去,实际相当于Member.add_points(member_a, 100)。所以self.points += amount指的是"给member_a这个对象的points属性加100",而不是给Member类加,也不是给member_b加。

这个设计一开始容易觉得啰嗦:为什么每个方法都要带一个self参数?用过Java或C++的朋友可能知道,它们是用this关键字隐式表达的。Python选择显式写出self,好处是意图一目了然,坏处是新手多了个门槛。你只要记住一条铁律:定义类的方法时,第一个位置必须留给self;调用对象的方法时,永远不用手动传self,Python会自动帮你传。

3.3 创建一个对象时,内存里发生了什么

从内存角度理解对象创建,是区分类和对象的关键。类本身是一段放在代码区的定义,不占"数据空间";只有当你Member(...)调用了构造函数,Python才在堆内存里分配出一块区域,存放这个对象独有的属性值。

你可以把类理解成一张生产蓝图,蓝图放在设计室档案柜里(类定义区),不占生产车间的地。每次调用Member(),就像生产线按下启动键,照蓝图生产出一个具体产品,放到仓库里(堆内存),这个产品占用了真实的存储空间。

用Python自带的id()函数验证一下:

print(id(member_a)) print(id(member_b))

两个对象的id一定不同,因为它们住在不同的内存地址。再看看类:

print(id(Member))

类也有自己的内存地址,但它的地址和任何对象的地址都不一样。这一行代码能帮你直观地确认:类是一份独立存在的"定义",对象是照着定义造出来的"实体"。

4. 属性和方法的归属:哪些属于类,哪些属于对象

4.1 实例属性的本质是"每个对象各存一份"

先搞清楚"实例属性"这个概念。在__init__里写的self.name = name,创建的就是实例属性。所谓实例,就是对象,实例属性就是"每个对象自己独有的变量"。

这个特性是"隔离"的基础。member_a和member_b虽然都由Member类生产,但各自有独立的name、phone、points存储空间。改member_a的points不会碰member_b,因为它们根本不在同一个内存块里。

实操中我会建议新手画一张小表,把每个对象的属性列出来,这样能非常直观地看到"每个对象各存一份"的含义:

对象namephonepoints
member_a张三13800001111100
member_b李四139000022220

同一个类生产出来的对象,字段结构完全一样,但字段里的值互不相同。这就是"类定结构,对象存数据"的最直接体现。

4.2 类属性:所有对象共享的"公共公告栏"

与实例属性相对的是类属性。直接在类内部定义的变量就是类属性,它不属于任何单一对象,而属于类本身,所有对象共享这一份数据。

class Member: level_name = "普通会员" # 类属性 min_points = 0 # 类属性 def __init__(self, name, phone): self.name = name self.phone = phone self.points = 0

在这个例子里,level_name和min_points是类属性。无论你创建多少个Member对象,它们看到的level_name都会是同一个值。这就像公司大厅里的公告栏——公告栏不是某个员工的私人物品,所有人都能看到同一块公告栏上的内容。

类属性的使用场景很典型:所有对象共享的配置、默认值、常量、通用规则。比如会员等级名称、最低积分门槛、创建者的版本号等。当你需要"所有对象对同一个值保持一致"时,就该用类属性而不是实例属性。

新手最容易犯的错是:想通过对象修改类属性,结果不小心给对象加了一个同名实例属性,把原来的类属性"遮蔽"了。比如写member_a.level_name = "黄金会员",你以为改的是全局公告栏,实际只是给member_a单独贴了一张小纸条,member_b看到的仍然是"普通会员"。这一点后面排查部分会专门讲。

4.3 方法为什么总是带 self,类方法和静态方法什么时候用

实例方法必须带self,因为它需要操作"具体某个对象"的数据。member_a.add_points(100)要把100加在member_a自己的points上,如果方法拿不到self,就不知道该改谁。

但有些操作不需要修改对象状态。比如你想实现一个函数,判断某个积分值是否达到升级标准,它不关心具体哪个会员,只关心传入的数值。这种情况下,用实例方法也行,但有点小题大做;更优雅的写法是用静态方法或类方法。

class Member: upgrade_threshold = 500 @staticmethod def is_upgrade(points): return points >= 500

@staticmethod定义的方法是"不需要self的方法",内部不依赖对象的任何状态。这种操作本质上是"相关工具函数",把它放在类里是为了组织代码方便,而不是因为它需要操作对象。

那什么时候用@classmethod呢?当你希望方法能够访问类属性,或者你想实现"替代构造器"的时候。典型例子:

class Member: def __init__(self, name, phone): self.name = name self.phone = phone @classmethod def create_empty(cls, phone): name = "未命名" + phone[-4:] return cls(name, phone)

create_empty不需要某个已存在的对象,但它需要类本身(cls参数),以便内部调用Member(...)生成新对象。这种"不依赖对象、但依赖类"的方法,就用类方法。

记忆方法很简单:方法里用到self就是实例方法,用到cls就是类方法,两者都不用就是静态方法。初期你只需要掌握实例方法,类方法和静态方法等真正遇到实际需求时再学习,完全不迟。

5. 新手最容易踩的五个坑

5.1 忘了写 self

这是出现频率最高的问题。新手写类方法经常这样:

class Member: def __init__(self, name): self.name = name def hello(): # 漏了 self print("hello")

直接调用member.hello()会报类型错误,因为Python自动把member传进去作为第一个位置参数,但hello()的定义不接受任何参数,参数数量对不上。解决办法只有一条:定义方法时第一个参数永远是self,除非你明确知道自己在写静态方法。

5.2 想改类属性,结果创建了实例属性

前面提过,对对象执行member_a.level_name = "黄金会员"不会改变其他对象看到的level_name。如果你确实想改全局的共享数据,正确的做法是通过类名操作:

Member.level_name = "黄金会员"

这样改的是类属性本身,所有对象下一次访问时都会看到新值。

这里有个容易被忽略的坑:如果你先通过实例读了一次类属性,比如print(member_a.level_name),这没问题。但如果你随后通过实例去设置它,Python不会报错,只是静默地给member_a创建了一个同名实例属性。从此以后,member_a看到的level_name是自己的"黄金会员",member_b看到的还是类的"普通会员"。同一个代码里,两个对象看到的"同一概念"不同值,非常容易排查半天。

5.3 判断对象相等:==和is搞混

两个对象的属性值完全一致,它们相等吗?这取决于你的判断维度。

class Member: def __init__(self, name): self.name = name m1 = Member("张三") m2 = Member("张三") print(m1 == m2) # 默认输出 False print(m1 is m2) # 输出 False

默认情况下==比较的是内存地址,所以m1和m2不相等。如果你想按"属性值相同就算相等",需要自己在类里定义__eq__方法。而is永远只比较身份(内存地址),它不会因为值相同就返回True。

实操中,很多业务系统里判断会员是否是同一个,建议用唯一ID而不是整对象比较。这个习惯能省去很多因为默认比较规则导致的诡异bug。

5.4 可变对象作为默认参数

这个问题不单出现在类的方法里,但在类方法中出现时破坏力更大:

class Member: def __init__(self, name, tags=[]): self.name = name self.tags = tags a = Member("张三") b = Member("李四") a.tags.append("大客户") print(b.tags) # 输出 ['大客户']

这个输出结果会让新手目瞪口呆:我只是给张三打了"大客户"标签,怎么李四也带上了?原因很简单:tags=[]这个默认列表在类定义时只创建一次,所有没显式传入tags的对象,共享同一个列表对象。想避免这个坑,需要这样写:

def __init__(self, name, tags=None): self.tags = [] if tags is None else tags

用不可变对象(如None)作为默认值,在方法内部再创建新的可变对象。这是一条所有Python开发者都应该刻在脑子里的规范。

5.5 把所有东西都往类里塞

类能帮助你组织代码,但过度使用会让代码变得僵硬。我看到过一些同学把简单的业务逻辑全部塞进类,一个工具方法明明不需要对象状态,也硬要写成实例方法,调用时必须先创建一个对象。

这违背了"类和对象是为了解决问题"的本质。类和对象是工具,不是装饰品。如果一个方法不依赖任何对象状态,写成普通函数或者静态方法更清晰、也更易测试。类用来承载"有状态、有行为"的实体,纯粹的算法和逻辑,用函数就够了。

6. 实操心得:什么时候该用类,什么时候别用

6.1 三种真正适合用类的场景

第一个场景是"多个实体共享同一套结构和行为"。会员、订单、商品、学生、员工,现实中每个实体都有属性(名字、价格、学号)和行为(消费、打折、上课)。把这些实体定义为类,代码结构会非常贴合业务语言,沟通也方便。

第二个场景是"你需要封装数据与操作,防止外部随意修改"。比如积分系统里,我不希望外部代码直接member.points = 10000绕过消费逻辑。我可以在属性命名上做约定(下划线开头),或者使用属性装饰器控制访问。类提供了"把数据和行为绑定在一起"的封装能力。

第三个场景是"你希望代码可以扩展和复用"。典型的是面向对象的多态特性:一个基类Animal,子类Dog和Cat分别实现自己的speak()方法。调用方只管面对Animal接口,实际执行哪个版本由对象决定。这种扩展性在大型项目里价值巨大。

6.2 能用函数干完的活,别急着套类

我给初学者的建议非常直接:不确定要不要用类的时候,先别用。如果你只是写一个脚本,把一两个数据拼一下算一下,普通函数加几个变量完全够用。硬套类会增加代码量,还可能为了"像面向对象"而引入一堆本不需要的抽象层。

一个常见的情况是:你看到教程里一堆类,于是自己写什么都想"规范化",结果一个只有十几行的数据处理逻辑,硬包出了三个类加两个接口。这属于为了用而用,而不是因为需要而用。

正确的思路是:先感知需求,再决定方案。当你发现有多个对象需要进行相同操作、或者数据散落多处难以统一修改、或者多处代码在维护同一组相互关联的数据时,自然会产生"该抽成一个类了"的直觉。这个直觉不是一上来就有的,而是写崩溃几次之后自然长出来的。

6.3 给初学者的决策清单

我来把经验浓缩成一个可以照着判断的清单:

  • 数据是不是只有一处使用,逻辑也很简单?——用普通变量和函数,别碰类。
  • 数据要在多处来回传,经常互相配对出现?——可以考虑用类把它们绑在一起。
  • 同一套流程要对不同个体分别执行,且个体状态互不影响?——用类,每个个体就是一个对象。
  • 需要批量生产很多个同结构的实体?——用类,类就是生产模板。
  • 代码复用主要靠拷贝粘贴?——停下来,这可能是需要类或函数来抽象的信号。

这个清单不是绝对的,但它足够帮你在学习阶段做出合理判断。我见过太多例子,不是类用多了,而是在不恰当的地方强行使用类导致代码别扭、学习方法也走偏了。

6.4 一个顺手的小经验

最后分享一个小技巧:调试类和对象时,强烈建议在类里加一个__repr__方法:

def __repr__(self): return f"Member(name={self.name}, phone={self.phone}, points={self.points})"

这个方法定义了对象在打印时的显示内容。有它之后,你调试时直接print(member_a),就能看到清晰可读的信息,而不是一行看不懂的<__main__.Member object at 0x7f...>。能让你的调试体验提升不少。

根据我自己的学习经历,类这个概念真正入门,不是靠背定义,而是靠亲手把一个小项目用类和对象重写一遍,对比重构前后的代码差异。等你在实践中吃过几次苦、踩过几个坑,再看任何讲解类和对象的文章,基本一遍就通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 11:08:07

基于SSM的社区疫情管理系统设计与实现

1. 选题背景与核心需求拆解先说结论&#xff1a;基于SSM的社区疫情管理系统&#xff0c;这个选题放在计算机毕业设计里&#xff0c;属于典型的"业务场景清晰、技术栈经典、工作量适中"的项目类型。我用这个词在技术社区里搜了一圈&#xff0c;发现近几年做这个方向的…

作者头像 李华
网站建设 2026/10/16 11:08:07

YOLO-World推理延迟剖析:文本编码器才是瓶颈及优化方案

第一次跑通 YOLO-World 的时候&#xff0c;我盯着 profiler 打出来的耗时数据愣了好一会儿。YOLO 家族在实时检测圈子里口碑一直不错&#xff0c;换上"开放词汇"的名头之后&#xff0c;我本能地觉得速度也不会差太多。结果实测单帧延迟比预期高出将近一倍&#xff0c…

作者头像 李华
网站建设 2026/10/16 11:07:22

PTA 1072 Gas Station:Dijkstra最短路径与多条件排序避坑解析

1. 题目到底在说什么&#xff1a;千万别被“加油站”三个字带偏PTA 1072 Gas Station这道题&#xff0c;光看标题很容易误会成“图论里的选址问题”&#xff0c;但实际读完你会发现&#xff0c;它更像一道“披着图论外衣的逻辑判断题”。题目背景是&#xff1a;一个小区里要建加…

作者头像 李华
网站建设 2026/10/16 11:06:59

锂电池激光模切机控制系统设计:从飞行切割到张力控制全解析

锂电池激光模切机控制系统设计分享 设备开发这个圈子&#xff0c;做锂电设备的这几年应该都有体会——极片成型环节&#xff0c;机械冲切的老路越走越窄&#xff0c;激光模切变成了产线上的标配。我去年完整做了一个激光模切机的控制系统项目&#xff0c;从方案选型到现场调试…

作者头像 李华
网站建设 2026/10/16 11:06:48

TortoiseGit状态图标不显示的排查与修复全攻略

这个情况太典型了&#xff0c;不少用 TortoiseGit 的人都被它坑过&#xff1a;明明代码仓库一切正常&#xff0c;Subversion 的提交、更新也都好用&#xff0c;但资源管理器里的文件就是光秃秃的&#xff0c;没有了那个标志性的绿色对勾&#xff0c;改动过的文件也不会变红。这…

作者头像 李华
网站建设 2026/10/16 11:06:42

二叉树的锯齿形层序遍历:BFS、DFS与双栈实现全解析

刷题刷到第 150 天&#xff0c;遇到的题目开始慢慢脱离“模板能直接套”的舒适区。LeetCode 103 这道“二叉树的锯齿形层序遍历”&#xff0c;光看题目名字就很有意思&#xff1a;锯齿形——不是普通的从上到下逐层扫描&#xff0c;而是第一层从左到右、第二层从右到左、第三层…

作者头像 李华