乐趣区

关于python:翻译实用的Python编程0502Classesencapsulation

目录 | 上一节 (5.1 再谈字典) | [下一节 (6 生成器)]()

5.2 类和封装

创立类时,通常会尝试将类的外部细节进行封装。本节介绍 Python 编程中无关封装的习惯用法(包含公有变量和公有属性)。

Public vs Private

尽管类的次要作用之一是封装对象的属性和外部实现细节。然而,类还定义了外界用来操作该对象的私有接口(public interface)。实现细节与私有接口之间的区别很重要。

问题

在 Python 中,简直所有与类和对象无关的货色都是 凋谢(open)的。

  • 能够轻松地查看对象的外部细节。
  • 能够随便地批改。
  • 没有访问控制的概念(例如:公有类成员)。

如何隔离 外部实现 的细节,这是一个问题。

Python 封装

Python 依赖编程约定来批示某些货色的用处。这就约定基于命名。有一种广泛的态度是,程序员应该遵守规则,而不是让语言来强制执行规定。

公有属性

以下划线 _ 结尾的任何属性被认为是公有的(private)。

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]
>>>

你可能会想要对此进行查看(译注:例如 shares 示意的是股份数目,值应该是整数。所以给 shares 赋值时应该对值进行查看。如果查看发现给 shares 赋的值不是整数,那么应该触发一个 TypeError 异样):

s.shares = '50'     # Raise a TypeError, this is a string

这时候你会怎么做?

托管属性

办法一:引进拜访办法(accessor methods)。

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

    # Function that layers the "get" operation
    def get_shares(self):
        return self._shares

    # Function that layers the "set" operation
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

蹩脚的是,这毁坏了咱们的已有代码。例如:之前是通过 s.shares = 50shares 赋值的,那么当初就要改成s.set_shares(50)shares 赋值,这很不好。

特色属性(Properties)

办法二:

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

当初,一般属性(normal attribute)的拜访触发了 @property@shares.setter 下的 getter 办法和 setter 办法。

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

应用该办法,不须要对源代码做任何批改。在类内(包含在 __init__() 办法内)有赋值的时候,间接调用新的 setter:

class Stock:
    def __init__(self, name, shares, price):
        ...
        # This assignment calls the setter below
        self.shares = shares
        ...

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

特色属性和公有名称(private names)的应用之间常常会呈现混同。只管特色属性外部应用的是公有名称,如 _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
    ...

这容许你删除 cost 前面的括号,暗藏 cost 是一个办法的事实:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares # Instance variable
100
>>> s.cost   # Computed Value
49010.0
>>>

对立拜访

最初一个例子展现了如何在对象上搁置一个更加对立的接口。如果不这样做,对象应用起来可能会令人困惑。

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() # Method
49010.0
>>> b = s.shares # Data attribute
100
>>>

为什么 cost 前面须要加上括号 (),然而 shares 却不须要?特色属性能够解决这个问题。

装璜器语法

@ 语法称为“装璜(decoration)”。它指定了一个修饰符(modifier),利用于紧接其后的函数定义:

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

更多细节在 第 7 节 中给到。

插槽属性(__slots__

你能够应用 __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'

管这样能够避免谬误和限度对象的应用,但实际上应用 __slots__ 是为了进步性能,进步 Python 利用内存的效率。

对于封装的最终阐明

不要滥用公有属性(private attributes),特色属性(properties),插槽属性(slots)等。它们有非凡的用处,你在浏览其它 Python 代码时可能会看到。然而,对于大多数日常编码而言,它们不是必须的。

练习

练习 5.6:简略特色属性

应用特色属性是一种十分有用的给对象增加“计算属性”的形式。尽管你在 stock.py 文件中创立了 Stock 对象,然而请留神,在 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
>>>

尝试将 cost作为办法调用(s.cost()),你会发现,当初曾经被定义为特色属性的 cost 无奈作为办法被调用。

>>> s.cost()
... fails ...
>>>

这些更改很可能会毁坏你之前的 pcost.py 程序,所以,你可能须要返回到 pcost.py 中去掉 cost() 办法前面的括号()

练习 5.7:特色属性和 Setters

请批改 shares 属性,以便将该值存储在公有属性中,并且应用属性函数(property functions)确保赋给 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:增加插槽属性(slots)

请批改 Stock 类,以便 Stock 类领有一个 __slots__ 属性。而后确认无奈增加新属性:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... see what happens ...
>>>

应用 __slots__ 时,Python 应用更高效的对象外部示意。如果你尝试查看实例 s 的底层字典会产生什么?

>>> s.__dict__
... see what happens ...
>>>

该当指出,__slots__ 作为数据结构是类中最罕用的一种优化。应用插槽属性使程序占用更少的内存,运行更快。然而,在其它大多数类中,你应该尽可能防止应用 __slots__

目录 | 上一节 (5.1 再谈字典) | [下一节 (6 生成器)]()

注:残缺翻译见 https://github.com/codists/practical-python-zh

退出移动版