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

刚接触 Python oop(OOP)的朋友,大概率踩过这两个坑:

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

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


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

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

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

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


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

TIP

以下都是约定俗成的规则。除了双下划线会被编译器做小动作外,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)              # 虽然能跑,但强烈不建议

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

    def get_score(self):
        return self.__score

    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
    def name(self):
        """学生姓名(只读)"""
        return self.__name

    @property
    def score(self):
        """学生分数(0-100)"""
        return self.__score

    @score.setter
    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           # 像操作公共属性一样
# s.score = -5         # 报错:ValueError: 分数必须在 0-100 之间

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

双下划线的成员只是名称被改写了,并非真正隐藏。用 dir() 查看实例属性就能发现端倪:__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. @property 实现。

参考实现

class Student:
    def __init__(self, name, gender):
        self.__name = name
        self.__gender = 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 里改就行)。赶紧去你的项目里试试吧!