目录
[1. 封装概念](#1. 封装概念)
[2. 权限控制](#2. 权限控制)
[3. Python 的三种访问权限](#3. Python 的三种访问权限)
[4. Getter 与 Setter](#4. Getter 与 Setter)
[5. @property 装饰器](#5. @property 装饰器)
[1. 常见魔法方法](#1. 常见魔法方法)
[2. 使用案例](#2. 使用案例)
[三、object 类](#三、object 类)
[1. 为什么所有类都继承 object](#1. 为什么所有类都继承 object)
[2. object 提供的方法](#2. object 提供的方法)
[3. 查看继承关系](#3. 查看继承关系)
[1. 多态的概念与价值](#1. 多态的概念与价值)
[2. 标准多态与鸭子多态](#2. 标准多态与鸭子多态)
[2.1 标准多态](#2.1 标准多态)
[2.2 鸭子类型](#2.2 鸭子类型)
[3. isinstance() 与多态](#3. isinstance() 与多态)
[4. 多态案例](#4. 多态案例)
[1. abc 模块与 @abstractmethod](#1. abc 模块与 @abstractmethod)
[2. 抽象类案例](#2. 抽象类案例)
[六、Python 对象与内存分析](#六、Python 对象与内存分析)
[1. 变量的本质](#1. 变量的本质)
[2. 可变对象与不可变对象](#2. 可变对象与不可变对象)
[3. 自定义类对象的内存表示](#3. 自定义类对象的内存表示)
[4. 引用传递分析](#4. 引用传递分析)
一、封装与权限控制
在掌握了类、对象以及继承的基本机制后,我们已经能够利用面向对象编程对现实世界中的实体进行初步的建模。然而,要构建出具备健壮性、可维护性和高扩展性的软件系统,还必须理解并运用面向对象的三大核心之二------封装与多态 ,以及用于规范系统架构的抽象类
本篇博客将作为上一章内容的延续,讲解这些设计在 Python 中的底层实现与内存机制
1. 封装概念
封装是面向对象编程的第一大核心原则。它的核心定义包含两个层面:
-
将实体的属性与方法结合在一起,封装成一个不可分割的独立单元(即类)
-
尽可能隐藏对象的内部实现细节,仅对外暴露有限的、安全的接口供外部系统调用
从系统架构的角度来看,封装建立了清晰的边界。对象的外部使用者无需关心对象内部的计算逻辑与数据结构是如何组织的,只需通过公开的接口与对象进行交互。这极大地降低了系统的模块间耦合度
2. 权限控制
在没有访问权限控制的系统里,类的所有内部属性都是完全公开的。外部代码可以任意读取甚至修改这些属性。这种高度的开放性会引入严重的系统隐患:
-
破坏内部数据:例如,一个表示 "银行账户" 的对象,其 "余额" 属性绝对不能为负数。如果外部代码可以直接执行 account.balance = -1000,就会直接导致业务逻辑崩溃
-
暴露实现细节:如果外部代码深度依赖类内部的某个临时变量,一旦后续为了优化性能而修改了该内部变量的名称或类型,所有外部调用代码都将发生连锁崩溃
因此,权限控制的核心目的在于保护对象内部状态的完整性 ,并实现接口与实现的分离
3. Python 的三种访问权限
与其他主流面向对象语言(如 Java、C++)通过 public、protected、private 关键字进行硬性物理限制不同,Python 采用了一种基于命名约定与名字修饰的软性限制机制
Python 官方规约了三种级别的访问权限:
| 权限级别 | 语法特征 | 外部访问表现 | 语义与设计意图 |
|---|---|---|---|
| 公开 (Public) | 无下划线前缀,如 name | 完全可访问 | 对外公开的标准服务接口或状态属性 |
| 受保护 (Protected) | 单下划线前缀,如 _age | 允许访问,但不推荐 | 内部API提示。警告外部开发者该属性不属于公开接口,仅允许在当前类和子类内部使用 |
| 私有 (Private) | 双下划线前缀,如__balance | 无法直接访问 | 严格隐藏的内部数据。防止外部或子类意外破坏 |
名字修饰机制
当 Python 解释器在加载类定义时,一旦发现某个属性或方法以双下划线 __ 开头(且结尾没有双下划线),它就会在内部自动将该名称改写为:_类名__属性名
python
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # 公开属性
self._risk_level = "High" # 受保护属性
self.__balance = balance # 私有属性
acc = BankAccount("Alice", 5000)
# 1. 验证公开与受保护属性的访问
print(acc.owner) # 正常输出:"Alice"
print(acc._risk_level) # 正常输出:"High"(但 IDE 会发出警告)
# 2. 尝试直接访问私有属性
# print(acc.__balance) # 错误!抛出 AttributeError
# 3. 查看 __dict__ 字典
print(acc.__dict__)
# 4. 通过修饰后的名称强制访问(极端工程环境下使用,不推荐)
print(acc._BankAccount__balance) # 正常输出:5000

Python 的私有属性并不是绝对安全。名字修饰机制的初衷是为了防止子类在继承时不小心覆盖了父类的内部属性(避免命名冲突),而不是为了防止外部恶意读取
4. Getter 与 Setter
为了在隐藏属性的同时允许外部安全地与数据交互,软件工程的经典做法是提供 Getter(获取器) 和 Setter(修改器) 方法。这样可以将属性的读写转换为方法调用,从而在方法内部嵌入业务校验逻辑
python
class BankAccount:
def __init__(self, balance):
self._balance = balance
# Getter 方法:提供读取渠道
def get_balance():
return self.__balance
# Setter 方法:提供带校验的写入渠道
def set_balance(balance):
if not isinstance(balance, (int, float)):
raise TypeError("交易金额必须为数值类型。")
if amount < 0:
raise ValueError("账户余额不能为负数。")
self.__balance = amount
5. @property 装饰器
虽然 Getter 和 Setter 解决了安全性问题,但在代码调用时,原本直观的 obj.balance = 100 变成了 obj.set_balance(100),这破坏了属性访问的简洁
Python 提供了 @property 装饰器。它允许开发者将类中的方法伪装成普通的属性来进行读取和赋值,同时保持底层校验逻辑的完整性
python
class SmartDevice:
def __init__(self, temperature):
# 内部存储实际数据的私有属性
self.__temperature = temperature
# 1. 定义 Getter:使方法可以像普通属性一样被读取
@property
def temperature(self):
"""返回当前设备的摄氏温度"""
# 可以在此处加入读取审计日志
return self.__temperature
# 2. 定义 Setter:接管属性赋值行为
@temperature.setter
def temperature(self, value):
if not isinstance(value, (int, float)):
raise TypeError("温度必须为数值。")
if not (-40 <= value <= 85):
raise ValueError("温度超出设备工作范围")
self.__temperature = value
调用侧代码
python
device = SmartDevice(25)
# 读取属性:底层隐式自动调用了被 @property 装饰的 temperature 方法
print(device.temperature) # 输出:25
# 修改属性:底层隐式自动调用了 @temperature.setter 装饰的方法
device.temperature = 30 # 成功赋值
二、魔法方法
在 Python 中,内置的数据类型展现出了高度的语法一致性。例如,我们可以对列表使用 len() 函数获取长度,或者使用 + 进行拼接。Python 允许自定义类通过实现一组特殊的内置方法,来融入这一套内建的行为体系。这些方法以双下划线开头和结尾,被称为魔法方法
魔法方法的核心设计哲学是协议。Python 的语言特性并不强制依赖严格的接口继承,而是依赖对象是否实现了特定的协议。只要自定义类实现了相应的魔法方法,Python 解释器就会在特定的运行时上下文中隐式地调用它们
1. 常见魔法方法
对象文本表示:str 与 repr
这两个方法都用于定义对象的字符串转换行为,但它们的设计面向和应用场景存在本质差异:
-
**str(self):**执行 print(obj) 或 str(obj) 时被触发。其返回值应当是可读性高、描述清晰的文本,侧重于展现对象核心的业务状态
-
repr(self):用于调试环境。当在终端直接输入对象名、使用 repr(obj),或者在容器中包含该对象时被触发。其返回值应当具有精确性与无歧义性,标准的规范是返回一段 "能够据此重新创建该对象" 的合法 Python 代码字符串(例如 ClassName(param1, param2))
容器与执行行为:len 与 call
-
len(self): 定义对象的容器属性。当执行内置函数 len(obj) 时被调用。该方法必须返回一个大于或等于零的整数。实现该方法后,对象在进行布尔值隐式转换时(如 if obj:),若长度为 0 则会被判定为 False
-
call(self, *args, kwargs): 使对象实例具备可调用性。实现该方法后,对象可以像普通函数一样,通过在实例名后加小括号 obj() 的形式直接执行。这在设计状态机、装饰器类或闭包替代方案时具有极高的价值
2. 使用案例
下面设计一个用于数据科学或工程计算的 "数据样本批次(DataBatch)" 管理类:
python
class DataBatch:
"""模拟深度学习或数据分析中的样本批次容器"""
def __init__(self, batch_id, samples):
self.batch_id = batch_id
self.samples = samples # 预期传入一个列表
self.is_processed = False
# 1. 实现 __str__:交付用户可视化文本
def __str__(self):
status = "已处理" if self.is_processed else "未处理"
return f"【数据批次】编号: {self.batch_id} | 状态: {status} | 包含样本数: {len(self.samples)}"
# 2. 实现 __repr__:交付精确的调试文本
def __repr__(self):
return f"DataBatch({self.batch_id}, {self.samples})"
# 3. 实现 __len__:对接系统标准长度查询
def __len__(self):
return len(self.samples)
# 4. 实现 __call__:使对象具备流水线处理器的行为
def __call__(self, processor_name):
print(f"正在使用处理器 [{processor_name}] 动态处理批次 {self.batch_id}...")
self.is_processed = True
return [str(sample).upper() for sample in self.samples]
运行实例
python
# 初始化一个批次对象
raw_data = ["sample_a", "sample_b", "sample_c"]
batch = DataBatch(101, raw_data)
# 测试一:调用 len() 函数
print(f"当前批次内部元素数量:{len(batch)}") # 输出:3
# 测试二:调用 print() 触发 __str__
print(batch)
# 测试三:触发 __repr__(在列表等容器中嵌套展现)
container = [batch]
print(container)
# 测试四:像函数一样直接调用对象,触发 __call__
processed_result = batch("NLP-Transformer")
print(f"处理后结果:{processed_result}")
print(batch)

尽管 Python 提供了大量的魔法方法,但在实际开发中不应过度滥用。只有当自定义类在语义上确实具备某种特定协议的特征时(例如,该类本质上确实是一个容器或一个执行单元),才应当去重写对应的魔法方法,以避免降低代码的可读性
三、object 类
当我们定义一个完全没有声明任何父类的孤立的类时,为什么它依然能够拥有 dir、hash 等一系列内置方法?
这是因为 Python 引入了统一类型系统的设计哲学。在 Python 3 中,所有类都隐式或显式地继承自一个祖先类------object 类。它是整个 Python 对象世界得以建立的根基
1. 为什么所有类都继承 object
在早期的 Python 2 版本中,存在 "旧式类(Classic Classes)" 与 "新式类(New-style Classes)" 的割裂。自 Python 3 开始,所有类被强制统一为新式类
隐式继承 object 带来了两项根本性的工程优势:
-
统一对象模型:无论是用户自定义的复杂业务类,还是系统内置类型(如 int、str、list),在类的谱系图上最终都汇聚于 object。这意味着在底层的 CPython 解释器中,所有实体都可以被视为一个通用的指针类型(PyObject*),极大地简化了语言运行时的架构设计
-
交付最小行为子集:object 类在内存空间分配、垃圾回收标记、基础属性检索等底层行为上,为一切派生出来的子类筑牢了底座
2. object 提供的方法
当我们声明一个空类时,该类就已经从 object 祖先继承了二十多个魔术方法。以下列举在后续架构设计中经常需要重写的几个高频底层方法:
| 内置魔术方法 | object 提供的默认底层行为逻辑 |
|---|---|
| new(cls, *args, **kwargs) | 真正的构造方法。负责开辟物理空间并返回未初始化的对象。通常不需要重写 |
| init(self, *args, **kwargs) | 初始化方法。在 object 中默认是一个空实现,供子类去覆盖注入初始状态 |
| eq(self, other) | 判定对象是否相等。默认的底层逻辑是比较两者的内存地址 |
| hash(self) | 计算对象的哈希值。默认基于内存地址计算,使得所有自定义对象默认都是可哈希的(可以存入字典的键或集合中) |
| dir(self) | 返回一个列表,列出当前对象内部挂载的所有合法的属性名与方法名(供系统 dir () 函数调用) |
3. 查看继承关系
我们可以通过读取类的特殊魔术属性 bases,在运行期查看某个类的父类谱系
python
class aloneClass:
"""没有任何显式继承的自定义类"""
pass
class SubClass(StandaloneCustomClass):
"""单重继承的子类"""
pass
# 1. 验证隐式直系父类
print(aloneClass.__bases__)
# 输出:(<class 'object'>,)
# 2. 验证多层继承链的顶端指向
print(SubClass.__bases__)
# 输出:(<class '__main__.aloneClass'>,)
# 3. 顺着 MRO 继承链追溯
print(SubClass.mro())

从输出的 MRO 链条可以清晰地看到:无论继承的层级有多深、网络有多复杂,任何检索路径的终点站,永远都是 <class 'object'>
object 类的存在不仅解释了魔术方法的来源,也为面向对象编程的下一大特性------多态奠定了物理基础。正是因为所有的类都汇聚于 object 轴心,Python 才能在函数调用和多态分发时,安全地假设 "传入的任何变量,都至少具备 object 所交付的那一套核心行为"
四、多态
封装与继承主要解决了代码的安全性 与复用性 。而多态 则专注于解决系统的灵活性 与可扩展性 。多态字面意思是 "多种形态",在计算机科学中,它指同一行为接口在面对不同的底层对象时,能够展现出截然不同的执行结果
1. 多态的概念与价值
多态的核心设计哲学是:将 "做什么" 与 "怎么做" 彻底分离
在软件开发中,如果没有多态,当我们需要处理一系列不同但相关的实体时,代码中必然充斥着大量的分支判断语句。每当系统中新增一种类型,开发者都必须深入核心调用逻辑中修改分支代码。这严重违背了开闭原则
引入多态后,调用方只需要面向一个抽象的、通用的行为契约进行编程,而不需要关心当前正在执行的对象究竟属于哪一个具体的子类。这种将类型判定推迟到程序运行期的机制,被称为动态绑定。它极大地降低了系统模块之间的耦合度
2. 标准多态与鸭子多态
Python 作为一门动态弱类型语言,在实现多态时表现出了与其他静态语言(如 Java、C++)截然不同的特性。在 Python 中,多态并存着两种实现:标准多态 与鸭子类型(Duck Typing)
2.1 标准多态
标准多态严格依赖类的继承拓扑结构
- 物理机制:子类继承自同一个父类,并各自重写了父类的某个通用方法。当高层函数接收一个父类对象参数时,传入任何一个子类对象都能被安全接收。在运行时,解释器会利用 MRO 机制动态分发,调用对应子类重写后的方法
2.2 鸭子类型
在 Python 的世界里,类型的界定是非常宽松的。Python 创始人提出了著名的 "鸭子类型" 哲学:
如果一只鸟走起来像鸭子,叫起来也像鸭子,那么它就可以被当成鸭子来看待
- 物理机制 :鸭子类型完全不要求多个类之间存在任何显式的继承关系 。只要这些类在设计上拥有同名的方法签名(即具备相同的行为接口),它们就可以在同一个高层函数中被多态性地调用。Python 关注的是对象 "能做什么",而不是对象 "是什么类型"
标准多态与鸭子多态的对比:
| 标准多态 | 鸭子类型 | |
|---|---|---|
| 继承依赖 | 强依赖,必须拥有共同的父类基根 | 完全无依赖,类与类之间逻辑独立 |
| 类型校验 | 属于强类型的显式契约(显式声明) | 属于行为强度的隐式契约(行为表现) |
| 模块耦合度 | 较高(子类与父类存在物理血缘绑定) | 极低(仅靠方法命名维系) |
| 应用场景 | 统一大类别的核心业务架构设计 | 插件系统、轻量级组件、解耦工具类 |
3. isinstance() 与多态
在上一章中我们了解到,isinstance() 可以安全地在运行期检测对象的类型。然而在面向多态的设计语境下,过度使用 isinstance()往往是设计严重退化的代码坏味道(Code Smell)
python
# 错误的多态实践:通过硬编码类型检查来实现分发
def execute_payment(payment, amount):
# 这种写法直接割裂了鸭子类型的灵活性
if isinstance(payment, WeChatPay):
payment.pay(amount)
elif isinstance(payment, AliPay):
payment.pay(amount)
正确的设计规范:
多态的精髓在于信任对象的行为。高层函数应当直接调用约定好的接口方法(如 payment.pay(amount)),而将类型判断交给 Python 运行时的动态分发机制。只有在必须确保严格物理类型的极端场景下,才应当动用 isinstance()
4. 多态案例
下面通过构建一个 "多渠道支付结算(PaymentCenter)" 案例,将标准多态与鸭子类型组合在一起进行展示
python
# 1. 构建统一基类
class BasePaymentGateway:
"""标准支付网关基类"""
def process_payment(self, amount):
# 交付一个空实现,或抛出异常强制子类覆盖
raise NotImplementedError("子类必须重写此方法。")
class WeChatPay(BasePaymentGateway):
def process_payment(self, amount):
print(f"【微信支付】成功受理,在线扣款 {amount} 元。")
class AliPay(BasePaymentGateway):
def process_payment(self, amount):
print(f"【支付宝】成功受理,账户划转 {amount} 元。")
# 2. 构建鸭子类型体系:独立的第三方积分兑换类
class PointVoucher:
"""无继承关系的类,但拥有相同的方法签名 process_payment"""
def __init__(self, voucher_code):
self.voucher_code = voucher_code
def process_payment(self, amount):
print(f"【代金券中心】使用 {self.voucher_code} 成功抵扣 {amount} 积分。")
# 3. 高层调用
def checkout_funnel(payment, order):
"""
结算流水线函数。
只要传入的对象拥有 process_payment 方法,即可参与多态分发。
"""
print("--- 启动结算安全审计 ---")
# 直接调用,不进行任何 isinstance() 拦截
payment.process_payment(order)
print("--- 结算完成,更新订单状态 ---\n")
# 4. 运行验证
wechat_tool = WeChatPay()
alipay_tool = AliPay()
point_tool = ThirdPartyPointVoucher("VOUCH-2026-99X")
# 验证一:标准多态实例调用
checkout_funnel(wechat_tool, 199.0)
checkout_funnel(alipay_tool, 299.0)
# 验证二:鸭子类型实例调用(尽管它没有继承 BasePaymentGateway,依然运转)
checkout_funnel(point_tool, 50.0)

通过此案例可以清晰地看到,上层函数 checkout_funnel 表现出了极高的通用性。未来即使系统需要接入全新的支付方式,只要新定义的类中包含了 process_payment 方法,该函数不需要做任何一行代码的修改,即可完美支持新业务的上线
五、抽象类与接口
在强类型语言(如 Java、Go)中,存在一个独立的 interface(接口)概念。接口不包含任何实质性的业务逻辑代码,它纯粹是一份行为契约,规定了实现该接口的类必须长成什么样
Python 虽然在语法层面没有提供 interface 关键字,但它通过抽象基类完美实现了这一接口思想
接口思想的核心价值在于:顶层架构师只负责制定规范,底层的业务开发人员负责填充细节。 这种设计能够确保整个系统的拼插式组件在接口级别保持高度的确定性
1. abc 模块与 @abstractmethod
Python 通过内置的 abc 模块来声明抽象类。构建一个标准抽象类需要依赖两个核心组件:
-
**abc.ABC 类:**作为抽象基类的隐式或显式父类
-
@abstractmethod 装饰器 :用于标记某个方法为抽象方法。抽象方法在抽象类中通常不需要有实质性代码,它唯一的目的是强制要求所有子类必须对其进行重写
python
from abc import ABC, abstractmethod
# 声明一个数据渲染的抽象基类
class DataRenderer(ABC):
@abstractmethod
def render_header(self):
"""强制子类必须实现:渲染页头"""
pass
@abstractmethod
def render_body(self, data):
"""强制子类必须实现:渲染数据主体"""
pass
抽象类与普通类的区别
抽象类与普通类的区别主要体现在以下两个角度:
| 普通类 | 抽象类 | |
|---|---|---|
| 实例化 | 可以直接在内存中创建对象 | 绝对禁止。如果尝试实例化包含抽象方法的类,Python 会在运行时直接抛出TypeError |
| 对子类的约束力 | 弱约束。子类继承后可以自由选择是否重写父类的方法 | 强约束。子类如果没有百分之百实现父类所有的 @abstractmethod,该子类同样无法被实例化 |
2. 抽象类案例
下面通过一个 "云存储服务适配器" 的案例,演示抽象类是如何在运行期强行勒令子类规范自身行为的:
python
from abc import ABC, abstractmethod
class CloudStorageAdapter(ABC):
"""
架构层:云存储公共接口规约
任何对接第三方云存储的驱动,都必须完整实现以下两个核心方法
"""
@abstractmethod
def upload_file(self, local_path, remote_path):
pass
@abstractmethod
def download_file(self, remote_path, local_path):
pass
# 子类实现
class AWSStorageAdapter(CloudStorageAdapter):
def upload_file(self, local_path, remote_path):
print(f"【AWS S3驱动】正在将 {local_path} 物理流上传")
def download_file(self, remote_path, local_path):
print(f"【AWS S3驱动】成功从远端拉取资源到本地 {local_path}。")
# 遗漏了某些抽象方法
class AlibabaCloudStorageAdapter(CloudStorageAdapter):
def upload_file(self, local_path, remote_path):
print(f"【阿里云OSS驱动】数据同步中...")
当你执行 AlibabaCloudStorageAdapter() 时,Python 不会立刻创建对象,而是先沿着这整个类的 MRO 检查:所有继承来的抽象方法是不是都已经被实现了。如果还有任何一个抽象方法没有实现,那么 Python 会认为这个类仍然是抽象类,于是在对象创建的第一步(new 阶段)就直接拒绝创建对象,并抛出 TypeError
六、Python 对象与内存分析
我们编写的代码最终都要落实到内存空间中。深刻理解 Python 自定义对象在内存中的具体表现形式,是解决系统内存泄漏、优化运行时性能的先决条件
1. 变量的本质
在许多传统的编译型语言(如 C 语言)中,声明一个变量 int a = 10 意味着在内存栈区中直接开辟了一块 4 字节的物理空间,并将数值 10 存进这块空间
但在 Python 的宇宙中,情况发生了根本性变化:变量本身绝对不存储任何实质性的数据,变量仅仅是一个指向具体对象物理内存首地址的引用指针
当我们执行语句 user = Student("001", "Alice") 时,内存做了以下两步操作:
-
Python 会在堆内存中开辟一块物理空间,用来创建并存放 Student 对象
-
随后,在当前的命名空间(栈区)中创建一个名为 user 的变量,并将这个堆内存对象的内存首地址赋值给 user 变量
2. 可变对象与不可变对象
根据对象所对应的堆内存数据在创建后是否允许被就地修改,Python 将对象划分为了两种:
不可变对象
-
包含类型:int、float、str、tuple、bool
-
物理特征:一旦该对象在堆内存中开辟,其包含的值就绝对不允许被更改
-
内存行为 :如果尝试对一个存储不可变对象的变量进行修改,Python 并不会去擦除原有内存中的数据,而是在堆内存中重新开辟空间创建一个新对象,并将变量的指针指向这个新对象。原有对象如果没有其他变量引用,则沦为内存垃圾直接回收

可变对象
-
包含类型 :list、dict、set 以及所有用户自定义的类对象实例
-
物理特征:对象创建后,允许直接在原有的物理空间上对内部的数据进行增、删、改操作
-
内存行为 :当我们修改一个自定义对象的实例属性(如执行 user.math = 90)时,变量 user 的指针的指向没有发生任何改变。改变的仅仅是该首地址内部所映射的数据

3. 自定义类对象的内存表示
前文提到,Python 实例对象的属性是通过 dict 字典进行底层管理的。现在,我们可以从物理内存的角度来理解这一机制:
python
class Node:
def __init__(self, val):
self.val = val
n1 = Node(100)
n2 = Node(200)
在堆内存中,n1 和 n2 各自拥有一块独立的结构体物理空间。这两块空间内部各包含一个指向各自 dict 哈希表的指针
-
当我们调用 n1.val 时,Python 解释器先根据变量 n1 找到其堆内存首地址,接着顺着该地址内部的字典指针,去对应的哈希表中检索出键名为 "val"
-
每一个自定义对象的属性读取,在底层都经历了一次 "通过指针进行哈希索引" 的动态查找过程。这就是 Python 面向对象高度动态与灵活的底层代价

4. 引用传递分析
由于变量仅仅是内存地址的引用,因此在将一个对象作为参数传递给函数,或者将其赋值给另一个新变量时,发生的是引用传递 。也就是说,多个变量在底层可能共享同一块物理内存
以下这段代码展示了在缺乏内存意识的情况下,由于引用共享而引发的灾难性后果:
python
class Student:
def __init__(self, name, math = 0):
self.name = name
# 错误操作示范
ref_a = Student("小刚")
ref_a.math = 80
# 程序员以为复制了一个新的备份
ref_b = ref_a
# 尝试只修改第二个备份的分数
ref_b.math = 99
# 检测第一个原始对象的分数
print(f"原始对象 A 的数学成绩:{ref_a.math}")
# 输出结果:原始对象 A 的数学成绩:99

底层原因:
赋值语句 ref_b = ref_a 并不会在堆内存中物理复制出一份全新的学生档案,它只是将 ref_a 变量内部存储的内存首地址拷贝给了 ref_b
此时,在内存中 ref_a 和 ref_b 两个指针同时指向了堆内存中同一个 Student 对象实例。因此,通过任何一个指针修改内部属性,都会导致两个变量的结果同步发生改变

总结
本章围绕 Python 面向对象编程的进阶内容展开,学习了封装、权限控制、多态、抽象类以及对象在内存中的存储方式。通过这些知识,我们不仅能够编写功能正确的类,还能够设计出结构更加清晰、扩展性更好的程序。同时,理解 Python 中变量保存的是对象引用,以及可变对象与不可变对象的区别,也为后续学习深拷贝、浅拷贝和内存管理等内容打下了基础
下一篇文章我们将继续学习 Python 函数的进阶用法,深入理解为什么 "函数也是对象",以及函数赋值、作为参数、作为返回值等高级特性,进一步体会 Python 作为一门高阶编程语言的灵活性
