或许,读者也认为,Python 不能实现真正意义上的对象封装,从上一节内容已经看到,以单下划线开始的命名是“君子约定”,以双下划线开始的命名是“虚晃一枪”。如果来“真”的,Python 能行吗?
Python 没有像 Java 等某些语言那样,以 public
和 private
等关键词定义类,可以说所有的类都是 pbulic 的,8.7.1节介绍的以命名“私有化”形式实现封装,也不是 Java 语言中的 private
。但是,Python 中有一种方法,能够让程序中的对象更接近“封装”。
在 IDE 中编写名为 mypassword.py
的程序文件,其代码如下:
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.password = 'default'
def get_pwd(self):
return self.password
def set_pwd(self, value):
self.password = value
print("you have set your password.")
然后从此文件所在目录进入到 Python 交互模式(参阅8.5.2节所示方法,这种调试方法以后会经常用到,请读者务必掌握)。
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.get_pwd()
'default'
>>> laoqi.set_pwd('123')
you have set your password.
>>> laoqi.password
'123'
>>> laoqi.password = '456'
>>> laoqi.get_pwd()
'456'
从上面的操作可知,实例 laoqi
的密码可以通过属性 password
或者方法 get_pwd()
读取,也可以通过属性 password
或者方法 set_pwd()
重置。显然,这样对密码的管理是非常不安全的——要进行适当的“封装”,基本要求是:密码只能通过属性读取,不能通过属性重置,即是只读的。
将 mypassword.py
中的文件按照下面方式进行修改。
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.__password = 'default'
@property # (1)
def password(self): # (2)
return self.__password
def set_pwd(self, value):
self.__password = value
print("you have set your password.")
为了实现密码只读的需求,使用了注释(1)所示的装饰器 @property
——这个装饰器是基于内置函数 property()
,并且将原来的方法 get_pwd()
更名为 password()
(如注释(2)所示)。此外,将原来的实例属性 password
重命名为 __password
。重新载入 mypassword
模块(参阅8.5.2节,最简单的方法是在交互模式中执行 exit()
函数退出后,在进入交互模式),执行如下操作:
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.password = '123' # (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> laoqi.__password = '123' # (4)
>>> laoqi.password
'default'
通过实例 laoqi
调用 password
属性——注意,不是 laoqi.password()
方法。虽然注释(2)定义的是 password()
方法,但是此方法被 @property
装饰之后,就可以用同名的属性形式调用,并得到了默认的密码值。
注释(3)试图通过赋值语句修改密码,结果失败。但,注释(4)貌似成功了,其实这也没有修改 laoqi.password
的值,只是为实例 laoqi
增加了一个名为 __password
的实例属性。如此,实现了密码的“只读”功能。
当然,因为还遗留了 laoqi.set_pwd()
方法,调用它还是能重置密码的。
>>> laoqi.set_pwd('456')
you have set your password.
>>> laoqi.password
'456'
但是,这样实现重置,有点“太丑”了,还是用 laoqi.password = '456'
的方式重置更优雅——注释(3)的执行结果已经说明,不能用赋值语句重置。还有,明码保存是不是太不安全?重置密码之后,最好是能加密保存。
于是有了第二个需求:能够用赋值语句(类似注释(3)那样)重置密码(从用户角度将,重置密码是非常必要的),并且密码要加密保存——否则不称之为“密”码。
根据这些需要,再次修改 mypassword.py
文件中的代码。
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.__password = 'default'
@property
def password(self):
return self.__password
@password.setter # (5)
def password(self, value): # (6)
value = int(value) + 728 # 最低级的加密方法
self.__password = str(value)
print("you have set your password.")
注意观察修改内容。注释(5)增加了一个装饰器(注释写法),它的作用就是让注释(6)所定义的方法变成以属性赋值的形式。在注释(6)的方法里面,用了一种最拙劣的加密方法。
重载模块 mypassword
后(参阅8.5.2节),在交互模式中继续操作:
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.password = '456' # (7)
you have set your password.
>>> laoqi.password # (8)
'1184'
注释(7)实现了用赋值语句重置密码的需求,并且从注释(8)的返回值可知,注释(7)所提交的密码已经被“加密”。
由上述内容,已经初步理解了 @property
装饰器的一个作用:将方法转换为属性访问。就凭这个功能,它就能让程序“优雅”很多。比如:
#coding:utf-8
'''
filename: computearea.py
'''
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
if __name__ == "__main__":
rect = Rectangle(7, 8) # (9)
rect_area = rect.area() # (10)
print(f"width = {rect.width}, height = {rect.height}")
print(f"call rect.area() method to rectangle area = {rect_area}")
在此程序中,注释(9)创建了一个矩形实例,注释(10)得到了此矩形的面积。虽然这种写法能够实现功能,但是在追求“优雅”的 Python 开发者心目中,注释(10)是“丑陋的”。实例的宽度和长度,分别用属性 rect.width
和 rect.height
得到,那么面积,也应该是实例的属性,不应该是方法。所以用 rect.area()
计算面积,本身就不很“OOP”。如果用 rect.area
这样的属性形式得到实例的面积,那才符合 OOP 思想,并体现着 Python 的优雅,更蕴含着开发者的智慧。
用 @property
对程序稍作修改:
#coding:utf-8
'''
filename: computearea.py
'''
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property # 增加装饰器
def area(self):
return self.width * self.height
if __name__ == "__main__":
rect = Rectangle(7, 8)
#rect_area = rect.area() # 不再调用方法
print(f"width = {rect.width}, height = {rect.height}")
print(f"call rect.area attribute to rectangle area = {rect.area}")
执行结果:
% python computearea.py
width = 7, height = 8
call rect.area attribute to rectangle area = 56
装饰器 @property
是基于内置函数 property()
,这方面类似于8.4.2和8.4.3节使用过的类方法、静态方法装饰器。它不仅能能实现“属性”的读、写,还能实现删除功能。下面的示例中,读者进一步体会 @property
的作用。
#coding:utf-8
'''
temperature.py
'''
class Celsius:
def __init__(self, temperature=0):
self.__temperature = temperature
@property
def temperature(self):
return self.__temperature
@temperature.setter
def temperature(self, value):
if value < -273.15: # (11)
raise ValueError("Temperature below -273 is not possible")
self.__temperature = value
@temperature.deleter
def temperature(self):
raise AttributeError("Can't delete attribute")
进入到交互模式(参阅8.5.2节),进行如下操作:
>>> from temperature import *
>>> person = Celsius()
>>> person.temperature
0
>>> person.temperature = 36
>>> person.temperature = -300 # (12)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/qiwsir/Documents/my_books/Python完全自学手册/codes/temperature.py", line 16, in temperature
raise ValueError("Temperature below -273 is not possible")
ValueError: Temperature below -273 is not possible
>>> del person.temperature # (13)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/qiwsir/Documents/my_books/Python完全自学手册/codes/temperature.py", line 21, in temperature
raise AttributeError("Can't delete attribute")
AttributeError: Can't delete attribute
重点看注释(12)的操作结果,之所抛出异常,是因为在程序中注释(11)对值的大小进行了判断,如果条件满足,就执行 raise
语句(参阅第10章10.3节)。
再看注释(13)的执行结果,抛出了 AttributeError
异常,是因为用装饰器 @temperature.deleter
所装饰的方法中,执行了 raise
语句,即禁止删除 person.temperature
,如此对该对象给予“保护”。
★自学建议 学到本章是对读者的最大考验,一般的学习者会止步于本书第7章,对第8章及以后的内容望而却步。为什么?因为从本章开始,不仅要综合运用已学过的知识,还对日常以“直觉感受”为主的思考问题方式提出了挑战。在8.3节的【自学建议】中已经提到了“抽象能力”之于编写类的重要性,并且建议读者要“多练习”。 在这里进一步建议,要对原有的练习作品,用后续所学去优化。如果读者现在“回头看”从第1章以来做过的各种练习,或许对某些问题又有了新的思考,甚至于认为书中的代码也不怎样——这说明已经有了较高的欣赏和评价能力。除了批判之外,更要自己动手,先把我写的示例代码进行优化——别忘了告诉我,让我和其他读者都能进步。 还有,要“胆子大一些”,敢于自己设计(或者是“想象”)一些项目,并动手尝试“做一做”,哪怕最后不成功,自己也有了经验。 ”