MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python类的属性访问控制

2021-10-091.9k 阅读

1. Python 类属性访问控制概述

在 Python 编程中,类是一种强大的抽象机制,它允许我们将数据和操作数据的方法封装在一起。类的属性(包括数据属性和方法)是类的重要组成部分。然而,有时候我们需要对类的属性访问进行控制,以保护数据的完整性、隐藏实现细节或实现特定的访问策略。

Python 不像一些其他编程语言(如 Java)那样有严格的访问修饰符(如 public、private、protected)。但 Python 通过约定和一些特殊的命名方式来实现类似的属性访问控制效果。

2. 公共属性

2.1 概念及特点

在 Python 中,默认情况下,类的属性都是公共的。这意味着在类的外部,可以直接通过类的实例或类本身来访问这些属性。公共属性没有任何访问限制,它们是类与外部交互的主要接口部分。

2.2 代码示例

class MyClass:
    def __init__(self):
        self.public_data = 10

    def public_method(self):
        return "This is a public method"


obj = MyClass()
print(obj.public_data)  
print(obj.public_method())  

在上述代码中,public_data 是一个公共的数据属性,public_method 是一个公共的方法。我们可以在类的外部直接通过实例 obj 来访问它们。

3. 受保护属性

3.1 概念及约定

Python 中并没有真正意义上的受保护属性,但通过约定,以单个下划线(_)开头的属性被视为受保护属性。这是一种向其他开发者传达该属性是内部使用的,不建议在类外部直接访问的方式。虽然这种约定并没有强制阻止外部访问,但遵循这种约定有助于提高代码的可读性和维护性。

3.2 代码示例

class AnotherClass:
    def __init__(self):
        self._protected_data = 20

    def _protected_method(self):
        return "This is a protected method"


obj2 = AnotherClass()
print(obj2._protected_data)  
print(obj2._protected_method())  

在这个例子中,_protected_data_protected_method 虽然可以在类外部访问,但按照约定,我们应该避免这样做。通常,受保护属性和方法是供类本身及其子类使用的。

4. 私有属性

4.1 概念及实现原理

Python 中的私有属性是以双下划线(__)开头的属性。当在类中定义这样的属性时,Python 会对其名称进行一种特殊的变换,称为名称改写(name mangling)。这种变换使得在类外部无法直接通过原始名称访问该属性,从而达到类似私有属性的效果。

名称改写的规则是在属性名前面加上 _类名。例如,对于类 MyClass 中的私有属性 __private_attr,经过名称改写后,实际的属性名变为 _MyClass__private_attr

4.2 代码示例

class PrivateClass:
    def __init__(self):
        self.__private_data = 30

    def __private_method(self):
        return "This is a private method"

    def access_private(self):
        return self.__private_data, self.__private_method()


obj3 = PrivateClass()
# 下面这行代码会报错,因为无法直接访问私有属性
# print(obj3.__private_data)  
# 下面这行代码也会报错,因为无法直接访问私有方法
# print(obj3.__private_method())  
print(obj3.access_private())  

在上述代码中,__private_data__private_method 是私有属性和方法。我们无法在类外部直接访问它们,但可以通过类内部的公共方法 access_private 来间接访问。

4.3 名称改写的进一步分析

名称改写不仅仅作用于属性和方法,对于类内部的变量等也同样适用。例如:

class NameManglingExample:
    def __init__(self):
        self.__var = 40
        self._var = 50

    def show_vars(self):
        print(self.__var)
        print(self._var)


obj4 = NameManglingExample()
# 下面这行代码会报错,因为无法直接访问改写后的名称
# print(obj4.__var)  
print(obj4._var)  

这里 __var 经过名称改写,而 _var 遵循受保护属性的约定。需要注意的是,虽然名称改写提供了一定程度的保护,但它并不是绝对安全的,因为仍然可以通过改写后的名称来访问私有属性。例如:

class PrivateAttrAccess:
    def __init__(self):
        self.__private_value = 60


obj5 = PrivateAttrAccess()
print(obj5._PrivateAttrAccess__private_value)  

不过,直接通过这种方式访问私有属性是不推荐的,因为这破坏了封装的原则,并且如果类的内部结构发生变化,改写后的名称也可能改变,导致代码出错。

5. 访问控制与继承

5.1 公共属性在继承中的表现

当一个类继承自另一个类时,公共属性和方法会被子类继承,子类可以像使用自己的属性和方法一样使用它们。

class ParentClass:
    def __init__(self):
        self.public_attr = 70

    def public_method(self):
        return "Parent's public method"


class ChildClass(ParentClass):
    pass


child_obj = ChildClass()
print(child_obj.public_attr)  
print(child_obj.public_method())  

在这个例子中,ChildClass 继承了 ParentClass 的公共属性 public_attr 和公共方法 public_method,可以在 ChildClass 的实例上直接访问。

5.2 受保护属性在继承中的表现

受保护属性同样会被子类继承。按照约定,子类可以访问父类的受保护属性,但在子类外部,仍然不应该直接访问这些属性。

class ParentWithProtected:
    def __init__(self):
        self._protected_attr = 80

    def _protected_method(self):
        return "Parent's protected method"


class ChildWithProtected(ParentWithProtected):
    def access_protected(self):
        return self._protected_attr, self._protected_method()


child_protected_obj = ChildWithProtected()
print(child_protected_obj.access_protected())  
# 虽然可以访问,但不建议在子类外部这样做
print(child_protected_obj._protected_attr)  

ChildWithProtected 继承了 ParentWithProtected 的受保护属性和方法。ChildWithProtected 内部可以通过公共方法 access_protected 来访问受保护成员,在子类外部也能访问,但不推荐。

5.3 私有属性在继承中的表现

私有属性不会被子类直接继承。由于名称改写,子类无法通过父类中定义的私有属性的原始名称来访问它们。

class ParentWithPrivate:
    def __init__(self):
        self.__private_attr = 90

    def __private_method(self):
        return "Parent's private method"


class ChildWithPrivate(ParentWithPrivate):
    def try_access_private(self):
        # 下面这行代码会报错,因为无法直接访问父类的私有属性
        # return self.__private_attr  
        pass


child_private_obj = ChildWithPrivate()
# 下面这行代码会报错,因为无法直接访问父类的私有方法
# print(child_private_obj.__private_method())  

在这个例子中,ChildWithPrivate 不能直接访问 ParentWithPrivate 的私有属性和方法。这有助于保持父类的封装性,防止子类意外修改父类的内部状态。

6. 使用特性(Properties)进行属性访问控制

6.1 特性的概念

特性(Properties)是一种特殊的属性,它允许我们以属性访问的方式来调用方法。通过使用特性,我们可以在获取(getter)、设置(setter)和删除(deleter)属性值时执行额外的代码逻辑,从而实现更细粒度的属性访问控制。

6.2 使用 property 函数创建特性

property 函数可以用来创建特性。它接受三个参数:fget(获取属性值的函数)、fset(设置属性值的函数)和 fdel(删除属性值的函数)。

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    def get_celsius(self):
        return self._celsius

    def set_celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is not possible")
        self._celsius = value

    def del_celsius(self):
        del self._celsius

    celsius = property(get_celsius, set_celsius, del_celsius, "Temperature in Celsius")


temp = Temperature(25)
print(temp.celsius)  
temp.celsius = 30
print(temp.celsius)  
# 下面这行代码会引发 ValueError
# temp.celsius = -274  
del temp.celsius
# 下面这行代码会报错,因为属性已被删除
# print(temp.celsius)  

在上述代码中,celsius 是一个特性。通过 get_celsiusset_celsiusdel_celsius 函数,我们可以在访问、设置和删除 celsius 属性时执行自定义的逻辑。

6.3 使用装饰器创建特性

Python 还提供了一种更简洁的方式来创建特性,即使用装饰器。

class Square:
    def __init__(self, side):
        self._side = side

    @property
    def side(self):
        return self._side

    @side.setter
    def side(self, value):
        if value < 0:
            raise ValueError("Side length cannot be negative")
        self._side = value

    @side.deleter
    def side(self):
        del self._side


square = Square(5)
print(square.side)  
square.side = 10
print(square.side)  
# 下面这行代码会引发 ValueError
# square.side = -1  
del square.side
# 下面这行代码会报错,因为属性已被删除
# print(square.side)  

在这个例子中,@property 装饰器将 side 方法转换为一个特性的获取方法。@side.setter 装饰器定义了设置 side 属性的方法,@side.deleter 装饰器定义了删除 side 属性的方法。

7. 访问控制与数据验证

7.1 公共属性的数据验证问题

对于公共属性,如果没有适当的访问控制,可能会导致数据的错误设置。例如:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height


rect = Rectangle(-5, 10)
print(rect.area())  

在这个例子中,width 被设置为负数,这在现实世界中对于矩形的宽度来说是不合理的。由于 widthheight 是公共属性,没有任何限制,导致了错误数据的设置。

7.2 通过访问控制实现数据验证

通过使用受保护或私有属性结合特性,可以实现数据验证。例如:

class BetterRectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value < 0:
            raise ValueError("Width cannot be negative")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value < 0:
            raise ValueError("Height cannot be negative")
        self._height = value

    def area(self):
        return self.width * self.height


rect2 = BetterRectangle(5, 10)
print(rect2.area())  
# 下面这行代码会引发 ValueError
# rect2.width = -5  

BetterRectangle 类中,widthheight 是通过特性实现的。在设置属性值时,会进行数据验证,确保宽度和高度不会被设置为负数。

8. 访问控制与封装的意义

8.1 封装的概念

封装是面向对象编程的一个重要原则,它将数据和操作数据的方法包装在一起,隐藏对象的内部状态和实现细节,只对外提供必要的接口。访问控制是实现封装的重要手段。

8.2 访问控制对封装的支持

通过合理使用公共、受保护和私有属性,以及特性等访问控制机制,我们可以更好地实现封装。公共属性和方法构成了类与外部交互的接口,受保护属性用于类内部和子类的交互,私有属性则隐藏了类的内部实现细节。这样,外部代码只能通过定义好的接口来访问和修改对象的状态,提高了代码的安全性和可维护性。例如,在一个银行账户类中,账户余额可以设为私有属性,通过公共的存款和取款方法来操作余额,这样可以保证余额的修改符合业务逻辑,并且不会被外部随意篡改。

9. 总结 Python 类属性访问控制要点

  • 公共属性:默认可直接在类外部访问,是类与外部交互的主要接口。
  • 受保护属性:以单下划线开头,遵循约定不建议在类外部直接访问,主要供类及其子类使用。
  • 私有属性:以双下划线开头,通过名称改写实现一定程度的保护,不建议在类外部直接访问。
  • 特性:通过 property 函数或装饰器实现,提供了更细粒度的属性访问控制,可在属性访问时执行自定义逻辑。
  • 访问控制与继承:公共和受保护属性会被继承,私有属性不会被直接继承,有助于保持类的封装性。
  • 数据验证:合理的访问控制可以结合数据验证,确保对象状态的合理性。
  • 封装意义:访问控制是实现封装的重要手段,提高代码的安全性和可维护性。

通过深入理解和正确运用 Python 的类属性访问控制机制,开发者可以编写出更健壮、可维护且符合面向对象编程原则的代码。无论是小型项目还是大型的企业级应用,良好的访问控制策略都能提升代码质量,减少潜在的错误和安全隐患。