
文章目录
📖 开篇导读
在前两节课中,我们学习了类和对象的基本概念,以及类的定义、属性查找、方法类型等进阶知识。你已经能够定义简单的类,并理解实例创建的基本流程。但要想在真实项目中写出健壮、易维护的面向对象代码,还需要掌握三个核心技能:
- 构造方法
__init__:如何设计灵活的初始化逻辑?如何处理参数验证?如何实现类似“重载”的效果? - 析构方法
__del__:当对象被销毁时,如何自动释放外部资源(如文件句柄、网络连接、数据库连接)? - 封装:如何隐藏内部数据,通过公有的方法安全地访问和修改属性?
@property装饰器如何让方法像属性一样调用,同时保持控制?
💡 工作场景:在实际开发中,构造方法用于初始化对象状态,经常会包含参数验证、默认值设置、依赖注入等;析构方法虽然不常用,但在封装底层资源(如C扩展、文件句柄)时非常重要;封装则是面向对象设计的基石,良好的封装能降低模块间的耦合,提高代码安全性。
本课将深入讲解构造方法与析构方法的细节,并通过大量案例演示封装思想的各种落地技巧。学完本课,你将能够设计出安全、易用、健壮的类。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 掌握__init__的高级用法:默认值、参数验证、调用其他方法、模拟重载 | 写出灵活健壮的构造函数 |
| 2️⃣ | 理解__del__析构方法的作用、触发时机及注意事项 | 管理外部资源,避免资源泄露 |
| 3️⃣ | 深入理解Python中的私有属性(名称修饰机制)和访问控制 | 面试常问“Python如何实现私有” |
| 4️⃣ | 掌握封装的最佳实践:getter/setter、@property、计算属性、懒加载 | 写出优雅的API |
| 5️⃣ | 了解__slots__在封装中的内存优化作用 | 性能优化场景 |
| 6️⃣ | 通过实战案例:安全的用户类、资源管理类、银行账户类 | 落地封装思想 |
🔥 面试考点:“
__init__和__new__的区别?”“__del__在什么时候被调用?有什么风险?”“Python中如何实现真正的私有属性?”“@property装饰器的作用和原理?”
📚 知识点理论精讲
一、构造方法 __init__ 的进阶用法
1.1 参数默认值与验证
构造方法可以设置默认参数,并在内部对传入的值进行验证。
class Person:
def __init__(self, name, age=0):
self.name = name
if age < 0:
raise ValueError("年龄不能为负数")
self.age = age
1.2 模拟构造方法重载
Python不支持多个同名方法,但可以通过*args、**kwargs或类方法来实现多种构造方式。
方式一:使用isinstance判断参数类型
class Point:
def __init__(self, x, y=None):
if y is None:
# 假设传入的是元组或列表
self.x, self.y = x[0], x[1]
else:
self.x = x
self.y = y
方式二:使用类方法(更清晰)
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
1.3 构造方法中调用其他方法
为了保持__init__简洁,可以调用内部辅助方法。
class Circle:
def __init__(self, radius):
self.radius = radius
self._validate()
self._calculate_area()
def _validate(self):
if self.radius <= 0:
raise ValueError("半径必须为正数")
def _calculate_area(self):
self.area = 3.14159 * self.radius ** 2
1.4 链式调用构造
有时希望构造后能立即链式调用方法,可以在__init__中返回实例(但实际上__init__必须返回None)。可以通过类方法实现。
class Query:
def __init__(self, table):
self.table = table
self._conditions = []
def where(self, condition):
self._conditions.append(condition)
return self # 返回自身实现链式
def execute(self):
return f"SELECT * FROM {self.table} WHERE {' AND '.join(self._conditions)}"
# 使用
q = Query("users").where("age > 18").where("city = '北京'")
print(q.execute())
二、析构方法 __del__
2.1 __del__ 的基本用法
__del__在对象被垃圾回收时调用,用于释放外部资源(如关闭文件、断开网络连接)。
class FileHandler:
def __init__(self, filename):
self.file = open(filename, 'r')
def __del__(self):
print(f"析构 {self.file.name} 文件")
self.file.close()
f = FileHandler("test.txt")
del f # 立即调用 __del__
2.2 注意事项和陷阱
__del__的调用时机不确定:由垃圾回收器决定,程序退出时也可能调用。- 循环引用会导致对象无法被及时回收(Python的垃圾回收器最终会清理循环引用,但
__del__可能被延迟甚至不被调用)。 - 如果
__del__中引用了全局变量,且全局变量已经被销毁,可能引发异常。 - 最佳实践:不要依赖
__del__来管理资源,应该使用上下文管理器(with语句)或在明确的时间点调用关闭方法。
# 推荐的资源管理方式
class Resource:
def __enter__(self):
print("获取资源")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("释放资源")
return False
with Resource() as r:
pass # 自动释放
三、私有属性与名称修饰
3.1 名称修饰(Name Mangling)
Python中没有真正的私有属性。添加双下划线前缀__会触发名称修饰,变成_类名__属性名,达到一定程度的“隐藏”。
class Account:
def __init__(self, balance):
self.__balance = balance # 私有属性
def get_balance(self):
return self.__balance
acc = Account(1000)
# print(acc.__balance) # AttributeError
print(acc._Account__balance) # 1000(仍然可以访问,但不推荐)
3.2 单下划线 _ 的约定
单下划线开头的属性(如_balance)是“受保护的”,属于编程约定,告诉其他开发者“不要直接访问”,但解释器不强制。
class Person:
def __init__(self, name):
self._name = name # 受保护属性
3.3 私有方法
同样,双下划线开头的方法也会被名称修饰,防止被子类覆盖。
class Base:
def __public(self):
print("私有方法")
def call_private(self):
self.__public()
class Derived(Base):
def __public(self):
print("子类的私有方法")
d = Derived()
d.call_private() # 调用的是父类的私有方法
3.4 使用场景建议
- 双下划线:用于防止子类意外覆盖,或表示该属性/方法是类的内部实现细节,外部不应依赖。
- 单下划线:用于“内部使用”的约定,但仍可能被子类或外部访问。
- 公开属性:大部分属性应该公开,通过
@property控制访问。
四、封装思想落地:@property 与 计算属性
4.1 经典的getter/setter模式
在Java/C++中,字段通常私有,通过get/set方法访问。Python更简洁。
class Student:
def __init__(self, name, score):
self.name = name
self._score = score
def get_score(self):
return self._score
def set_score(self, value):
if 0 <= value <= 100:
self._score = value
else:
raise ValueError("成绩必须在0-100之间")
4.2 @property 装饰器
Python提供了@property,让方法像属性一样调用,同时保留验证逻辑。
class Student:
def __init__(self, name, score):
self.name = name
self._score = score
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if 0 <= value <= 100:
self._score = value
else:
raise ValueError("成绩无效")
@score.deleter
def score(self):
print("删除成绩")
del self._score
s = Student("张三", 85)
print(s.score) # 85(像属性一样)
s.score = 95
# del s.score # 调用deleter
4.3 计算属性
没有实际存储,通过其他属性动态计算得出。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
@property
def perimeter(self):
return 2 * (self.width + self.height)
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
# r.area = 100 # AttributeError: can't set attribute
4.4 懒加载属性
缓存昂贵的计算结果,第一次访问时计算,之后直接返回缓存值。
class DataLoader:
def __init__(self, filepath):
self.filepath = filepath
self._data = None
@property
def data(self):
if self._data is None:
print("加载数据...")
with open(self.filepath) as f:
self._data = f.read()
return self._data
loader = DataLoader("large.txt")
# 第一次访问才真正加载
print(len(loader.data))
4.5 使用__slots__优化封装
当类需要大量实例时,__slots__可以节省内存,并限制动态添加属性,增强封装。
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # AttributeError
五、综合实战:封装思想落地
下面通过几个完整案例展示如何在实际类中应用构造、析构、私有属性和封装。
5.1 安全的银行账户类
class BankAccount:
"""银行账户,演示封装、验证、只读属性"""
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # 私有属性
self.__transactions = [] # 交易记录
@property
def balance(self):
"""只读余额(通过getter)"""
return self.__balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须为正")
self.__balance += amount
self.__transactions.append(f"存款 +{amount}")
return True
def withdraw(self, amount):
if amount <= 0:
raise ValueError("取款金额必须为正")
if amount > self.__balance:
raise ValueError("余额不足")
self.__balance -= amount
self.__transactions.append(f"取款 -{amount}")
return True
@property
def transactions(self):
"""返回交易记录的副本,防止外部修改"""
return self.__transactions.copy()
def __str__(self):
return f"账户[{self.owner}] 余额: {self.balance}"
5.2 文件资源管理器(演示析构)
class ManagedFile:
"""使用析构方法自动关闭文件,但更推荐使用上下文管理器"""
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
def open(self):
self.file = open(self.filename, self.mode)
return self.file
def close(self):
if self.file:
self.file.close()
print(f"关闭文件 {self.filename}")
def __del__(self):
# 保险措施:如果忘记关闭,析构时关闭
self.close()
但更推荐使用上下文管理器:
class Resource:
def __enter__(self):
print("获取资源")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("释放资源")
return False # 不抑制异常
with Resource() as r:
pass
5.3 带有验证的用户类(使用property)
class User:
def __init__(self, username, email):
self.username = username
self._email = email
self._password_hash = None
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if '@' not in value:
raise ValueError("无效的邮箱地址")
self._email = value
@property
def username(self):
return self._username
@username.setter
def username(self, value):
if len(value) < 3:
raise ValueError("用户名至少3个字符")
self._username = value
def set_password(self, password):
# 模拟哈希
self._password_hash = hash(password)
def check_password(self, password):
return hash(password) == self._password_hash
5.4 懒加载属性实战
import hashlib
class Document:
def __init__(self, content):
self._content = content
self._checksum = None
@property
def content(self):
return self._content
@property
def checksum(self):
"""懒加载计算checksum,只在第一次访问时计算,后续缓存"""
if self._checksum is None:
print("计算checksum...")
self._checksum = hashlib.md5(self._content.encode()).hexdigest()
return self._checksum
@property
def size(self):
return len(self._content)
doc = Document("Hello World" * 1000)
print(doc.size) # 立即返回
print(doc.checksum) # 第一次触发计算
print(doc.checksum) # 第二次使用缓存
💻 代码案例实操
案例1:构造方法模拟重载的多种方式
"""
constructor_overload.py
演示实现多种构造方式
"""
class Rectangle:
def __init__(self, width, height=None):
if height is None: # 单个参数认为是正方形
self.width = width
self.height = width
else:
self.width = width
self.height = height
@classmethod
def from_tuple(cls, tup):
return cls(tup[0], tup[1])
@classmethod
def square(cls, side):
return cls(side, side)
def area(self):
return self.width * self.height
r1 = Rectangle(4, 5)
r2 = Rectangle(6) # 正方形
r3 = Rectangle.from_tuple((2, 3))
r4 = Rectangle.square(7)
print(r1.area(), r2.area(), r3.area(), r4.area())
案例2:__del__的循环引用陷阱
"""
del_cycle.py
演示循环引用导致__del__延迟或不被调用
"""
import gc
class Parent:
def __init__(self, name):
self.name = name
self.child = None
def __del__(self):
print(f"Parent {self.name} 被销毁")
class Child:
def __init__(self, name):
self.name = name
self.parent = None
def __del__(self):
print(f"Child {self.name} 被销毁")
# 创建循环引用
p = Parent("P1")
c = Child("C1")
p.child = c
c.parent = p
# 删除外部引用
del p
del c
# 循环引用导致无法回收,__del__不会被调用
# 手动垃圾回收可以回收,但__del__可能仍不执行
gc.collect()
案例3:封装与property实现只读属性及验证
"""
property_validation.py
使用property实现年龄验证和只读ID
"""
import uuid
class Person:
def __init__(self, name, age):
self.name = name
self._age = age
self._id = str(uuid.uuid4()) # 只读属性
@property
def id(self):
return self._id
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError("年龄必须在0-150之间")
self._age = value
@property
def adult(self):
"""计算属性:是否成年"""
return self._age >= 18
p = Person("张三", 20)
print(p.id)
print(p.adult) # True
p.age = 25
# p.id = "new" # AttributeError: can't set attribute
案例4:懒加载属性实现缓存
"""
lazy_property.py
使用property实现懒加载缓存
"""
import time
class ExpensiveResource:
def __init__(self, data_id):
self.data_id = data_id
self._processed_data = None
def _load_data(self):
"""模拟耗时加载"""
print(f"正在加载数据 {self.data_id}...")
time.sleep(2) # 模拟延迟
return f"Processed data for {self.data_id}"
@property
def data(self):
if self._processed_data is None:
self._processed_data = self._load_data()
return self._processed_data
# 使用
resource = ExpensiveResource(100)
print("对象已创建,尚未加载数据")
print(resource.data) # 第一次访问,加载
print(resource.data) # 第二次,直接返回缓存
案例5:银行账户系统(完整封装)
"""
bank_system.py
完整的银行账户类,包含交易记录、利息计算等
"""
import random
class BankAccount:
interest_rate = 0.02 # 类变量:利率2%
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.__account_no = self._generate_account_no()
self.__balance = initial_balance
self.__transactions = []
self._record_transaction("开户", initial_balance, initial_balance)
def _generate_account_no(self):
return str(random.randint(10000000, 99999999))
def _record_transaction(self, desc, amount, new_balance):
from datetime import datetime
self.__transactions.append({
'time': datetime.now(),
'desc': desc,
'amount': amount,
'balance': new_balance
})
@property
def account_no(self):
return self.__account_no
@property
def balance(self):
return self.__balance
@property
def transactions(self):
# 返回副本,防止外部修改
return self.__transactions.copy()
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须为正")
self.__balance += amount
self._record_transaction("存款", amount, self.__balance)
return True
def withdraw(self, amount):
if amount <= 0:
raise ValueError("取款金额必须为正")
if amount > self.__balance:
raise ValueError("余额不足")
self.__balance -= amount
self._record_transaction("取款", -amount, self.__balance)
return True
def transfer(self, target, amount):
"""转账到另一个账户"""
self.withdraw(amount)
target.deposit(amount)
self._record_transaction(f"转出给{target.owner}", -amount, self.__balance)
target._record_transaction(f"转入自{self.owner}", amount, target.balance)
return True
def add_interest(self):
"""增加利息"""
interest = self.__balance * self.interest_rate
self.deposit(interest)
self._record_transaction("利息", interest, self.__balance)
def __str__(self):
return f"账户[{self.owner}][{self.__account_no}] 余额: {self.balance:.2f}"
# 使用
acc1 = BankAccount("张三", 1000)
acc2 = BankAccount("李四", 500)
acc1.deposit(200)
acc1.withdraw(150)
acc1.transfer(acc2, 300)
acc1.add_interest()
print(acc1)
print(acc2)
案例6:使用__slots__优化内存与属性控制
"""
slots_optimize.py
对比__slots__对内存的影响,同时限制属性
"""
class WithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# 动态添加属性测试
wos = WithoutSlots(1, 2)
wos.z = 3 # 允许
ws = WithSlots(1, 2)
try:
ws.z = 3
except AttributeError as e:
print(e) # 'WithSlots' object has no attribute 'z'
# 查看内存占用(粗略)
import sys
print(f"WithoutSlots 实例大小: {sys.getsizeof(wos)} bytes")
print(f"WithSlots 实例大小: {sys.getsizeof(ws)} bytes")
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 在__init__中返回非None值 | TypeError | 构造方法必须返回None |
| 2 | 依赖__del__及时释放资源 | 资源可能长时间占用 | 使用上下文管理器with |
| 3 | 在__del__中访问全局变量 | 变量可能已被销毁,引发异常 | 避免在__del__中使用全局资源 |
| 4 | 误用双下划线试图隐藏属性 | 实际仍可通过_类名__属性访问 | 理解名称修饰只是“改名字”,不是真正的私有 |
| 5 | 在@property.setter中无限递归 | 如self.score = value调用自身 | setter中应操作_score私有变量 |
| 6 | 子类使用__slots__但父类未定义 | 子类仍有__dict__,未优化 | 父类也定义__slots__,或接受未优化 |
| 7 | 在__init__中调用会覆盖子类的方法 | 可能导致初始化未按预期进行 | 明确哪些方法是留给子类重写的 |
| 8 | 混淆类方法和实例方法,在类方法中访问实例属性 | 错误或AttributeError | 类方法只能操作类属性或调用类方法 |
| 9 | 在property的getter中修改状态 | 违反“读取不应该有副作用”原则 | getter应只返回值,修改操作放到方法中 |
| 10 | 大量使用@property导致性能下降 | 每次访问都有函数调用开销 | 可考虑缓存结果(懒加载) |
📝 课后实战练习题
第1题:__init__设计
设计一个Book类,包含标题、作者、出版年份。在构造方法中验证年份不能晚于当前年份,标题不能为空。提供类方法from_dict(cls, dict)从字典创建对象。
第2题:资源管理器(使用__del__和with)
实现一个DatabaseConnection类,模拟数据库连接,包含connect()和disconnect()方法。在__del__中自动断开连接。同时实现上下文管理器协议,使其支持with语句。
第3题:私有属性与property
定义一个Temperature类,私有属性_celsius。通过@property提供celsius(可读写)和fahrenheit(只读,自动转换)。当设置celsius时,范围限制在-273.15到1000之间。
第4题:懒加载属性
创建一个LazyImage类,初始化时只保存图片路径,不实际加载。提供data属性,第一次访问时读取图片二进制数据并缓存,第二次直接返回缓存。模拟读取使用open(filepath, 'rb').read()。
第5题:银行账户扩展
在银行账户案例基础上,增加:冻结账户功能(frozen标志),当账户冻结时,任何取款、转账、存款操作都应抛出AccountFrozenError自定义异常。使用私有属性存储冻结状态,通过freeze()和unfreeze()方法控制。
第6题:__slots__实验
定义一个Particle类(粒子系统),包含x, y, vx, vy四个属性。分别用普通类和__slots__类创建100万个实例,比较内存占用(可使用tracemalloc模块)和实例化速度。写出结论。
第7题:构造器链式调用
实现一个QueryBuilder类,支持链式调用:qb = QueryBuilder('users').select('name', 'age').where('age > 18').order_by('age'),最后调用execute()生成SQL字符串。使用__init__初始化表名,方法返回self。
🧠 知识点思维导图总结
🔜 下节课预告
封装是面向对象的三大特性之一。下一节课我们将学习第二个特性:继承。通过继承,我们可以复用父类的代码,构建层次化的类结构。
第24课:继承、单继承、多继承、多层继承底层原理精讲
内容包括:
- 单继承:子类调用父类方法,
super()的使用 - 多继承:MRO(方法解析顺序)、菱形继承问题
- 多层继承:属性查找路径
isinstance()和issubclass()- 抽象基类(ABC)初步
- 实战:游戏角色继承体系
继承让我们能够写出更简洁、更具扩展性的代码。
🌟 学习鼓励:本课内容非常贴近实际开发,特别是
@property和封装设计,是写出高质量类的关键。请务必自己动手编写几个类,尝试用property控制属性访问,体会封装带来的安全感。下一节课的继承会更加精彩!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

862

被折叠的 条评论
为什么被折叠?



