Python学习笔记八 面向对象高级编程(一)
参考教程:廖雪峰官网https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000
一、使用__slots__
正常情况下,当定义了一个类之后,我们可以给这个类的实例绑定任何属性,这就是动态语言的优势:
class Student(object): def __init__(self,name,age): self.name=name self.age=age bob=Student('Bob',19) stam=Student('Stam',20) bob.score=98 #给示例bob增加了属性score可以访问 print('%s:%d years old,score is %d.'%(bob.name,bob.age,bob.score)) #实例stam并没有score属性,报错 print('%s:%d years old,score is %d.'%(stam.name,stam.age,stam.score))
同样也可以给类的实例绑定方法:
#先定义一个类的实例方法 def set_num(self,num): self.num=num #导入types库 from types import MethodType #使用绑定方法Methodtype() bob.set_num=MethodType(set_num,bob) bob.set_num(11) #输出11 print(bob.num) #报错,因为实例stam并没有绑定这个方法 stam.set_num(12) print(stam.num)
上述代码中给实例绑定方法的方式是先创建一个类的实例方法,然后再通过MethodType()把这个方法绑定到实例对象bob上。
另外一种给实例绑定方法的方式如下:
def sumfunc(x): print('this is sumfunc function!---%d'% x) bob.pfunc=sumfunc bob.pfunc(101)
而上述这种绑定方法只能针对具体的实例绑定 ,如果在一个类定义之后,发现这个类需要一种方法,也可以通过如下方式绑定:
#先定义一个Student类 #此时类中没有set_score方法 class Student(object): def __init__(self,name): self.name=name #创建类的实例 bob=Student('Bob') #此时发现需要给Student增加一个设置分数的方法 #先定义一个类的方法 def set_score(self,score): self.score=score #再将这个方法名直接赋给类的属性,为方便起见,当然是给类新增一个和这个方法同名的类方法 #此时,Student类不仅有了这个set_score方法,也有了score这个属性 Student.set_score=set_score bob.set_score(96) print(bob.score)
在实际应用中,我们往往可以实现知道某个类可能需要新增的属性,而可以通过某些手段限制类定义的属性,这里就需要用到'__slots__':
class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
通过给实例增加属性看一下__slots__的效果:
class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称 bob=Student() bob.name='Bob' print(bob.name) bob.age='16' print(bob.age) #以下代码出错,因为属性country不在__slots__范围里 bob.country='British' print(bob.country)
但__slots__仅对其所在的类的实例起作用,对这个类的所有子类实例是没有限制作用的,如果要限制子类的新增属性,需要在子类定义中的__slots__设置。而子类设置__slots__之后,其可以新增的属性不仅仅是其slots限定的范围,还可以包括父类slots所含的属性:
class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称 class Wstudent1(Student): pass Wangm=Wstudent1() #子类在本身没有__slots__限制的情况下,其实例新增属性不受父类__slots__限制 Wangm.num=16 print(Wangm.num) #因Wstudent2定义时通过__slots__限制了属性 #所以其实例新增属性受到父类和其本身的__slots__的限制 class Wstudent2(Student): __slots__=('country','score') Wanglei=Wstudent2() Wanglei.score=100 print(Wanglei.score) Wanglei.country='China' print(Wanglei.country) Wanglei.age=15 print(Wanglei.age) Wanglei.name='WangLei' print(Wanglei.name) #本身和父类的__slots__中均不含num属性,所以报错 Wanglei.num=1 print(Wanglei.num)
二、使用@property
"在这一节的学习中,发现自己对于类属性中访问限制一些概念理解得还不够透彻,因此在开始@property之前,先复习巩固一下访问限制的相关内容。"
首先看一下最简单、基础的代码:
class Student(object): def __init__(self,name,score): self.name=name self.score=score wanghai=Student('WangHai',99) liyue=Student('LiYue',100) print(wanghai.score) print(liyue.score) wanghai.score=95 liyue.name='LiYU' print(wanghai.score) print(liyue.name)
对于上面的代码,Student类有两个属性score和name,并且在类定义结束后,使用者可以随时修改实例的属性值。
那么,针对于此,怎样才可以让使用者不能随意修改呢?我们首先想到添加类方法set方法:
class Student(object): def __init__(self,name,score): self.name=name self.score=score def set_name(self,name): self.name=name def set_score(self,score): self.score=score wanghai=Student('WangHai',99) print(wanghai.score) #是不是真的不能修改了呢? wanghai.score=95 wanghai.name='wanghang' print(wanghai.name) print(wanghai.score)
可是运行结果可以发现,还是可以直接给实例的属性赋值修改。所以可见,set方法不能约束使用者直接修改属性值的行为。
那么有没有什么方法可以起到约束的作用呢。
答案是在定义类的时候在需要约束的属性名前增加两个下划线"__":
class Student(object): def __init__(self,name,score): self.__name=name self.score=score def set_name(self,name): self.__name=name def set_score(self,score): self.score=score wanghai=Student('WangHai',99) #正常输出99 print(wanghai.score) #下一句报错 print(wanghai.__name)
我们可以发现,通过对name属性名修改为'__name'之后,访问wanghai.__name报错,提示信息为类没有这个属性。
而访问score属性正常,因为并没有对score设置限制。
再进一步,是不是对于"__name"属性无法直接赋值修改了呢,上段代码中增加:
wanghai.__name="WH" print(wanghai.__name)
我们却奇怪的发现,居然代码可以执行,可以修改,并且正常输出了"WH"。这到底是怎么回事呢?
是因为对于设置属性名的访问限制方法,即增加双下划线的方法后,其实在Python解释器遇见这个属性名后,会在解释器内部再次修改这个属性名为:'_Student__name',而非代码中我们看到的"__name"!这也就可以解释为何上述代码可以执行了,因为上面代码实质上是给实例增加了一个__name属性,这个属性非类定义中的'__name"属性(类定义中的该属性名实际为_Student__name)。
如果类定义中有get方法,那通过get方法我们可以更清晰的看出上述代码增加后其实并没有改变类定义中的'__name'属性值:
class Student(object): #简便起见,仅考虑一个属性 def __init__(self,score): #这里的__score解释器转换为_Student__score self.__score=score def get_score(self): #这里的__score解释器转换为_Student__score return self.__score def set_score(self,score): self.__score=score #1、常规创建实例 wanghai=Student(99) #正常输出99 print(wanghai.get_score()) #通过set方法修改属性值 wanghai.set_score(100) #正常输出100 print(wanghai.get_score()) #尝试直接访问属性 #因为类定义中的__score非这里的__score #并且实例中也没有增加这个__score属性,所以报错类不存在这个__score print(wanghai.__score) #那么尝试直接通过_Student__name属性名直接访问 #可以正常输出,输出100 print(wanghai._Student__score) #继续测试,给实例增加__score属性 wanghai.__score=88 #此时增加的非类定义中的__score,所以是可以添加成功,并且可以直接访问 #输出88 print(wanghai.__score) #再看一下类定义中的__score属性值(实际为_Student__name) #输出依然是100,没有变化 print(wanghai.get_score()) print(wanghai._Student__score)
所以综上,小结一下:
1、如果要真正限制类定义中的属性的修改和访问,需要在类定义中的属性名前面增加"__",而这个方法的实质是在Python解释器解释类定义代码过程中将其真正的属性名视为"_类名__属性名",而这一点对于使用者是不可见的,所以对于使用者无法真正修改和访问这个属性。
2、对应于此,为规范代码,同时体现访问限制,类定义中须配套定义get和set方法。
3、其他:如要进一步限制使用者对于类属性的增加、属性值的修改这些,可以使用__slots__方法。
___________________________________________________________________________________________
接上,按照上述的一个规范的访问限制类定义的写法(按最简单的仅有一个属性的情况):
class Student(object): #简便起见,仅考虑一个属性 def __init__(self,score): #这里的__score解释器转换为_Student__score self.__score=score def get_score(self): #这里的__score解释器转换为_Student__score return self.__score def set_score(self,score): self.__score=score
并且在使用过程中,须自觉严格使用这些set和get方法;那么一个带来的烦恼就是每次使用都需要写实例名.set_属性名()或get_属性名(),相比直接通过'实例名.属性名'要麻烦一些,由此,Python提供了一种便利的方法,也是本节的重点:@property。
通过@property和@属性名.setter语句的添加可以实现直接使用'实例名.属性名'来访问或修改属性值。
同时,需要注意的是,当通过@装饰后,原先定义的set/get方法名同时需要修改为属性名(只是set/get对应的参数不同,分别是self和self、参数。
class Student(object): def __init__(self,score): self.__score=score @property def score(self): return self.__score @score.setter def score(self,score): self.__score=score wanghai=Student(96) print(wanghai.score) wanghai.score=99 print(wanghai.score)
针对上例,可以扩展成一个更为健壮的代码,在setter代码段中增加判断传入参数的合理性的代码:
class Student(object): def __init__(self,score): self.__score=score @property def score(self): return self.__score @score.setter def score(self,score): if not isinstance(score,int): raise ValueError('输入参数非数值!') elif score>100 or score<0: raise ValueError('输入数值参数应该在0~100范围内!') else: self.__score=score wanghai=Student(96) print(wanghai.score) wanghai.score='a' wanghai.score=109
也可以设置类的只读属性,这种情况下,只需要对需要设为只读的属性增加@property,无需对应设置'@属性名.setter'。
class Student(object): def __init__(self,name,score): self.__name=name self.__score=score #__name为只读属性 @property def name(self): return self.__name @property def score(self): return self.__score @score.setter def score(self,score): if not isinstance(score,int): raise ValueError('输入参数非数值!') elif score>100 or score<0: raise ValueError('输入数值参数应该在0~100范围内!') else: self.__score=score wanghai=Student('WangHai',96) print(wanghai.name) print(wanghai.score) wanghai.score=100 print(wanghai.score) #报错,name为只读属性 #wanghai.name='WH0' #下面代码虽然执行成功,但只是给实例新增了一个'__name'属性 wanghai.__name='WH1' #访问类定义中的属性,即_Student__name print(wanghai.name) print(wanghai._Student__name)
练习:
''' 请利用@property给一个Screen对象加上width和height属性 以及一个只读属性resolution ''' #这题有些没弄清的是属性名前面必须加下划线或者双下划线,否则就报错 #那就当一个习惯保持双下划线命名吧 class Screen(object): @property def width(self): return self.__width @property def height(self): return self.__height @property def resolution(self): return self.__width*self.__height @width.setter def width(self,width): if not isinstance(width,int): raise ValueError('参数必须是整数值!') else: self.__width=width @height.setter def height(self,height): if not isinstance(height,int): raise ValueError('参数必须是整数值!') else: self.__height=height # 测试: s = Screen() s.width = 1024 s.height = 768 print('resolution =', s.resolution) if s.resolution == 786432: print('测试通过!') else: print('测试失败!')
三、定制类
(一)__str__
在一个类的实例通过print()方法打印出来时总是类似于"<__main__.Student object at 0x109afb190>"的型式,不仅复杂,也不明确看出实例的具体信息。
那么在类的定义中,可以增加__str__方法,返回这个类的实例信息:
class Student(): def __init__(self,name): self.name=name def __str__(self): return 'Student object (name:%s).'% self.name #输出:Student object (name:Bob). print(Student('Bob'))
需要注意的是,上述__str__能替代的情况是Student('参数')这种情况,如果对于"变量名=Student('参数')"时,在命令行格式下,直接敲'变量名'还是返回的是原始信息,这是因为这种情况下,使用的是__repr__()而非__str__()。
如果要设置__repr__,最简单的方法如下:
class Student(): def __init__(self,name): self.name=name def __str__(self): return 'Student object (name:%s).'% self.name __repr__=__str__
(二)__iter__
如果一个类想通过for/in循环,类似于list或tuple,就必须实现一个__iter__()方法,这个方法返回一个迭代对象,然后Python的for循环就可以不断调用这个迭代对象的__next__()方法获取循环下一个值,直到遇到StopIteration错误。
class Fib(object): def __init__(self): self.a,self.b=0,1 def __iter__(self): return self def __next__(self): self.a,self.b=self.b,self.a+self.b if self.a<100: return self.a else: raise StopIteration() for i in Fib(): print(i)
(三)__getitem__
上面的Fib()类虽然可以通过for/in循环,但依然不能当成list使用,比如不能使用列表的下标访问元素。如果要实现这个功能,需要给这个类添加__getitem__方法。
class Fib(object): def __getitem__(self,n): a,b=1,1 for x in range(n): a,b=b,a+b return a f=Fib() print(f[0]) print(f[3]) print(f[13]) print(f[33]) #可以看出__getitem__()方法定义后自动也给类添加了for循环功能 for i in Fib(): if i<1000: print(i) else: break
(四)__getattr__
正常情况下如果调用一个不存在的类的属性或方法时就会报错。
如果要处理这个报错,那就需要写一个__getattr__方法,当调用不存在的属性时,Python解释器就会尝试去调用这个方法。
class Student(object): def __init__(self,name): self._name=name def __getattr__(self,attr): #可以根据情况设定有可能被使用者误访问的属性 if attr=='score': return 99 #对于其他情况,报错 else: raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) s=Student('Bob') #正常访问,输出Bob print(s._name) #通过getattr方法返回,输出99 print(s.score) #通过getattr方法,抛出错误提示 print(s.country)
(五)__call__
通常在调用类的实例的方法的时候,需要以“实例名.方法名()”的形式,那么能不能直接通过“实例名()”直接调用呢?在类定义中只需要加一个__call__()方法,就可以实现:
class Student(object): def __init__(self,name): self._name=name def __call__(self): print('instance\'s name is %s' % self._name) def sp(self): print('Student\'s another method for printing 12345!') Student('Bob')() Student('Stam')() Student('Steve').sp()
通过代码我们可以发现,当定义了__call__()方法后,实例名()对应的就是call方法,而对于其他类中定义的方法还需要原始的写法才可以使用。
在实例直接调用后,这里实例对象就类似于函数,而实质上这两者也没有本质的区别。对于带有__call__()定义的类的实例,它就是一个Callable对象(可调用对象),同样函数也是一个Callable对象。
class Student0(object): def __init__(self,name): self._name=name def __call__(self): print('instance\'s name is %s' % self._name) class Student1(object): def __init__(self,name): self._name=name s0=Student0('Bob') s1=Student1('Bob') #s0是Callable对象,输出True print(callable(s0)) #s1不是Callable对象,输出False print(callable(s1)) #max是函数,输出True print(callable(max)) #输出False print(callable(1))
四、使用枚举类
枚举类型可以看做是一系列常量的集合,在Python中可以通过字典、类去实现,在通过字典或者常规的类定义的话,无法避免对其的修改,因此在Python3.4后的版本提供了enum库,可以定义枚举类型的类(枚举类)。
from enum import Enum #注意在定义枚举类的时候基类参数需要设置为Enum #星期的枚举类共有7个成员 #每个成员都有名字和值 #定义成员的时候,成员名字不能相同 #成员的值可以相同 #但相同的值的成员,后面的成员名被视为第一个对应该值的成员的别名 class Weekday(Enum): Monday=1 Mon=1 #Mon被视为Monday的成员 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7 #打印成员 print(Weekday.Mon) print(Weekday.Sunday) #遍历成员 for x in Weekday: #获取成员名 print(x.name) #获取成员值 print(x.value) #获取成员信息 print(x)
而如果对于具有同一值的多个成员名的情况,需要全部遍历出来,则需要用到__members__属性:
''' 输出: ('Monday', <Weekday.Monday: 1>) ('Mon', <Weekday.Monday: 1>) ('Tuesday', <Weekday.Tuesday: 2>) ('Wednsday', <Weekday.Wednsday: 3>) ('Thursday', <Weekday.Thursday: 4>) ('Friday', <Weekday.Friday: 5>) ('Saturday', <Weekday.Saturday: 6>) ('Sunday', <Weekday.Sunday: 7>) ''' for x in Weekday.__members__.items(): print(x)
枚举类也可以限制为不可以设定同一值的多个成员名,则需要import进unique模块,并且在枚举类定义前加上装饰器@unique
from enum import Enum,unique @unique class Weekday(Enum): Monday=1 #Mon=1,这句代码就不能设置了,会报错 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7
在for循环遍历中通过替代变量可以直接获取各个成员,如果知道某个成员的名字或者值,那么也可以获取该成员。
from enum import Enum class Weekday(Enum): Monday=1 Mon=1 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7 #通过成员名获取成员 print(Weekday['Tuesday']) print(Weekday['Monday']) print(Weekday['Mon']) #这里也可以看出Mon是Monday的别名 #通过成员值获取成员 print(Weekday(6))
练习:
''' 把Student的gender属性改造为枚举类型,可以避免使用字符串 ''' from enum import Enum,unique @unique class Gender(Enum): Male=0 Femal=1 class Student(object): def __init__(self,name,gender): self._name=name self._gender=gender # 测试: bart = Student('Bart', Gender.Male) if bart._gender == Gender.Male: print('测试通过!') else: print('测试失败!')