一、基本概念(官方定义)
在Python中,变量和方法名前的下划线有特定含义,这属于Python的命名约定(naming convention)和名称修饰(name mangling)机制:
官方定义:
- 单下划线前缀
_var:表示该标识符是"非公开API"的一部分(implementation detail) - 双下划线前缀
__var:触发Python的名称修饰(name mangling)机制,将其转换为_ClassName__var
这些机制在PEP 8(Python风格指南)和Python语言参考中有明确定义。
二、通俗易懂的类比
想象你在整理一间办公室:
- 公开文件:放在桌面上,任何人都可以查阅
- 内部文件 (
_):放在标有"内部使用"的文件夹里,但没有锁 - 机密 文件 (
__):放在保险柜中并改名存档,需要特殊途径才能找到
三、实际代码演示
单下划线_:建议性的私有
class BankAccount:
def __init__(self, account_number, balance):
# 公开属性:账号,可以被任何地方访问
self.account_number = account_number
# 受保护属性:余额,按约定不应直接访问,但技术上可以
self._balance = balance
# 公开方法:显示账户信息,返回格式化的账户字符串
def display_info(self):
return f"Account: {self.account_number}, Balance: ${self._balance}"
# 受保护方法:计算利息,内部使用,不建议外部直接调用
def _calculate_interest(self, rate):
return self._balance * rate
# 创建账户实例
my_account = BankAccount("6217001234", 1000)
# 外部访问
print(my_account.account_number) # 正常访问:6217001234
print(my_account._balance) # 技术上可以访问,但违反约定:1000
print(my_account._calculate_interest(0.03)) # 技术上可以调用,但不符合约定:30.0
技术解析:
- 单下划线前缀在Python中没有特殊语法意义
- 它是纯粹的命名约定,表示"这是内部实现细节"
- Python解释器不会对其做任何特殊处理
dir()函数可以看到这些属性:'_balance','_calculate_interest'
双下划线__:名称修饰机制
class UserAccount:
def __init__(self, username, password):
# 公开属性:用户名,可以从任何地方访问
self.username = username
# 受保护属性:邮箱,按约定不应直接访问
self._email = None
# 私有属性:密码,会被Python名称修饰机制改名
self.__password = password
# 公开方法:验证密码,外部接口用于检查密码是否匹配
def verify_password(self, input_password):
return self.__password == input_password # 内部可正常访问私有属性
# 私有方法:重置密码,只能由类内部调用,外部无法直接访问
def __reset_password(self):
self.__password = "default_password"
return True
# 创建用户实例
user = UserAccount("zhang_san", "abc123")
# 尝试外部访问
print(user.username) # 正常访问:zhang_san
# 这两行会引发AttributeError
try:
print(user.__password) # 错误:'UserAccount' object has no attribute '__password'
user.__reset_password() # 错误:'UserAccount' object has no attribute '__reset_password'
except AttributeError as e:
print(f"Error: {e}")
# 但可以通过修饰后的名称访问
print(user._UserAccount__password) # 可以访问:abc123
技术解析:
- 双下划线触发Python的名称修饰(name mangling)机制
- 解释器将
__attr自动转换为_ClassName__attr - 这不是为了安全,而是为了避免子类中的命名冲突
dir()函数会显示修饰后的名称:'_UserAccount__password','_UserAccount__reset_password'
四、继承中的区别(关键点)
这是理解单下划线和双下划线最关键的场景:
class Parent:
def __init__(self):
# 受保护变量:子类可以访问和覆盖
self._protected = "Protected variable from parent"
# 私有变量:会被名称修饰,子类无法直接访问或覆盖
self.__private = "Private variable from parent"
# 受保护方法:子类可以访问和覆盖
def _protected_method(self):
return "Protected method from parent"
# 私有方法:会被名称修饰,子类无法直接访问或覆盖
def __private_method(self):
return "Private method from parent"
class Child(Parent):
def __init__(self):
super().__init__()
# 覆盖父类的受保护变量
self._protected = "Protected variable overridden by child"
# 创建子类自己的私有变量,不会覆盖父类的私有变量
self.__private = "Private variable of child (won't override parent's)"
# 覆盖父类的受保护方法
def _protected_method(self):
return "Protected method overridden by child"
# 创建子类自己的私有方法,不会覆盖父类的私有方法
def __private_method(self):
return "Private method of child (won't override parent's)"
# 公开方法:测试访问父类和自己的变量和方法
def test_access(self):
# 可以访问和覆盖父类的受保护成员
print(f"Child accessing _protected: {self._protected}")
print(f"Child calling _protected_method: {self._protected_method()}")
# 无法直接访问父类的私有成员,但可以访问自己的私有成员
try:
print(f"Trying to access parent's __private: {self.__private}")
except AttributeError as e:
print(f"Child cannot directly access parent's private var: {e}")
# 测试
child = Child()
child.test_access()
# 通过名称修饰后的属性访问
print(f"Accessing parent's private var via mangled name: {child._Parent__private}")
print(f"Accessing child's private var via mangled name: {child._Child__private}")
输出结果:
Child accessing _protected: Protected variable overridden by child Child calling _protected_method: Protected method overridden by child Child cannot directly access parent's private var: 'Child' object has no attribute '_Child__private' Accessing parent's private var via mangled name: Private variable from parent Accessing child's private var via mangled name: Private variable of child (won't override parent's)
技术说明:
- 单下划线(
_)成员可以被子类访问和覆盖 - 双下划线(
__)成员在子类中不会被意外覆盖,因为它们在父类和子类中被分别修饰为不同的名称
五、Python不强制封装的原因
Python的设计哲学体现在"The Zen of Python"(Python之禅)中:
“明了胜于晦涩”
“实用性比纯粹性更重要”
Python创造者Guido van Rossum曾说过:
“我们都是成年人(consenting adults)”
这反映了Python的几个设计理念:
- 信任开发者:认为程序员有足够判断力
- 透明性:代码应当可理解,甚至内部实现也是
- 灵活性优先:有时需要访问"私有"成员(如测试、调试、高级用例)
- 实用主义:在某些情况下,能访问"私有"成员更为实用
六、使用指南:何时使用哪种前缀
无前缀 (公开 API)
适用场景:
- 类的公开接口
- 希望外部代码直接使用的属性和方法
- 稳定且有文档支持的功能
class Rectangle:
def __init__(self, width, height):
# 公开属性:宽度,可以从任何地方读取和修改
self.width = width
# 公开属性:高度,可以从任何地方读取和修改
self.height = height
# 公开方法:计算面积,稳定的对外接口
def area(self):
return self.width * self.height
单下划线_(内部实现)
适用场景:
- 模块级别的"私有"函数和变量
- 类的内部属性和方法,不希望成为公开API
- 在子类中可能被重用的功能
- 作为临时变量或未使用的变量
class DataProcessor:
def __init__(self, data):
# 公开属性:原始数据,可以从外部访问
self.data = data
# 受保护属性:处理状态标记,内部使用,不应从外部直接访问
self._processed = False
# 公开方法:对外接口,处理数据
def process(self):
# 调用内部辅助方法进行预处理
result = self._preprocess()
# 进一步处理...
self._processed = True
return result
# 受保护方法:内部辅助方法,不建议外部调用,但子类可能会使用
def _preprocess(self):
# 数据预处理逻辑,将每个元素乘以2
return [x * 2 for x in self.data]
双下划线__(名称修饰)
适用场景:
- 防止子类覆盖(名称冲突)
- 真正的私有实现细节
- 类的"核心"内部机制
- 存储敏感信息(但不要仅依赖此机制做安全防护)
class AuthenticationSystem:
def __init__(self, username, password):
# 公开属性:用户名
self.username = username
# 私有属性:密码哈希,不希望外部直接访问,也不希望子类意外覆盖
self.__password_hash = self.__hash_password(password)
# 公开方法:验证密码,对外接口
def verify(self, password):
# 内部调用私有方法计算哈希,然后比较
hashed = self.__hash_password(password)
return hashed == self.__password_hash
# 私有方法:密码哈希算法,内部实现细节,不希望被外部访问或子类覆盖
def __hash_password(self, password):
# 实际系统应使用更安全的哈希算法
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
七、实际项目中的应用
Django框架示例
Django框架是Python中优秀的命名约定实践:
class QuerySet:
# 公开API:过滤方法,是稳定的公开接口
def filter(self, *args, **kwargs):
"""
返回与给定查询参数匹配的新QuerySet
"""
# 内部调用受保护方法实现具体功能
return self._filter_or_exclude(False, *args, **kwargs)
# 受保护方法:内部过滤实现,可能在子类中重用
def _filter_or_exclude(self, negate, *args, **kwargs):
# 实现细节...
pass
# 私有方法:准备查询的内部方法,完全是实现细节,不希望被外部或子类直接使用
def __prepare_lookup(self, key):
# 完全内部实现...
pass
现代Python项目惯例
目前Python社区的最佳实践趋势:
- 谨慎使用双下划线:通常单下划线足够表达"内部使用"的意图
- 提供完整的API文档:清晰指明哪些是公开API,哪些是内部实现
- 类型注解与docstring:使用类型提示和文档字符串增强代码可读性
- 接口设计优于访问控制:设计良好的接口比严格的封装更重要
from typing import List, Optional
class Customer:
"""客户信息管理类"""
def __init__(self, name: str, email: str) -> None:
# 公开属性:客户姓名,可以从外部访问
self.name = name
# 公开属性:客户邮箱,可以从外部访问
self.email = email
# 受保护属性:购买记录列表,内部使用,不应直接从外部访问
self._purchases: List[str] = []
def add_purchase(self, item: str) -> None:
"""
添加购买记录
Args:
item: 购买的商品名称
"""
# 向购买记录列表添加商品
self._purchases.append(item)
def get_purchase_history(self) -> List[str]:
"""
获取购买历史
Returns:
购买商品列表的副本
"""
# 返回购买记录的副本,防止外部代码意外修改内部状态
return self._purchases.copy()
八、小结与建议
单下划线(_):
- Python语法上不特殊,仅为约定
- 表示"这是内部实现,不是公开API"
- 子类和外部技术上可以访问,但不建议直接使用
双下划线(__):
- 触发Python的名称修饰机制
- 主要目的是防止子类命名冲突,而非安全封装
- 在当前类中可以正常访问,但在子类和外部访问需要使用修饰后的名称
实际使用建议:
- 默认使用无前缀(公开API)
- 内部实现使用单下划线(
_) - 只有在担心子类覆盖时才使用双下划线(
__) - 记住Python的理念:“明确胜于晦涩”
无论你是初学者还是有经验的开发者,理解这些命名约定都能帮助你编写更符合Python风格的代码,并更好地理解其他Python项目。













