类与封装

Beginner

This tutorial is from open-source community. Access the source code

简介

编写类时,通常会尝试封装内部细节。本节将介绍一些用于此目的的 Python 编程习惯用法,包括私有变量和属性。

公共与私有

类的主要作用之一是封装对象的数据和内部实现细节。然而,类还定义了一个供外部世界用来操作对象的公共接口。实现细节与公共接口之间的这种区别很重要。

一个问题

在 Python 中,几乎关于类和对象的所有内容都是开放的

  • 你可以轻松检查对象的内部结构。
  • 你可以随意更改内容。
  • 没有严格的访问控制概念(即私有类成员)

当你试图隔离内部实现的细节时,这就是一个问题。

Python 封装

Python 依靠编程约定来表明某些内容的预期用途。这些约定基于命名。有一种普遍的态度是,由程序员来遵守规则,而不是让语言强制实施这些规则。

私有属性

任何以单下划线 _ 开头的属性名都被视为私有属性。

class Person(object):
    def __init__(self, name):
        self._name = 0

如前所述,这只是一种编程风格。你仍然可以访问和更改它。

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

一般来说,任何以单下划线 _ 开头的名称,无论是变量、函数还是模块名,都被视为内部实现。如果你发现自己直接使用这样的名称,那你可能做错了。应该寻找更高级别的功能。

简单属性

考虑以下类。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

一个令人惊讶的特性是,你可以将属性设置为任何值:

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

你可能会看到这种情况并认为需要一些额外的检查。

s.shares = '50'     ## 引发 TypeError,这是一个字符串

你会怎么做呢?

托管属性

一种方法:引入访问器方法。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.set_shares(shares)
        self.price = price

    ## 实现“获取”操作的函数
    def get_shares(self):
        return self._shares

    ## 实现“设置”操作的函数
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

糟糕的是,这会破坏我们所有现有的代码。s.shares = 50 变成了 s.set_shares(50)

属性

对于前面的模式,还有另一种方法。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

现在,普通的属性访问会触发 @property@shares.setter 下的 getter 和 setter 方法。

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## 触发 @property
50
>>> s.shares = 75    ## 触发 @shares.setter
>>>

使用这种模式,无需对源代码进行任何更改。当在类内部进行赋值时,包括在 __init__() 方法内部,新的 setter 也会被调用。

class Stock:
    def __init__(self, name, shares, price):
     ...
        ## 此赋值会调用下面的 setter
        self.shares = shares
     ...

  ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

属性和使用私有名称之间常常存在混淆。虽然属性在内部使用像 _shares 这样的私有名称,但类的其他部分(不是属性本身)可以继续使用像 shares 这样的名称。

属性对于计算数据属性也很有用。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price
  ...

这使你可以省略额外的括号,隐藏它实际上是一个方法的事实:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## 实例变量
100
>>> s.cost   ## 计算值
49010.0
>>>

统一访问

最后一个示例展示了如何为对象提供更统一的接口。如果不这样做,对象的使用可能会令人困惑:

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## 方法
49010.0
>>> b = s.shares ## 数据属性
100
>>>

为什么 cost 需要使用 () 来调用,而 shares 却不需要呢?属性可以解决这个问题。

装饰器语法

@ 语法被称为“装饰”。它指定了一个应用于紧跟其后的函数定义的修饰符。

...
@property
def cost(self):
    return self.shares * self.price

第 7 节给出了更多细节。

__slots__ 属性

你可以限制属性名称的集合。

class Stock:
    __slots__ = ('name','_shares','price')
    def __init__(self, name, shares, price):
        self.name = name
     ...

对于其他属性,它会引发错误。

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' object has no attribute 'prices'

虽然这可以防止错误并限制对象的使用,但它实际上是用于提高性能,并使 Python 更有效地使用内存。

关于封装的最后说明

不要过度使用私有属性、属性、插槽等。它们有特定的用途,你在阅读其他 Python 代码时可能会看到它们。然而,对于大多数日常编码来说,它们并非必需。

练习 5.6:简单属性

属性是向对象添加“计算属性”的一种有用方式。在stock.py中,你创建了一个Stock对象。注意,在你的对象上,提取不同类型数据的方式存在轻微不一致:

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

具体来说,注意你必须在cost上添加额外的(),因为它是一个方法。

如果你将cost变成一个属性,就可以去掉cost()上额外的()。修改你的Stock类,使成本计算如下工作:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

尝试将s.cost()作为函数调用,观察到由于cost已被定义为属性,它现在不起作用了。

>>> s.cost()
... 失败...
>>>

进行此更改可能会破坏你早期的pcost.py程序。你可能需要回去并去掉cost()方法上的()

练习 5.7:属性与设置器

修改shares属性,使其值存储在一个私有属性中,并使用一对属性函数来确保它始终被设置为整数值。以下是预期行为的示例:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>

练习 5.8:添加插槽

修改Stock类,使其具有__slots__属性。然后,验证不能添加新属性:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... 看看会发生什么...
>>>

当你使用__slots__时,Python 会使用更高效的对象内部表示形式。如果你尝试检查上面s的底层字典会发生什么?

>>> s.__dict__
... 看看会发生什么...
>>>

需要注意的是,__slots__最常用于对用作数据结构的类进行优化。使用插槽将使此类程序使用少得多的内存并运行得稍快一些。不过,在大多数其他类上你可能应该避免使用__slots__

总结

恭喜你!你已经完成了“类与封装”实验。你可以在 LabEx 中练习更多实验来提升你的技能。