Python 笔记09
构造方法与属性
这篇教程我们一起了解构造方法和属性。
构造方法在之前的教程中我们已经接触过。
它就是__init()__这个方法。
特别说明:在Python中,这种两侧带有下划线的方法称为魔法方法或特殊方法,它们都有一些特殊的用途。
对象的创建就是通过构造方法来完成的,它的主要功能是完成对象的初始化。
当实例化一个类的对象时,会自动调用构造方法。
构造方法和其他方法一样也可以重写。
不过需要注意,重载构造方法,有的时候会出现问题。
例如,我们在超类中为构造方法定义了某个特性,而在子类中也为构造方法定义了某个特性,这个时候构造方法被重写。
当我们通过子类实例化的对象,调用超类中的特性时,会找不到这个特性,从而引发异常。
示例代码:(错误示例)
class SuperClass:
def __init__(self): # 构造方法
self.name = '小楼'
class SubClass(SuperClass):
def __init__(self): # 重写构造方法
self.learn = 'Python'
def say(self):
print('我是%s,我在学习%s。' % (self.name, self.learn))
SubClass().say() # 抛出异常
运行上方代码,会抛出异常。
AttributeError: ‘SubClass’ object has no attribute ‘name’
特性错误:’SubClass’类的对象不包含特性 ‘name’
那么,如何解决这个问题?
super函数的使用
我们可以使用super函数,通过super函数访问超类的特性。
示例代码:
class SubClass(SuperClass):
def __init__(self): # 重写构造方法
super().__init__()
self.learn = 'Python'
def say(self):
print('我是%s,我在学习%s。' % (self.name, self.learn))
SubClass().say() # 显示输出结果为:我是小楼,我在学习Python。
并且,不管有多少个超类,都只需要一个super()函数,就能够访问所有超类的特性。
但是,需要为每一个超类的构造方法使用super()函数才可以。
class SuperClass1:
def __init__(self): # 构造方法
super().__init__() # 调用super函数
self.name = '小楼'
class SuperClass2:
def __init__(self):
super().__init__() # 调用super函数
self.sex = '帅哥'
class SubClass(SuperClass1, SuperClass2):
def __init__(self): # 重写构造方法
super().__init__() # 调用super函数
self.learn = 'Python'
def say(self):
print('我是%s%s,我在学习%s。' % (self.sex, self.name, self.learn))
SubClass().say() # 显示输出结果为:我是帅哥小楼,我在学习Python。
访问器与属性
在之前的教程中,我们接触过访问器,就是类中定义的get_name()和set_name()这样的函数,用于控制特性的设置和读取。
这样定义的函数,我们是希望能够在设置和读取特性时,能够做一些其他的事情。
示例代码:
class Shape: # 定义形状类
def get_height(self): # 定义获取特性的函数
return self.height # 返回特性值
def set_height(self, height): # 定义修改特性的函数
if isinstance(height, (int, float)): # 验证参数类型
self.height = height # 设置特性值
else:
raise TypeError # 引发类型异常
上方代码中,我们在修改特性时,加入了对参数类型验证的语句,如果输入的参数是整数或小数,能够修改特性的值,否则,就会引发异常。
特别说明:代码中除非必须尽量回避isinstance()函数,它不是多态的。
不过,并不是所有的特性都会通过私有的访问器进行访问的控制。
接下来,我们再来看一段代码。
对于立方体来说,有长、宽、高的特性,如果我们提供了体积的元组,这些长宽高的特性能够动态获取。
示例代码:
class Cube: # 定义立方体类
def __init__(self):
self.length = 0
self.width = 0
self.height = 0
def set_cubage(self, cubage): # 定义修改特性的函数
if len([i for i in cubage if isinstance(i, (int, float))]) == 3: # 验证参数类型,验证通过的数值形成列表。
self.length, self.width, self.height = cubage # 设置特性值
else:
raise TypeError # 引发类型异常
def get_cubage(self): # 定义获取特性的函数
return self.length, self.width, self.height # 返回特性值
cube = Cube()
cube.set_cubage((10, 10, 10))
print(cube.get_cubage()) # 显示输出结果为:(10, 10, 10)
cube.length = 20
print(cube.get_cubage()) # 显示输出结果为:(20, 10, 10)
在上方这段代码中,体积不是特性。
如果我们想把体积也变成特性,那么,需要将体积特性放在访问器。
示例代码:(错误示例)
def set_cubage(self, cubage):
if len([i for i in cubage if isinstance(i, (int, float))]) == 3:
self.cubage = cubage # 新增特性
self.length, self.width, self.height = cubage
else:
raise TypeError
def get_cubage(self):
return self.cubage # 修改返回值为新增特性
cube = Cube()
cube.set_cubage((10, 10, 10))
print(cube.get_cubage()) # 显示输出结果为:(10, 10, 10)
cube.length = 20
print(cube.get_cubage()) # 显示输出结果没有改变,仍然为:(10, 10, 10)
经过修改后的代码,虽然将新特性放入了访问器,但是,当修改长度特性时,结果没有变化。
这是因为修改length特性,并没有调用set_cubage()方法,所以get_cubage()方法的返回值不变。
那怎么解决这个问题呢?
如果,将每个特性都放入访问器,在设置特性的方法中重新绑定cubage的值,是能够正常实现的。
但是,这样的话,整个代码会变得很乱。
而且,导致使用了这个类的客户程序(使用代码的代码)也都需要修改。
例如,设置长度就不能再使用cube.length这样的特性进行设置,而需要使用cube.set_length()这样的方法。
其实,只加入一句代码来解决这个问题。
示例代码:
cubage = property(get_cubage,set_cubage)
上方代码中,cubage是新定义的特性,后方的property函数将两个访问器(注意顺序)绑定到这个特性,这样就可以正常使用新特性了。
我们测试一下。
示例代码:
cube = Cube()
cube.set_cubage((10, 10, 10))
print(cube.get_cubage()) # 显示输出结果为:(10, 10, 10)
cube.length = 20
print(cube.get_cubage()) # 显示输出结果为:(20, 10, 10)
cube.cubage=(30,40,50)
print(cube.get_cubage()) # 显示输出结果为:(30, 40, 50)
不管通过set_cubage()方法,还是直接设置特性,都能够正常返回结果。
property()实际上不是函数,它是一个类,所有的工作由这个类的特殊方法来完成。
property(fget=None,fset=None,fdel=None,fdoc=None)具有四个参数,这四个参数分别是特性的获取、设置、删除特性的方法以及说明文档字符串。
如果没有参数,属性不可读写;只写入一个参数,则属性为只读;写入两个参数,属性可以读写。
通过property()函数定义的特性(变量),称为属性。
而普通的的特性(变量),称为字段。
所以,属性就是包含了访问器的字段(变量)。
那么,到此为止我们了解到类的实例特性包含:字段、属性与方法。
一些魔法方法
__getattribute__(self,name):获取特性值时自动调用的方法,参数name填入特性名称。
__getattr__(self, item):通过调用来获取特性值的方法,参数填入特性名称。
__setattr__(self,key,value):设置特性值时自动调用的方法,参数key填入特性名称,参数value填入特性的值。
__delattr__(self,name):删除特性的方法,参数name填入特性名称。
__dict__:包含一个字典,存储实例的所有特性,包括新增的特性。
这些魔法方法,我们可以重写来满足我们的一些需求。
示例代码:(getattr,setattr,delattr__,dict)
class Class:
def __init__(self, sex, age):
self.sex = sex
self.age = age
def __getattr__(self, item): # 重写getattr方法
if self.sex == '女' and item == 'age': # 符合指定条件的情形
return '保密' # 返回指定的值
else: # 其他情形
return self.__dict__[item] # 从字典获取返回值
def __setattr__(self, key, value): # 重写setattr方法
if key == 'age' and value <= 0: # 符合指定条件的情形
raise ValueError('年龄数值错误!') # 抛出异常
else:
self.__dict__[key] = value # 修改字典中的值
def __delattr__(self, item): # 重写delattr方法
del self.__dict__[item] # 删除字典中指定的键值
cls = Class('男', 18)
print("我是%s孩,我的年龄%s。" % (cls.__getattr__('sex'), cls.__getattr__('age')))
# 显示输出结果为:我是男孩,我的年龄18。
cls.__setattr__('sex', '女')
cls.__setattr__('age', 16)
print("我是%s孩,我的年龄%s。" % (cls.__getattr__('sex'), cls.__getattr__('age')))
# 显示输出结果为:我是女孩,我的年龄保密。
cls.__delattr__('sex')
print(cls.__dict__) # 显示输出结果为:{'age': 16}
示例代码:(getattribute)
class Class:
def __init__(self, sex, age):
self.sex = sex
self.age = age
def __getattribute__(self, item): # 重写getattr方法
attr = super().__getattribute__
if attr('sex') == '女' and item == 'age': # 符合指定条件的情形
return '保密' # 返回指定的值
else: # 其他情形
return attr(item) # 返回原始的值
cls = Class('男', 18)
print("我是%s孩,我的年龄%s。" % (cls.sex, cls.age)) # 显示输出结果为:我是男孩,我的年龄18。
cls.sex = '女'
cls.age = 16
print("我是%s孩,我的年龄%s。" % (cls.sex, cls.age)) # 显示输出结果为:我是女孩,我的年龄保密。
上方代码中,要格外注意:在重写方法时,读取特性不能通过“self.特性”的方式去获取,否则会引发异常。
因为,通过“self.特性”的方式获取时,会自动调用__getattribute__()方法自身,这就会导致不停的循环调用。
所以,一定要通过超类的方法进行获取,这里使用了super函数。
其实,还有另外一种方法,就是通过object这个超类去调用。
因为,在Python 3(注意版本)中定义的类,都会继承object类(即便没有显示继承)。
示例代码:(方法部分)
def __getattribute__(self, item): # 重写getattr方法
attr = object.__getattribute__
if attr(self,'sex') == '女' and item == 'age': # 符合指定条件的情形
return '保密' # 返回指定的值
else: # 其他情形
return attr(self,item) # 返回原始的值
注意:使用object类去调用方法的时候,需要提供两个参数,第一个是self,第二个是特性名称。