7.7 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/02_Classes_encapsulation.md |
02_Classes_encapsulation 总结
本文介绍 Python 中类与对象的封装方式,重点说明公共接口与内部实现的区别,以及 Python 如何通过命名约定、属性管理、property 和 __slots__ 来实现较弱但实用的封装。
核心主题
公共接口与私有实现
类的一个重要作用是封装对象的数据和内部实现细节,同时向外部提供稳定的公共接口。外部代码应通过公共接口操作对象,而不应依赖对象的内部结构。
不过,Python 的对象系统非常开放:
- 可以轻易查看对象内部属性;
- 可以随意修改对象属性;
- 没有强制性的私有成员访问控制机制。
因此,Python 的封装主要依赖程序员遵守约定,而不是语言强制执行。这与 python encapsulation 和 object oriented programming 密切相关。
私有属性约定
Python 中,以下划线 _ 开头的名称通常被视为“私有”或“内部实现细节”:
class Person(object):
def __init__(self, name):
self._name = name
这种私有性只是约定,并不阻止外部访问:
p = Person('Guido')
p._name = 'Dave'
一般规则是:变量、函数、模块名只要以下划线开头,就表示它们不属于公共接口。直接使用这类名称通常意味着代码正在依赖实现细节,应优先寻找更高层的公共功能。
简单属性的问题
普通 Python 类可以直接暴露属性:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
这种写法简单直接,但缺点是无法限制属性值类型:
s = Stock('IBM', 50, 91.1)
s.shares = 100
s.shares = "hundred"
s.shares = [1, 0, 0]
如果希望 shares 始终是整数,就需要引入某种属性管理机制。
访问器方法的问题
一种传统做法是使用 getter/setter 方法:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.set_shares(shares)
self.price = price
def get_shares(self):
return self._shares
def set_shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
self._shares = value
这种方式可以加入类型检查,但会破坏原有调用方式:
s.shares = 50
必须改为:
s.set_shares(50)
这会影响已有代码的兼容性。
property:受管理的属性
Python 提供 @property 机制,使方法可以像普通属性一样访问,同时仍能在读取或赋值时执行自定义逻辑。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
现在,普通属性访问会触发 getter/setter:
s = Stock('IBM', 50, 91.1)
s.shares # 调用 @property
s.shares = 75 # 调用 @shares.setter
重要特点:
- 外部代码仍然使用
s.shares,无需改成s.get_shares()或s.set_shares(); - 类内部的
self.shares = shares同样会触发 setter; - 实际数据通常保存在私有属性中,如
_shares; - 除 property 本身外,类中其他代码仍可以继续使用公共属性名
shares。
这一模式是 Python 中实现 managed attributes 的常见方式。
计算属性
property 也常用于把计算结果包装成属性。例如股票成本可以由 shares * price 计算得到:
class Stock:
@property
def cost(self):
return self.shares * self.price
这样调用者可以写:
s = Stock('GOOG', 100, 490.1)
s.cost
而不是:
s.cost()
这让对象接口更加统一:普通数据属性和计算属性都可以通过无括号的属性访问方式获得。
统一访问原则
如果一个对象既有数据属性,又有计算方法,接口可能显得不一致:
s.cost() # 方法
s.shares # 数据属性
使用 property 后,调用方式可以统一为:
s.cost
s.shares
这种设计隐藏了“数据是存储的还是计算的”这一实现细节,使公共接口更稳定。这一点体现了封装的核心价值:调用者不需要知道内部实现。
装饰器语法
@property 使用的是 Python 装饰器语法:
@property
def cost(self):
return self.shares * self.price
@ 表示把紧随其后的函数定义交给某个装饰器处理。这里 property 会把方法转换成属性描述符。该主题可进一步关联到 python decorators。
slots:限制属性集合
__slots__ 可以限制实例允许拥有的属性名:
class Stock:
__slots__ = ('name', '_shares', 'price')
def __init__(self, name, shares, price):
self.name = name
如果尝试设置未声明的属性,会抛出 AttributeError:
s.prices = 410.2
# AttributeError: 'Stock' object has no attribute 'prices'
__slots__ 的作用包括:
- 防止拼写错误导致意外创建新属性;
- 限制对象使用方式;
- 减少对象内存占用;
- 在某些数据结构类中提高运行效率。
不过,本文强调 __slots__ 主要是性能和内存优化工具,而不是日常封装的首选手段。多数普通类不需要使用它。该主题可关联到 python slots 和 python object memory。
练习要点
练习 5.6:简单 property
将 Stock.cost() 方法改为 cost 属性,使其调用方式从:
s.cost()
变为:
s.cost
改动后,s.cost() 将不再可用,因为 cost 已经是属性值,而不是可调用方法。相关程序如 pcost.py 也需要同步移除括号。
练习 5.7:property 与 setter
把 shares 改为受管理属性:
- 实际值存储在
_shares; - 通过
@property读取; - 通过
@shares.setter设置; - setter 中检查值必须是整数;
- 非整数赋值时抛出
TypeError。
目标行为:
s.shares = 50
s.shares = 'a lot' # TypeError
练习 5.8:添加 slots
为 Stock 添加 __slots__,验证:
- 不能随意添加新属性,如
s.blah = 42; - 使用
__slots__后,实例通常不再有普通的__dict__; - 这说明对象内部表示发生变化,内存使用更高效。
关键结论
- Python 的封装主要依赖命名约定,而非强制访问控制。
_name这类前导下划线名称表示内部实现细节,不应被外部代码直接依赖。property可以在保持属性访问语法不变的情况下加入验证、计算和封装逻辑。property有助于提供统一接口,隐藏数据是存储的还是计算的。__slots__可以限制属性集合并优化内存,但不应滥用。- 私有属性、property 和 slots 都是有特定用途的工具,大多数日常代码不需要过度使用。
相关概念
- python encapsulation
- object oriented programming
- managed attributes
- python properties
- python decorators
- python slots
- python object memory