在Python开发中,下划线前缀不是装饰,而是有明确语义的命名约定。很多新手以为 __var 是“私有变量”,实际上它触发的是名称修饰(name mangling)。本文通过代码实例和继承场景,带你彻底搞懂单下划线_和双下划线__的区别,并给出项目中的使用建议。
一、概念与类比
Python官方定义(PEP 8):
- 单下划线前缀 _var:表示该标识符是“非公开API”或实现细节,仅是一个约定。
- 双下划线前缀 __var:触发名称修饰,Python解释器将其转换为 _ClassName__var。
类比:公开文件放桌面;内部文件放标有“内部使用”的文件夹但没有锁;机密文件放保险柜并改名,需要特殊途径才能找到。
二、代码演示
2.1 单下划线 _ :建议性的私有
- 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
复制代码
技术要点:
- 单下划线没有特殊语法意义,纯粹是命名约定。
- dir() 可以看到这些属性:'_balance', '_calculate_interest'。
2.2 双下划线 __ :名称修饰机制
- class UserAccount:
- def __init__(self, username, password):
- self.username = username
- self._email = None
- 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
- try:
- print(user.__password) # AttributeError
- except AttributeError as e:
- print(f"Error: {e}")
- try:
- user.__reset_password() # AttributeError
- except AttributeError as e:
- print(f"Error: {e}")
- print(user._UserAccount__password) # abc123,绕过名称修饰
复制代码
技术要点:
- 双下划线触发名称修饰,将 __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()
- # 输出:
- # 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'
- 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}")
复制代码
结论:单下划线成员可以被子类访问和覆盖;双下划线成员在父类和子类中分别被修饰为不同名称,不会意外覆盖。
四、使用指南
- 无前缀:公开API,稳定且文档支持的接口。- class Rectangle:
- def __init__(self, width, height):
- self.width = width
- self.height = height
- def area(self):
- return self.width * self.height
复制代码
- 单下划线 _ :内部实现,子类可重用,但不建议外部直接使用。- 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):
- 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 框架的 QuerySet 类使用单下划线作为内部过滤实现,双下划线用于完全内部的查询准备方法,外部只暴露 filter() 等公开API。现代Python项目建议:
- 谨慎使用双下划线,单下划线通常足够。
- 提供完整的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:
- self._purchases.append(item)
- def get_purchase_history(self) -> List[str]:
- return self._purchases.copy()
复制代码
六、小结
- 单下划线_ :纯约定,子类可覆盖,表示“内部实现”。
- 双下划线__ :触发名称修饰,防止子类意外覆盖,但在当前类和子类中可以通过 _ClassName__attr 访问。
- 默认优先使用无前缀(公开API),内部实现用单下划线,只有在担心子类覆盖时才用双下划线。
理解这些命名约定,能让你写出更符合Python风格的代码,也更易读懂大型Python项目。 |