第23课:Python|构造方法、析构方法、私有属性与封装思想落地实现

在这里插入图片描述

文章目录


📖 开篇导读

在前两节课中,我们学习了类和对象的基本概念,以及类的定义、属性查找、方法类型等进阶知识。你已经能够定义简单的类,并理解实例创建的基本流程。但要想在真实项目中写出健壮、易维护的面向对象代码,还需要掌握三个核心技能:

  1. 构造方法 __init__:如何设计灵活的初始化逻辑?如何处理参数验证?如何实现类似“重载”的效果?
  2. 析构方法 __del__:当对象被销毁时,如何自动释放外部资源(如文件句柄、网络连接、数据库连接)?
  3. 封装:如何隐藏内部数据,通过公有的方法安全地访问和修改属性?@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类方法只能操作类属性或调用类方法
9property的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


🧠 知识点思维导图总结

第23课:构造、析构、私有与封装

构造方法 init

参数默认值与验证

模拟重载

参数个数判断

类工厂方法

调用其他辅助方法

链式调用(return self)

析构方法 del

触发时机(垃圾回收)

资源释放(但不可靠)

循环引用导致延迟

推荐使用上下文管理器

私有属性与名称修饰

__双下划线:名称修饰

_单下划线:约定(受保护)

私有方法同样被修饰

并非真正私有,仅改名

封装实现

getter/setter手动方法

@property装饰器

getter

setter

deleter

计算属性(无存储)

懒加载属性(缓存)

__slots__内存优化

实战案例

银行账户类

文件资源管理

用户验证类

懒加载文档

面试考点

__init__与__new__区别

__del__风险

私有属性原理

property底层描述符


🔜 下节课预告

封装是面向对象的三大特性之一。下一节课我们将学习第二个特性:继承。通过继承,我们可以复用父类的代码,构建层次化的类结构。

第24课:继承、单继承、多继承、多层继承底层原理精讲

内容包括:

  • 单继承:子类调用父类方法,super()的使用
  • 多继承:MRO(方法解析顺序)、菱形继承问题
  • 多层继承:属性查找路径
  • isinstance()issubclass()
  • 抽象基类(ABC)初步
  • 实战:游戏角色继承体系

继承让我们能够写出更简洁、更具扩展性的代码。

🌟 学习鼓励:本课内容非常贴近实际开发,特别是@property和封装设计,是写出高质量类的关键。请务必自己动手编写几个类,尝试用property控制属性访问,体会封装带来的安全感。下一节课的继承会更加精彩!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值