前言
Python 中没有 public、private、protected 这样的访问控制关键字。取而代之的,是一套基于下划线的约定和名称改写机制。这套规则看似简单,但细节很多——单下划线、双下划线、前后双下划线,各自有不同的含义。而 property 装饰器则提供了另一种优雅的属性访问控制方式,让 Python 类的接口设计更加灵活。本文将这两块内容合并讲解,厘清所有容易混淆的地方。
下划线命名规则总览
Python 中共有五种与下划线相关的命名模式:
| 模式 |
示例 |
含义 |
| 单前导下划线 |
_name |
约定:内部使用,不应外部访问 |
| 单结尾下划线 |
name_ |
避免与 Python 关键字冲突 |
| 双前导下划线 |
__name |
名称改写(name mangling) |
| 双前导+双结尾下划线 |
__name__ |
魔法方法/属性,Python 保留 |
| 裸下划线 |
_ |
临时或不重要的变量 |
下面逐一展开。
单前导下划线:_name
这是 Python 中最常见的”私有”约定。在模块、类或函数内部,以 _ 开头的名称表示”这是内部实现细节,外部不应直接访问”。
在模块中
1 2 3 4 5 6 7 8
| def public_api(): """对外开放的接口""" return _internal_helper()
def _internal_helper(): """内部辅助函数,不对外暴露""" return "done"
|
使用 from mymodule import * 时,以下划线开头的名称不会被导入(除非在 __all__ 列表中显式声明)。
1 2 3 4
| from mymodule import *
public_api() _internal_helper()
|
在类中
1 2 3 4 5 6 7
| class Person: def __init__(self, name, age): self._name = name self._age = age
p = Person("张三", 25) print(p._name)
|
再次强调:这仅仅是约定,Python 解释器不会阻止你访问。它表达的是开发者”请勿直接使用”的意图。
单结尾下划线:name_
当变量名与 Python 关键字冲突时,在末尾加一个下划线来规避:
1 2 3 4 5 6 7
| class Person: def __init__(self, name, class_): self.name = name self.class_ = class_
def process(type_, list_): print(type(list_))
|
常见场景:class_、type_、list_、dict_、id_、filter_。
双前导下划线:__name 与名称改写
这是 Python 中唯一有实际语法效果的下划线模式。当一个类属性以 __ 开头(不以 __ 结尾)时,解释器会自动触发名称改写(Name Mangling)——将该属性名改写为 _类名__属性名 的形式。
基本示例
1 2 3 4 5 6 7 8 9 10 11
| class Dog: def __init__(self, name): self.__name = name
def __bark(self): print(f"{self.__name} 在叫")
d = Dog("旺财")
print(d._Dog__name) d._Dog__bark()
|
为什么要这么设计
名称改写的目的是防止子类意外覆盖父类的”私有”属性,而不是做安全控制。
1 2 3 4 5 6 7 8 9 10 11 12
| class Animal: def __init__(self): self.__id = 0
class Cat(Animal): def __init__(self): super().__init__() self.__id = 100
c = Cat() print(c._Animal__id) print(c._Cat__id)
|
两个 __id 互不干扰——这正是名称改写的价值所在。如果没有改写机制,子类的 __id 会直接覆盖父类的 __id。
改写规则细节
- 只对类内部定义的名称生效,事后赋值不会触发改写
- 只改写双下划线开头、不以双下划线结尾的名称(
__init__ 等魔法方法不受影响)
- 改写后名称就是
_类名__属性名,规则简单且可预测
1 2 3 4 5 6 7
| class Test: def __init__(self): self.__var = 1
t = Test() t.__other = 2 print(t.__other)
|
常见误区
很多教程说 __name 是”私有变量,外部无法访问”——这在 Python 中并不准确。改写只是换了名字,目的从来不是安全或权限控制。
1 2 3 4 5 6
| class Secret: def __init__(self): self.__password = "123456"
s = Secret() s._Secret__password = "hacked"
|
如果 Guido van Rossum 想要真正的私有变量,他会设计一个访问控制机制,而不是一个改名规则。
双前导+双结尾下划线:__name__
这类名称是 Python 语言保留的魔法方法/属性(也称 dunder methods),由 Python 解释器调用。例如 __init__、__str__、__repr__、__len__、__call__ 等。
绝对不要自定义以 __ 开头和结尾的名称,以免与 Python 的语言机制或未来版本冲突。
1 2 3 4 5 6 7 8 9 10
| class Person: def __init__(self, name): self.name = name
def __str__(self): return f"Person({self.name})"
|
裸下划线:_
一个单独的 _ 通常用作”不关心这个值”的占位符:
1 2 3 4 5 6 7 8 9 10 11 12
| for _ in range(5): print("Hello")
name, _, age = ("张三", "男", 28)
try: 1 / 0 except ZeroDivisionError as _: print("除零错误")
|
在 Python REPL 交互环境中,_ 还保存着上一次表达式的结果值。
property 装饰器
理解了前导下划线的约定后,自然会产生一个需求:如果我约定 _name 是内部属性,那外部如何安全地读写它? 这就是 property 要解决的问题。
property 允许你将属性的访问(读、写、删除)包装为方法调用,从而实现:
- 属性值的校验
- 计算属性(衍生值)
- 向后兼容(将原本直接暴露的属性升级为带逻辑的方法,外部调用代码无需改动)
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Person: def __init__(self, name, age): self._name = name self._age = age
@property def name(self): """getter:获取姓名""" return self._name
@name.setter def name(self, value): """setter:设置姓名时可加校验""" if not isinstance(value, str): raise TypeError("姓名必须是字符串") if not value.strip(): raise ValueError("姓名不能为空") self._name = value
@name.deleter def name(self): """deleter:删除姓名""" print(f"正在删除 {self._name}") del self._name
@property def age(self): return self._age
@age.setter def age(self, value): if value < 0 or value > 150: raise ValueError(f"年龄 {value} 不合法") self._age = value
|
使用方式与普通属性完全一致:
1 2 3 4 5 6 7 8 9 10 11
| p = Person("张三", 25)
print(p.name)
p.name = "李四"
del p.name
|
为什么不用 Java 风格的 get/set 方法
1 2 3 4 5 6 7 8 9 10 11
| class Person: def get_name(self): return self._name
def set_name(self, value): self._name = value
p = Person() print(p.get_name()) p.set_name("张三")
|
property 让它变成了这样:
对于一个已有类,如果你最初直接暴露了 self.name,后来需要加校验逻辑,可以直接改为 property,外部调用代码一行都不用改——这正是 property 设计的核心优势。
只读属性
只提供 getter,不提供 setter 即可:
1 2 3 4 5 6 7 8 9 10 11 12
| class Circle: def __init__(self, radius): self._radius = radius
@property def area(self): """面积是计算属性,不应被赋值""" return 3.14159 * self._radius ** 2
c = Circle(5) print(c.area)
|
旧式 property 写法
@property 装饰器是 Python 2.6+ 引入的语法糖。在此之前,property 是这样用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Person: def __init__(self, name): self._name = name
def _get_name(self): return self._name
def _set_name(self, value): self._name = value
def _del_name(self): del self._name
name = property(_get_name, _set_name, _del_name, "文档字符串")
|
语法糖版本可读性好太多,旧式写法仅作了解即可。
property 实用场景
场景一:懒加载/缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class DataLoader: def __init__(self, filepath): self._filepath = filepath self._data = None
@property def data(self): if self._data is None: print("首次读取文件...") self._data = {"key": "从文件加载的内容"} return self._data
dl = DataLoader("data.json") print(dl.data) print(dl.data)
|
场景二:反向兼容的升级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class User: def __init__(self, email): self.email = email
class User: def __init__(self, email): self._email = None self.email = email
@property def email(self): return self._email
@email.setter def email(self, value): if "@" not in value: raise ValueError("邮箱格式不正确") self._email = value
|
外部调用 user.email = "xxx" 的代码无需任何改动,升级完全透明。
场景三:日志追踪
1 2 3 4 5 6 7 8 9 10 11 12
| class Config: def __init__(self): self._debug = False
@property def debug(self): return self._debug
@debug.setter def debug(self, value): print(f"[Config] debug 从 {self._debug} 变为 {value}") self._debug = value
|
下划线与 property 的结合
回到最初的问题:在 Python 中如何设计类的接口?
| 做法 |
示例 |
说明 |
内部存储使用 _name |
self._name = name |
约定:这是内部属性 |
| property 提供受控访问 |
@property + @name.setter |
外部通过 property 读写 |
真正不希望子类覆盖的用 __name |
self.__id = id |
触发名称改写,防止子类意外覆盖 |
一个完整的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class BankAccount: def __init__(self, owner, balance): self._owner = owner self.__balance = balance
@property def owner(self): return self._owner
@property def balance(self): """余额只读""" return self.__balance
def deposit(self, amount): if amount <= 0: raise ValueError("存款金额必须大于 0") self.__balance += amount return self.__balance
def withdraw(self, amount): if amount <= 0: raise ValueError("取款金额必须大于 0") if amount > self.__balance: raise ValueError("余额不足") self.__balance -= amount return self.__balance
|
1 2 3 4 5 6 7 8
| acc = BankAccount("张三", 1000) print(acc.owner) print(acc.balance)
acc.deposit(500) acc.withdraw(200) print(acc.balance)
|
总结
_name:约定俗成的”内部使用”,from module import * 不导入,但 Python 不做强制
name_:和后置关键字冲突时用的替代名
__name:名称改写为 _类名__name,用于防止子类意外覆盖
__name__:Python 保留的魔法方法,绝不能自定义
_:临时占位符,表示”我不关心这个值”
property:Python 风格的 getter/setter,保持 obj.attr 的自然语法,同时允许在访问背后执行逻辑
- 典型模式:内部用
_name 存储数据,外部通过 @property 提供受控访问