很多面向对象的编程语言中都使用了 getter(也称为“accessors”)和 setter(又名“mutators”)来确保数据封装的原则。数据封装——正如我们在教程的面向对象编程的介绍中学到的那样——被视为数据与对它们进行操作的方法的捆绑。这些方法当然是用于检索数据的getter和用于更改数据的setter。根据这个原则,将类的属性设为私有以隐藏和保护它们。
不幸的是,人们普遍认为一个合适的 Python 类应该通过使用 getter 和 setter 来封装私有属性。一旦这些程序员中的一个引入了新属性,他或她就会将其设为私有变量并“自动”为该属性创建一个 getter 和一个 setter。这些程序员甚至可以使用编辑器或 IDE,它们会自动为所有私有属性创建 getter 和 setter。如果程序员使用公共属性,这些工具甚至会发出警告!Java 程序员在阅读以下内容时会皱起眉头,拧紧鼻子,甚至惊恐地尖叫: Pythonic 引入属性的方式是将它们公开。
我们稍后会解释这一点。首先,我们在下面的例子中演示,我们如何以 Javaesque 方式设计一个类,使用 getter 和 setter 来封装私有属性self.__x
:
类 P :
def __init__ ( self , x ):
self 。__x = x
def get_x ( self ):
返回 self 。__x
def set_x ( self , x ):
self 。__x = x
我们可以在以下演示会话中看到如何使用此类和方法:
from mutators import P
p1 = P ( 42 )
p2 = P ( 4711 )
p1 。get_x ()
输出:
42
p1 。set_x ( 47 )
p1 。set_x ( p1 . get_x () + p2 . get_x ())
p1 . get_x ()
输出:
4758
你如何看待“p1.set_x(p1.get_x()+p2.get_x())”这个表达式?很丑,不是吗?如果我们有一个公共属性 x,那么编写如下表达式会容易得多:
p1.x = p1.x + p2.x
这样的赋值比 Javaesque 表达式更容易编写,最重要的是更容易阅读。
让我们以 Pythonic 的方式重写类 P。没有 getter,没有 setter,self.__x
我们使用公共属性代替私有属性:
类 P :
def __init__ ( self , x ):
self 。x = x
很漂亮,不是吗?就三行代码,如果我们不计算空行!
从 p 导入 P
p1 = P ( 42 )
p2 = P ( 4711 )
p1 。X
输出:
42
p1 。x = 47
p1 。x = p1 。x + p2 。×
p1 。X
输出:
4758
“但是,但是,但是,但是,但是……”,我们可以听到他们的嚎叫和尖叫,“但是没有数据封装!” 是的,在这种情况下没有数据封装。在这种情况下我们不需要它。在我们的起始示例中,get_x 和 set_x 所做的唯一一件事就是“获取数据”,而无需额外执行任何操作。
但是如果我们想在未来改变实现会发生什么?这是一个严肃的论点。让我们假设我们想像这样改变实现:属性 x 可以有 0 到 1000 之间的值。如果分配的值大于 1000,x 应该设置为 1000。相应地,x 应该设置为 0,如果值小于 0。
很容易改变我们的第一个 P 类来解决这个问题。我们相应地更改 set_x 方法:
类 P :
def __init__ ( self , x ):
self 。set_x ( x )
def get_x ( self ):
返回 self 。__x
def set_x ( self , x ):
如果 x < 0 :
self 。__x = 0
elif x > 1000 :
self . __x = 1000
其他:
自我. __x = x
以下 Python 会话显示它以我们希望的方式工作:
从 mutators1 导入 P
p1 = P ( 1001 )
p1 。get_x ()
输出:
1000
p2 = P ( 15 )
p2 。get_x ()
输出:
15
p3 = P ( - 1 )
p3 。get_x ()
输出:
0
但有一个问题:让我们假设我们用 public 属性设计了我们的类,没有方法:
类 P2 :
def __init__ ( self , x ):
self 。x = x
人们已经使用了很多,他们编写了这样的代码:
p1 = P2 ( 42 )
p1 。x = 1001
p1 。X
输出:
1001
如果我们现在以类 P 的方式更改 P2,我们的新类将破坏接口,因为属性 x 将不再可用。这就是为什么在 Java 中建议人们只使用带有 getter 和 setter 的私有属性,这样他们就可以更改实现而不必更改接口。
但是 Python 为这个问题提供了解决方案。该解决方案称为属性!
具有属性的类如下所示:
类 P :
def __init__ ( self , x ):
self 。x = x
@property
def x ( self ):
返回 self 。__X
@x 。setter
def x ( self , x ):
如果 x < 0 :
self 。__x = 0
elif x > 1000 :
self . __x = 1000
其他:
自我。__x = x
用于获取值的方法用“@property”修饰,即我们将此行直接放在标题前面。必须用作 setter 的方法用“@x.setter”修饰。如果函数被称为“f”,我们将不得不用“@f.setter”来装饰它。有两点值得注意:我们只是在__init__
方法中放置了代码行“self.x = x”,并且属性方法x用于检查值的限制。第二个有趣的事情是我们写了“两个”具有相同名称和不同数量参数的方法“def x(self)”和“def x(self,x)”。我们在课程的前一章中了解到这是不可能的。由于装饰,它在这里工作:
从 p2 导入 P
p1 = P ( 1001 )
p1 。X
输出:
1000
p1 。x = - 12
p1 。X
输出:
0
或者,我们可以使用不带装饰器的不同语法来定义属性。如您所见,代码肯定没有那么优雅,我们必须确保__init__
再次在方法中使用 getter 函数:
类 P :
def __init__ ( self , x ):
self 。set_x ( x )
def get_x ( self ):
返回 self 。__x
def set_x ( self , x ):
如果 x < 0 :
self 。__x = 0
elif x > 1000 :
self . __x = 1000
其他:
自我. __x = x
x = 属性(get_x , set_x )
最新版本还有一个问题。我们现在有两种方法可以访问或更改 x 的值:使用“p1.x = 42”或“p1.set_x(42)”。这样我们就违反了 Python 的基本原则之一:“应该有一种——最好只有一种——明显的方法来做到这一点。” (见Python 之禅)
我们可以通过将 getter 和 setter 方法变成私有方法来轻松解决这个问题,我们的类 P 的用户无法再访问这些方法:
类 P :
def __init__ ( self , x ):
self 。__set_x ( x )
def __get_x ( self ):
返回 self 。__x
def __set_x ( self , x ):
如果 x < 0 :
self 。__x = 0
elif x > 1000 :
self . __x = 1000
其他:
自我. __x = x
x = 属性(__get_x , __set_x )
尽管我们通过使用私有的 getter 和 setter 解决了这个问题,但带有装饰器“@property”的版本是 Pythonic 的方式!
从我们到目前为止所写的内容以及在其他书籍和教程中也可以看到的内容,我们很容易得出这样的印象,即属性(或 mutator 方法)和属性之间存在一对一的联系,即每个属性都有或应该有自己的属性(或 getter-setter-pair),反之亦然。即使在 Python 之外的其他面向对象语言中,实现这样的类通常也不是一个好主意。主要原因是许多属性仅在内部需要,并且为类的用户创建接口不必要地增加了类的可用性。一个类的可能用户不应该被无数 - 主要是不必要的 - 方法或属性“淹没”!
以下示例显示了一个类,该类具有无法从外部访问的内部属性。这些是私有属性self.__potential
_physical
和self.__potential_psychic
. 此外,我们表明可以从多个属性的值中推导出一个属性。我们示例的属性“条件”以描述性字符串的形式返回机器人的条件。条件取决于机器人的精神值和物理条件的总和。
机器人类:
def __init__ ( self , name , build_year , lk = 0.5 , lp = 0.5 ):
self 。姓名 = 姓名
自我。build_year = build_year
self 。__potential_physical = lk
self 。__potential_psychic = LP
@property
DEF 条件(自):
š = 自我。__potential_physical + self 。__potential_psychic
if s <= - 1 :
返回 “我觉得很痛苦!”
elif s <= 0 :
返回 “我感觉不好!”
elif s <= 0.5 :
返回 “可能更糟!”
elif s <= 1 :
返回 “似乎没问题!”
否则:
返回 “太棒了!”
如果 __name__ == "__main__" :
, 1979 , 0.2 , 0.4 )
ÿ = 机器人(“卡利班” , 1993年, - 0.4 , 0.3 )
打印(X 。条件)
打印(Ý 。条件)
输出:
好像没问题!
我心情不好!
公共属性而不是私有属性
让我们总结一下私有和公共属性、getter 和 setter 以及属性的用法: 假设我们正在设计一个新类,并且我们正在考虑一个实例或类属性“OurAtt”,这是我们设计类所需要的。我们必须注意以下问题:
- 我们班级的可能用户是否需要“OurAtt”的值?
- 如果没有,我们可以或应该将其设为私有属性。
- 如果必须访问它,我们将其作为公共属性访问
- 当且仅当我们必须对数据进行一些检查或转换时,我们才会将其定义为具有相应属性的私有属性。(举个例子,你可以再看看我们的类P,其中的属性必须在0到1000之间的区间内,这是由属性“x”保证的)
- 或者,您可以使用 getter 和 setter,但使用属性是处理它的 Pythonic 方式!
假设我们将“OurAtt”定义为公共属性。我们的类已经被其他用户成功使用了一段时间。
class OurClass :
def __init__ ( self , a ):
self 。OurAtt = a
x = OurClass ( 10 )
打印( x . OurAtt )
输出:
10
现在是让一些传统的 OOPistas 吓得魂飞魄散的一点:想象一下,“OurAtt”已被用作整数。现在,我们的类必须确保“OurAtt”必须是 0 到 1000 之间的值?没有财产,这真是一个可怕的场景!由于属性,这很容易:我们创建了“OurAtt”的属性版本。
class OurClass :
def __init__ ( self , a ):
self 。OurAtt = 一个
@property
高清 OurAtt (个体经营):
回归 自我。__OurAtt
@OurAtt 。setter
def OurAtt ( self , val ):
如果 val < 0 :
self 。__OurAtt = 0
elif val > 1000 :
self 。__OurAtt = 1000
其他:
自我。__OurAtt = val
x = OurClass ( 10 )
打印( x . OurAtt )
输出:
10
这很棒,不是吗?您可以从可以想象到的最简单的实现开始,以后可以自由迁移到属性版本,而无需更改界面!所以属性不仅仅是 getter 和 setter 的替代品!
您可能已经注意到的另一件事:对于类的用户,属性在语法上与普通属性相同。