简介
编写类时,通常会尝试封装内部细节。本节将介绍一些用于此目的的 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 代码时可能会看到它们。然而,对于大多数日常编码来说,它们并非必需。
属性是向对象添加“计算属性”的一种有用方式。在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()方法上的()。
修改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
>>>
修改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 中练习更多实验来提升你的技能。