以目前对类Employee、Writer和Leader的定义,通过它们生成的对象在类外对属性进行修改是被允许的。这就产生了两个问题:一个是修改后其它在这个属性基础上生成的属性能自动更新么?第二个任意修改属性不怕输进来是“非法数据”么?对第一个问题的回答是“不能!”,对第二个问题的回答是“怕!”。
先来看第一个问题,在类外对Employee生成对象的属性进行修改:
class Employee:
……
def __init__(self, surname, fname, salary):
self.fname = fname
self.surname = surname
self.salary = salary
self.fullname = self.fname + ‘ ‘ + self.surname
self.email = self.fname + '.' + self.surname + This email address is being protected from spambots. You need JavaScript enabled to view it.'
def info_summary(self):
return '{}, {} \n{}'.format(self.fullname, self.salary, self.email)
……
employee2 = Employee('Bilbo', 'Baggins', 6000)
print(employee2.info_summary) (1)
employee2.fname = ‘Frodo’
print(employee2.fname) (2)
print(employee2.info_summary) (3)
修改.fname前info_summary的内容(1)是:
Bilbo Baggins, 6000
This email address is being protected from spambots. You need JavaScript enabled to view it.
(2)的输出结果是:Frodo,说明对属性的修改成功了
但是info_summary内容(3)却依然是:
Bilbo Baggins, 6000
This email address is being protected from spambots. You need JavaScript enabled to view it.
可见对象属性fname的更新虽然成功,但在它基础上生成的fullname和email属性却没有跟着进行更新。原因在于这两个属性是在初始化对象__init__()时被赋值的,fname的更新并没有激发对fullname和email的更新,所以还保持初始化时的值。
那么该怎样让这两个属性也跟着更新呢?
先把它们从初始化函数中拿出来,不做一锤子买卖;然后把对它们的赋值做成函数,这样每次引用属性都会调用函数重新对其赋值使其“与时俱进”。这种处理虽然解决了更新问题,但引用时却不得不在原来的形式的基础上加(),即由原来的.fullname和.email变成.fullname()和.email()。用起来很别扭,牵扯也有点大,类内类外凡是用到这个属性的位置都要找出来加()修改。如果在函数前加装饰器@property,引用时就可以把()拿掉了。装饰器@property会告诉python解释器,“虽然我是个函数,但我更是属性,调用时无需加()”
……
@property
def fullname(self):
return self.fname + ' ' + self.surname
@property
def email(self):
return self.fname + '.' + self.surname + This email address is being protected from spambots. You need JavaScript enabled to view it.'
……
employee2.fname = ‘Frodo’ (1)
print(employee2.info_summary) (2)
(1)对.fname修改后,(2)的输出是:
Frodo Potter, 4000
This email address is being protected from spambots. You need JavaScript enabled to view it.
可见背后偷偷进行函数调用后,属性.fullname和.email的内容果然“与时俱进”了。
对用户来说属性.fullname和.fname没什么区别,既然.fname可以在重新赋值,那么.fullname应该也可以啦。如果做一个employee2.fullname = ‘Thranduil’会怎样呢?会出现“AttributeError: can't set attribute”属性不能设置的错误。这个问题先放下,继续看下一个问题。
可以对属性.fname赋值,那么赋一个88 (employee2.fname = 88)会怎样呢?运行print(employee2.fullname)错误出现:“……line XX, in fullname return self.fname + ' ' + self.surname TypeError: unsupported operand type(s) for +: 'int' and 'str'”。因为fullname是将fname与surname用‘+’连起来,‘+’不能连接数字和字符串。这也回答了本章开始时提出的第二个问题:不加过滤地任意修改属性值会导致输入“非法数据”。为了不让外界随意修改属性值,在属性前加两个下划线把属性藏在类里面,访问不了自然乱改不成。
……
def __init__(self, fname, surname, salary):
self.__fname = fname
……
print(employee1.fname)
运行时的会出现没有这个属性的错误提示:“AttributeError: 'Employee' object has no attribute 'fname'”
print(employee1.__fname)
运行时的会出现没有这个属性的错误提示:“AttributeError: 'Employee' object has no attribute '__fname'”
类内的变量前加两个下划线__(如__fname)表明这是类私有变量,只在类内部使用,不允许在外面访问。类外无论是用.fname还是用.__fname都访问不了。
加一个下划线_(如_fname)表明这是半私有变量,类似于房门不上锁只是挂了个牌子“私人住宅勿进入”,但还是可以推门进入的。也就是说在类外可以访问._fname,而不会像直接访问全私有变量(.__fname)那样报错。
当然属性变量变私有不是为了不让访问,而是为了控制访问。变私有后的访问跟对属性fulltime更新的处理方式一样用对象方法函数来解决:函数前加装饰器@property,并且第一个形参设为self:
……
def __init__(self, fname, surname, salary):
self.__fname = fname
self.__surname = surname
self.__salary = salary
Employee.employeeNum += 1
@property
def fname(self):
return self.__fname
@property
def surname(self):
return self.__surname
@property
def salary(self):
return self.__salary
……
print(employ1.infoSummary)
从代码的运行结果可以看出这样处理后访问属性没有问题。不仅在类外可以正常访问,在类内部对属性的访问也直接用.fname,不需要改成.__fname。这样在email和fullname内部对fname和surname的引用都不用动。
访问属性值的问题解决了,那么如何有控制地改变属性的值呢?用装饰器@属性名.setter + 函数,第一个参数设为self,第二个参数接收实参(赋给属性的值)。比如为了给fullname属性赋值:
@fname.setter
def fname(self, value):
为了防止赋给属性的值是“非法数据”,在赋值函数内对输入数据的“合法性”进行检查。假设用形参value接收外界要赋的值,isinstance(value, str)可判断value是不是字符串(字符串类的实例是具体的字符串)。
@fname.setter
def fname(self, value): # 处理改变fname属性值的请求
if isinstance(value, str): # 只接受字符串类型的赋值
self.__fname = value
else:
print('名必须是字符串')
增加一个使得fullnames属性直接接收字符串的方法函数,允许将空格分隔名和姓的字符串赋给fullname直接改雇员姓名。可使用字符串方法函数split()将名和姓分开后分别赋给属性fname和surname。
除了@property和@函数名.setter装饰的属性读取和赋值函数内还需要全私有变量(比如__fname)外,类的其他部分都可以拿掉两个下划线了(比如直接使用fname)。这样做不只是容易输入,更是实现读取输入属性值都调用专门的读取赋值函数,降低错误发生几率,还可以对输入值的合法性做必要检查。
最后就是初始化时给属性一个初值,容许生成对象时只给部分实参的情况。
class Employee(object):
……
def __init__(self, fname=’Unknown’, surname’Unknown’, salary=0):
self.fname = fname
self.surname = surname
self.salary = salary
Employee.employeeNum += 1
@property
def fname(self): # 处理读取fname属性值的请求
return self.__fname
@fname.setter
def fname(self, value): # 处理改变fname属性值的请求
if isinstance(value, str): # 只接受字符串类型的赋值
self.__fname = value
else:
print('名必须是字符串')
@property
def surname(self): # 处理读取surname属性值的请求
return self.__surname
@surname.setter
def surname(self, value): # 处理改变surname属性值的请求
if isinstance(value, str): # 只接受字符串类型的赋值
self.__surname = value
else:
print('姓必须是字符串')
@property
def salary(self): # 处理改变salary属性值的请求
return self.__salary
@salary.setter
def salary(self, amount): # 处理改变salary属性值的请求
# 只接受整型和浮点型数据的赋值
if isinstance(amount, int) or isinstance(amount, float):
self.__salary = amount
else:
print('工资必须是整数或浮点数')
@property
def fullname(self): # 处理读取fullname属性值的请求
return self.fname + ' ' + self.surname
@fullname.setter
def fullname(self, value): # 处理改变fullname属性值的请求
if isinstance(value, str): # 只接受字符串类型的赋值
self.fname, self.surname = value.split()
else:
print('姓名必须是空格分隔的两个字符串')
@property
def email(self): # 处理读取email属性值的请求
return self.fname + '.' + self.surname + This email address is being protected from spambots. You need JavaScript enabled to view it.'
@email.setter
def email(self, value): # 处理改变email属性值的请求
print('邮件地址不接受赋值,会自动生成')
……
class Writer(Employee):
def __init__(self, fname, surname, salary, masterwork=’Unkonwn’):
super().__init__(fname, surname, salary)
self.masterwork = masterwork
@property
def masterwork(self): # 处理读取masterwork属性值的请求
return self.__masterwork
@masterwork.setter
def masterwork(self, value): # 处理改变masterwork属性值的请求
if isinstance(value, str): # 只接受字符串类型的赋值
self.__masterwork = value
else:
print('代表作必须是字符串')
……
class Leader(Employee):
def __init__(self, fname, surname, salary, subordinates=None):
Employee.__init__(self, fname, surname, salary)
self.subordinates = subordinates
@property
def subordinates(self): # 处理读取subordinates属性值的请求
return self.__subordinates
@subordinates.setter
def subordinates(self, value): # 处理改变subordinates属性值的请求
if value is None:
self.__subordinates = []
else:
if isinstance(value, list):
# 接受列表类型的赋值,实际上还应该进一步确认列表元素都是Employee类、Writer类和Leader类对象
self.__subordinates = value
else:
if isinstance(value, Employee) or isinstance(value, Writer) or isinstance(value, Leader):
# 接收Employee类、Writer类和Leader类对象,转成列表后进行赋值
self.__subordinates = [value]
else:
print('下属参数只接收雇员对象或雇员对象列表')
self.__subordinates = []
……
employee2.fname = 'Frodo'
print(employee2.info_summary())
运行结果:
Frodo Baggins, 6000
This email address is being protected from spambots. You need JavaScript enabled to view it.
employee1.surname = 88
运行结果:
姓必须是字符串
employee1.email = This email address is being protected from spambots. You need JavaScript enabled to view it.'
运行结果:
邮件地址不接受赋值,会自动生成
empWriter1.fullname = 'Charles Dickens'
empWriter1.masterwork = 'Oliver Twist'
print(empWriter1.info_summary())
运行结果:
Charles Dickens, 8000
This email address is being protected from spambots. You need JavaScript enabled to view it.
Oliver Twist
employee3 = Employee('Tom')
print(employee3.info_summary())
运行结果:
Tom Unknown, 0
This email address is being protected from spambots. You need JavaScript enabled to view it.
即将推出的Python ABC教程对PythonABC视频内容进行了梳理,修正了发现的错误、对代码做了些许优化、替换掉视频中的英文注释、替换掉国内不能访问的资源……敬请关注,谢谢
欢迎访问PythonABC网站:pythonabc.org