Python 类属性封装与访问控制教程

刚接触 Python 面向对象编程(OOP)的朋友,大概率踩过这两个坑:

  1. 给属性加了 __ 前缀以为“锁死”了,结果换个方式还是能读写;
  2. 外部随便给实例属性赋值(比如把学生分数改成 -10 或 200),逻辑崩了都找不到源头。

这就是封装没做到位的问题——今天我们就用 Python 的命名约定和工具,把类属性的“权限门”搭得明明白白。


1. 先搞懂:什么是真正的封装?

OOP 三大特性(封装、继承、多态)里,封装是基础。它的核心不是把属性“完全藏起来看不见摸不着”,而是:

  • 把「数据」和「操作数据的方法」绑定在一个类里;
  • 对外部隐藏内部实现细节(比如存分数用了什么变量名);
  • 只暴露安全的、受控制的访问入口(比如只能把分数改成 0-100)。

Python 没有像 Java/C++ 那样的 public/protected/private 关键字,但通过命名约定语法糖,完全能实现类似的效果。


2. Python 的三种“访问控制级别”

注意引号——这些都是约定俗成的规则,除了双下划线会被编译器做小动作外,Python 本身不会强制拦截任何访问(毕竟 Python 是“我们都是成年人”的语言)。

2.1 公共成员 Public

默认情况,所有属性/方法都是公共的,类内部、外部、子类都能随便用

class Student:
    def __init__(self, name):
        self.name = name  # 公共属性:直接裸奔

# 外部直接读写
s = Student("Alice")
print(s.name)  # 输出 Alice
s.name = "Bob" # 直接改掉也没问题

2.2 受保护成员 Protected

单个下划线 _ 开头的成员,按约定是「只给自己和子类用,外部别碰」。

但 Python 不拦——比如你非要用,代码照样跑,只是团队里其他开发者会觉得你“不懂事”。

class Student:
    def __init__(self, name):
        self._name = name  # 受保护属性:加了个“请勿打扰”的门帘

s = Student("Alice")
print(s._name)  # 输出 Alice,但不建议这么写

2.3 私有成员 Private(看起来最安全)

双下划线 __ 开头、非双下划线结尾的成员,Python 编译器会偷偷做一个叫「名称改写 Name Mangling」的操作,改成 _ClassName__var 的格式,表面上看外部无法直接访问。

class Student:
    def __init__(self, name):
        self.__name = name  # 私有属性:上了个“假锁”

s = Student("Alice")
# print(s.__name)  # 直接报错:AttributeError: 'Student' object has no attribute '__name'

3. 假锁变真门:实现受控制的封装

虽然双下划线的“假锁”防不住有心的开发者,但配合访问器(getter/setter),就能变成真正的“安全门”——我们可以在门里加参数验证、日志记录、权限检查等逻辑。

3.1 传统方法:显式写 getter/setter

早期 Python 或者习惯 Java/C++ 的开发者会这么写:

class Student:
    def __init__(self, name, score):
        self.__name = name    # 私有姓名(只读比较合理,入学后一般不能改)
        self.__score = score  # 私有分数(可读写,但必须验证)
    
    # 姓名的 getter(不提供 setter)
    def get_name(self):
        return self.__name
    
    # 分数的 getter
    def get_score(self):
        return self.__score
    
    # 分数的 setter(加了 0-100 的验证)
    def set_score(self, score):
        if not isinstance(score, (int, float)):
            raise TypeError("分数必须是数字")
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError("分数必须在 0-100 之间")

但这种写法不够 Pythonic——外部用的时候要写 s.get_name(),而不是更自然的 s.name

3.2 推荐方法:用 @property 装饰器

Python 提供了 @property 这个语法糖,能把「方法伪装成属性」,既保留了安全验证,又有公共属性那样的简洁调用。

class Student:
    def __init__(self, name, score):
        self.__name = name
        self.__score = score
    
    # 只读属性:只加 @property,不加 @xxx.setter
    @property
    def name(self):
        """学生姓名(只读)"""
        return self.__name
    
    # 可读可写属性:先加 @property 定义 getter,再加 @xxx.setter 定义 setter
    @property
    def score(self):
        """学生分数(0-100)"""
        return self.__score
    
    @score.setter  # 注意格式:必须和 getter 的方法名完全一致
    def score(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("分数必须是数字")
        if 0 <= value <= 100:
            self.__score = value
        else:
            raise ValueError("分数必须在 0-100 之间")

现在外部调用就非常自然了:

s = Student("Bob", 85)
print(s.name)    # 输出 Bob,像访问公共属性一样
# s.name = "Charlie"  # 直接报错:AttributeError: can't set attribute(因为没加 setter)
s.score = 90     # 输出 90,像赋值公共属性一样
# s.score = -5   # 报错:ValueError: 分数必须在 0-100 之间

4. 揭秘名称改写:为什么“假锁”能被打开?

刚才说双下划线的成员是“假锁”,那怎么打开?直接看 dir() 打印的实例属性列表就知道了——Python 把 __name 改成了 _Student__name

class Student:
    def __init__(self, name):
        self.__name = name

s = Student("Alice")
print(dir(s))  # 会看到很多内置属性,其中有个 '_Student__name'
print(s._Student__name)  # 输出 Alice!但千万不要在生产代码里这么写
WARNING
  1. 违反封装的约定;
  2. 类名如果改了,这个名称也会跟着变,代码直接崩;
  3. 其他开发者看了会骂街(或者至少心里吐槽)。

5. 别搞混:特殊变量名

还有一种以双下划线开头+双下划线结尾的变量/方法,比如 __init____len____dict__,这些是 Python 的特殊成员(魔术方法/属性)不会被名称改写,可以直接访问:

class Student:
    def __init__(self):
        self.__special_attr__ = "这是特殊属性"

s = Student()
print(s.__special_attr__)  # 直接输出:这是特殊属性

6. 动手练:封装 gender 属性

给你一个小练习,要求:

  1. gender 设为私有属性;
  2. 只能初始化为 'male''female'
  3. 后续修改也只能是这两个值;
  4. 用你喜欢的方式(传统 getter/setter 或 @property)实现。

参考答案(用 @property)

class Student:
    def __init__(self, name, gender):
        self.__name = name
        self.__gender = None  # 先设为 None,用 setter 初始化(复用验证逻辑)
        self.gender = gender  # 调用下面的 setter
    
    @property
    def gender(self):
        return self.__gender
    
    @gender.setter
    def gender(self, value):
        if value not in ('male', 'female'):
            raise ValueError("性别必须是 'male' 或 'female'")
        self.__gender = value

# 测试代码
if __name__ == "__main__":
    try:
        bart = Student('Bart', 'male')
        assert bart.gender == 'male', "初始性别设置失败"
        bart.gender = 'female'
        assert bart.gender == 'female', "修改性别设置失败"
        # 测试非法值
        # bart.gender = 'unknown'  # 应该报错
        print("✅ 测试成功!")
    except Exception as e:
        print(f"❌ 测试失败:{e}")

7. 最佳实践总结

  1. 默认设私有,按需暴露:除非确定是“完全公开、不需要验证”的属性,否则都加 __ 前缀设为私有;
  2. 用 @property 代替显式 getter/setter:更 Pythonic,代码更简洁;
  3. setter 必须加验证:这是封装的核心价值之一,别只做简单的赋值;
  4. 单下划线仅用于内部约定:比如工具方法、暂时不想公开的属性;
  5. 不要碰名称改写后的变量:碰了就是给自己和团队挖坑;
  6. 合理设计只读/读写属性:比如姓名、身份证号这种一般不能改的,就只加 @property,不加 @xxx.setter

好的封装能让你的代码更健壮(减少外部误操作)、更易维护(内部实现细节改了,外部调用不用变)、更易扩展(以后加权限检查、日志记录直接在 setter 里加就行)。赶紧去你的项目里试试吧!