第9章:符合Python风格的对象-私有属性和“受保护的”属性、使用 __slots__ 类属性节省内存空间、覆盖类属性

私有属性和“受保护的”属性

私有属性:

Python 不能像 Java 那样使用 private 修饰符创建私有属性,但是 Python 有个简单的机制,能避免子类意外覆盖“私有”属性。

举个例子:有人编写了一个名为 Dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉,这是个难以调试的问题。

为了避免这种情况,如果以 __mood 的形式( 两个前导下划线,尾部没有或最多有一个下划线 )命名实例属性,Python 会把属性名存入实例的 __dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对 Dog 类来说,__mood 会变成 _Dog__mood;对 Beagle 类来说,会变成 _Beagle__mood。这个语言特性叫“名称改写”(name mangling)。

示例 9-10 私有属性的名称会被“改写”,在前面加上下划线和类名:

class Dog:
    __moon = 'Happy'


class Beagle(Dog):
    moon = 'Good'


print('Dog:', Dog.__dict__)
# Dog: {'__module__': '__main__', '_Dog__moon': 'Happy', '__doc__': None,
# '__dict__': <attribute '__dict__' of 'Dog' objects>, 
# '__weakref__': <attribute '__weakref__' of 'Dog' objects>, }

print('Beagle:', Beagle.__dict__)
# Beagle: {'__module__': '__main__', 'moon': 'Good', '__doc__': None}

从上方示例中可以看出 Dog 类的 __dict__ 存储的属性已经被改写为  _Dog__moon

⚠️ 注意:“名称改写”是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事。 

 “受保护的”属性:

但是,不是所有 Python 程序员都喜欢名称改写功能,也不是所有人都喜欢 self.__x 这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写“受保护”的属性( 如 self._x )。

Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。Python 文档把使用一个下划线前缀标记的属性称为“受保护的”属性。 使用 self._x 这种形式保护属性的做法很常见,但是很少有人把这种属性叫作“受保护的”属性,有些人甚至将其称为“私有”属性。

需要注意的是,Python 并不能真正的实现私有和“受保护”的属性,不会强制约束你的行为,请不要干愚蠢的事。

使用 __slots__ 类属性节省空间

默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。

定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 中所含的信息不会变化,例如:

class Vector2d:

    __slots__ = ('_x', '_y')
    typecode = 'd'

    def __init__():
    self._x = 'xxx'
    self._y = 'yyy'

    # 下面是各个方法(因排版需要而省略了)
    ......

在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了”。这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数百万个实例同时活动,这样做能节省大量内存。

此外,还有一个实例属性可能需要注意,即 __weakref__ 属性,为了 让对象支持弱引用,必须有这个属性。用户定义的类中默认就有 __weakref__ 属性。可是,如果类中定义了 __slots__ 属性,而且想把实例作为弱引用的目标,那么要把 '__weakref__' 添加 到 __slots__ 中。

⚠️ 总结注意事项:

  1. 类的 __slots__ 属性只是修改实例属性的存储方式,而不修改类属性的存储方式。
  2. 继承自超类的 __slots__ 属性没有效果,Python 只会使用各个类中自己定义的 __slots__ 属性。
  3. 在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性。除非把 '__dict__' 加 入 __slots__ 中(这样做就失去了节省内存的功效)。

  4. 如果不把 '__weakref__' 加入 __slots__,实例就不能作为弱引用的目标。

覆盖类属性

Python 有个很独特的特性:类属性可用于为实例属性提供默认值,但如果为不存在的实例属性赋值,会新建实例属性并覆盖掉同名类属性。

Vector2d 中有个 typeCode 类属性,__bytes__ 方法两次用到了它,而且都故意使用 self.typeCode 读取它的值。因为 Vector2d 实例本身没有 typeCode 属性,所以 self.typeCode 默认获取的是 Vector2d.typeCode 类属性的值。但是,如果为不存在的实例属性赋值,会新建实例属性。假如我们为 typecCode 实例属性赋值,那么同名类属性不受影响。然而,自此之后,实例读取的 self.typeCode 是实例属性 typeCode,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的 typeCode 属性定制不同的值。

示例:

class Vector2d:
    # typeCode 是类属性,在 Vector2d 实例和字节序列之间转换时使用。
    typeCode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    def __bytes__(self):
        return bytes([ord(self.typeCode)]) + bytes(array(self.typeCode, self))


v1 = Vector2d(3, 4)

# 1. Vector2d.typeCode 属性的默认值是 'd',即转换成字节序列时使用 8 字节双精度浮点数表示向量的各个分量。
# 因为 Vector2d 实例本身没有 typeCode 属性,所以 self.typeCode 默认获取的是 Vector2d.typeCode 类属性的值。
print(bytes(v1))  # b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x08@'

# 2. 如果在转换之前把 Vector2d 实例的 typeCode 属性设为 'f',那么使用 4 字节单精度浮点数表示各个分量
# 为不存在的实例属性赋值,会新建实例属性,自此之后,实例读取的 self.typeCode 是实例属性 typeCode,也就是把同名类属性遮盖了。
v1.typeCode = 'f'
print(bytes(v1))  # b'f\x00\x00@@\x00\x00@@'

# 3. 同名类属性不受影响,Vector2d.typeCode 属性的值不变,只有 v1 实例的 typeCode 属性使用 'f'。
print(Vector2d.typeCode)  # d
print(v1.typeCode)  # f

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值