类和对象

以目前对类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()将名和姓分开后分别赋给属性fnamesurname

除了@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