Python class attribute encapsulation and access control tutorial

Friends who are new to Python oop (OOP) will most likely have stepped on these two pitfalls:

  1. Added attributes__The prefix was thought to be "locked", but it turned out that it could still be read and written in another way;
  2. Assign values ​​to instance attributes externally (for example, change student scores to-10or200), the logic collapses and the source cannot be found.

This is the problem of insufficient encapsulation - today we will use Python's naming conventions and tools to make the access control of class attributes clear.


1. First understand: what is real encapsulation?

Among the three major features of OOP (encapsulation, inheritance, polymorphism), encapsulation is the foundation. Its core is not to hide attributes completely, but to:

  • Bind data and data manipulation methods in a class;
  • Hide internal implementation details from the outside (such as what variable names are used to store scores);
  • Only expose secure, controlled access (for example, you can only change the score to 0-100).

Python has nothing like Java/C++publicprotectedprivateKeywords, but through naming convention and syntax sugar, similar effects can be achieved.


2. Python’s three “access control levels”

TIP

The following are conventional rules. Except for double underscores, which will be done by the compiler, Python itself will not forcibly intercept any access - after all, this is a "we are all adults" language.

2.1 Public members (Public)

By default, all properties/methods are public and can be used freely inside, outside, and subclasses.

class Student:
    def __init__(self, name):
        self.name = name   # 公共属性:完全暴露

s = Student("Alice")
print(s.name)              # Alice
s.name = "Bob"             # 直接修改也无妨

2.2 Protected members (Protected)

Start with **single underscore_The members starting with ** are by convention internal members for themselves and subclasses, and it is best not to touch them from the outside.

But Python will not stop you - if you insist on accessing the code, you will still run, but other people in the team will think you are "ignorant".

class Student:
    def __init__(self, name):
        self._name = name   # “请勿打扰”的内部属性

s = Student("Alice")
print(s._name)              # 虽然能跑,但强烈不建议

2.3 Private members (Private)

Start with ** double underline__For members that begin with and do not end with a double underscore, the Python compiler will automatically do Name Mangling and become_ClassName__varformat. Ostensibly not directly accessible from the outside.

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

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

3. Fake lock turns into real door: achieving controlled encapsulation

Although double underscores cannot prevent malicious people, with accessors (getters/setters), it can become a real "security door" - we can add logic such as parameter verification, logging, and permission checking to the door.

3.1 Traditional method: explicitly write getter/setter

Early Python or developers accustomed to Java/C++ will write like this:

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 之间")

However, this way of writing is not Pythonic enough - you have to write it when callings.get_name(), rather than the more naturals.name

Python provides@propertyThis syntactic sugar can "disguise" methods as attributes, retaining security verification while having concise calls like public attributes.

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 之间")

External calls are now very natural:

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. Revealing the name rewriting: Why can the "fake lock" be opened?

Double-underlined members only have their names rewritten, not really hidden. usedir()You can find clues by looking at the instance properties:__namebecame_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. Violation of the encapsulation contract;
  2. Once the class name changes, the rewritten name will also change accordingly, and the code will crash immediately;
  3. Other developers’ blood pressure will soar when they see it.

5. Don’t get confused: special variable names

There is also a variable/method that starts with a double underscore and ends with a double underscore, such as__init____len____dict__. This is Python's magic method/property that will not be rewritten by name and can be accessed directly.

class Student:
    def __init__(self):
        self.__special_attr__ = "我是特殊属性,不会被改写"

s = Student()
print(s.__special_attr__)    # 正常输出

6. Hands-on practice: Encapsulationgenderproperty

Let’s do a small exercise and ask for:

  1. putgenderSet as private property;
  2. Can only be initialized to'male'or'female'
  3. Subsequent modifications can only be made to these two values;
  4. use@propertyaccomplish.

Reference implementation

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. Summary of best practices

  1. Private by default, exposed on demand: Unless it is determined that the attribute is "completely public and does not require verification", otherwise add it__prefix;
  2. **use@propertyReplaces explicit getters/setters: more Pythonic and more natural to call;
  3. Setter must be verified: This is one of the core values ​​of encapsulation, don’t just do simple assignment;
  4. Single underline is only used for internal agreements: such as tool methods and properties that you do not want to make public for the time being;
  5. Don’t touch the variable with the rewritten name: If you touch it, you will dig a hole for yourself and the team;
  6. Properly design read-only/read-write attributes: Attributes such as name and ID number generally only leave@property, do not add@xxx.setter

Good encapsulation can make the code more robust (to prevent external misoperations), easier to maintain (if the internal implementation is changed, external calls will be insensitive), and easier to expand (permission checks and logging will be added in the future, just change it directly in the setter). Hurry up and try it in your project!