博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
(XWZ)的Python学习笔记Ⅲ——面向对象高级编程
阅读量:2173 次
发布时间:2019-05-01

本文共 15021 字,大约阅读时间需要 50 分钟。


目录

 


面向对象高级编程

__slots__

  1. 在上一篇文章讲到过,我们可以通过简单的操作就能够给实例或类绑定属性。这里我们来讨论如何给实例和类绑定方法,先来看个例子:
    from types import MethodTypeclass Dog():    def __init__(self, name):        self.Name = namedef show(self):    print('my name\'s %s' % self.Name)d = Dog('Dolly')

     需要注意的是:若是通过下面这种方式给实例绑定方法则在调用方法时,解释器就不会自动把本实例作为一个参数传入self中了,也就是不会将实例和方法进行绑定

    In [2]: d.show = showIn [3]: d.show()Traceback (most recent call last):  File "
    ", line 1, in
    d.show()TypeError: show() missing 1 required positional argument: 'self'

     正确的绑定操作应该是这样:

    In [5]: d.show = MethodType(show, d) #通过MethodType函数给实例d绑定方法In [6]: d.show() #调用刚刚绑定的方法my name's Dolly

    从例子中可以看到,我们通过MethodType函数来给实例绑定方法。这里我们仅仅是给实例d绑定了show方法,也就是说其它的实例并没有show方法:

    In [8]: d2 = Dog('Mike')In [9]: d2.show()Traceback (most recent call last):  File "
    ", line 1, in
    d2.show()AttributeError: 'Dog' object has no attribute 'show'

    为了解决这个问题,我们可以尝试给类绑定方法,因为类的方法是所有实例所共有的,给类绑定方法非常简单:

    In [10]: def bark(self):    ...:     print('wang wang wang !')    ...:     In [11]: Dog.bark = bark

    现在我们来试试:

    In [12]: d.bark()wang wang wang !In [13]: d2.bark()wang wang wang !

     

  2. 有时我们想给实例绑定的属性添加一个限定,使得属性的绑定不能是任意的,而只能绑定我们规定的几种属性。这就要使用一个特殊的变量——__slots__:
    class  Dog():    __slots__ = ('name', 'gender') #限定Dog的实例只能够绑定name和gender这两个属性

    现在我们来试试给实例绑定属性:

    In [2]: d = Dog()In [3]: d.name = 'Dolly' #给d绑定name属性In [4]: d.nameOut[4]: 'Dolly'In [5]: d.gender = 'femal' #给d绑定gender属性In [6]: d.genderOut[6]: 'femal'In [7]: d.color = 'yello' #给d绑定color属性Traceback (most recent call last):  File "
    ", line 1, in
    d.color = 'yello'AttributeError: 'Dog' object has no attribute 'color'

    我们看到在给实例d绑定name和gender这两个属性时没问题,在绑定属性color时就抛出了错误,因为color这个属性是不被允许的!如果再定义一个Dog的子类Husky,并且Husky类中没有对__slots__进行声明,那么Husky的实例对属性的绑定是不受其父类Dog影响的,可以任意绑定;如果Husky中对__slots__也进行了声明,那么Husky实例所允许绑定的属性是两个声明的__slots__属性之和

@property

  1. 通过对前面知识的学习,我们在给实例绑定属性时一般进行类似以下操作:
    In [13]: class Dog():    ...:     def __init__(self, name):    ...:         self._name = name #绑定属性_name    ...:     In [14]: d = Dog('Dolly')In [16]: d.age = 999999 #绑定属性age

    但是这样会产生的一个问题是:如果没有任何措施加以限制的话,属性值是可以改成任意值的,就比如上述例子中将age设为999999,这明显不符合实际!也许我们可以定义一个set_age()方法用于修改属性值,在方法中对属性的取值加以限制:

    class Dog():    def get_age(self):        return self.age        def set_age(self, val):        if not isinstance(val, int): #检查val的类型            raise ValueError('年龄只能为整数!')        elif val < 0 or val > 30: #检查val的取值            raise ValueError('年龄只能在0~30之间!')        self.age = val

    现在我们来试试:

    In [18]: d = Dog()In [19]: d.set_age('六岁')Traceback (most recent call last):  File "
    ", line 1, in
    d.set_age('六岁') File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in set_age raise ValueError('年龄只能为整数!')ValueError: 年龄只能为整数!In [20]: d.set_age(35)Traceback (most recent call last): File "
    ", line 1, in
    d.set_age(35) File "C:/Users/Whisky/.spyder-py3/temp.py", line 12, in set_age raise ValueError('年龄只能在0~30之间!')ValueError: 年龄只能在0~30之间!In [21]: d.set_age(6)In [22]: d.get_age()Out[22]: 6

    其实心细的同学会发现这没从根本上解决问题,我们还是可以随意更改age的值,只要不通过调用set_age()方法就行了:

    In [24]: d.age = 100In [25]: d.ageOut[25]: 100

    那么到底怎么解决这个问题呢?这就要使用Pyhton提供的@property装饰器了!先来看下面的例子:

    class Dog():    @property    def age(self):        return self._age ##这里不要写成self.age        @age.setter    def age(self, val):        if not isinstance(val, int):            raise ValueError('年龄必须为整数值!')        elif val < 0 or val > 30:            raise ValueError('年龄只能在0~30间')        self._age = val ##这里不要写成self.age = val

    Python内置的@property装饰器能够把方法转成属性的调用,而另一个装饰器@age.setter能够把方法转成属性的赋值,来看下怎么使用:

    In [21]: d = Dog()In [22]: d.age = 6 #实际上是转化为d.set_age(6)In [23]: d.age #实际上是转化为d.get_age()Out[23]: 6In [27]: d.age = 999 #实际上是转化为d.set_age(999)Traceback (most recent call last):  File "
    ", line 1, in
    d.age = 999 File "C:/Users/Whisky/.spyder-py3/temp.py", line 14, in age raise ValueError('年龄只能在0~30间')ValueError: 年龄只能在0~30间

    我们看到,在使用d.age = 999对age的值进行修改时,实际上是转化为执行d.set_age(999),而999是age不能接受的取值故抛出错误。_在两个方法中我们使用的属性是_age而非age,其实将_age改成其他的名字也行,但就是不能写成和age一样!!!自己试试看会出现什么情况!这实际上是给属性age取了个别名_age,试试看直接访问_age会是什么结果:

    In [24]: d._ageOut[24]: 6In [25]: d._age = 999In [26]: d.ageOut[26]: 999

    可以看到,我们其实还是可以直接修改age,你非要给age胡乱的设值也没办法,所以只能要求你遵守编程规范,按规矩办事😅。从上面这些例子我们可以到,@property修饰的方法是用于“读”,而@属性.setter修饰的方法是用于“写”。所以如果要创建一个只读属性那么就只需要添加@property装饰器:

    class Dog():    @property    def age(self):        return self._age        @age.setter    def age(self, val):        if not isinstance(val, int):            raise ValueError('年龄必须为整数值!')        elif val < 0 or val > 30:            raise ValueError('年龄只能在0~30间')        self._age = val        @property     def birth(self): #birth为只读属性        return 2020 - self.age
    In [58]: print(Dog.birth)
    In [59]: d.birthOut[59]: 2014In [60]: d.birth = 1996 #尝试修改只读属性的值Traceback (most recent call last): File "
    ", line 1, in
    d.birth = 1996AttributeError: can't set attribute

    我们可以用一句话来总结这两个装饰器的功能:@property能够使我们在访问属性时按照我们自己规定的方式进行,@属性.setter能够使我们在设定属性的值时按照我们自己规定的方式进行

 

定制类

  1. __str__()。当我们在类中实现了__str__()方法后,在使用print()函数打印某个对象时,会自动调用__str__()函数:
    class Dog():    def __init__(self, name):        self._name = name        def __str__(self):        return 'hello, my name\'s %s' % self._name
    In [70]: d = Dog('Dolly')In [71]: print(d)hello, my name's Dolly

    那么直接在控制台输入d会打印出什么结果呢?试试看:

    In [72]: dOut[72]: <__main__.Dog at 0x2976bdb2f28>

    我们希望输出的和print(d)结果一样,实际上这两者理应是一样的。这时就需要实现__repr__()方法了,也许你会这样实现:

    class Dog():    def __init__(self, name):        self._name = name        def __str__(self):        return 'hello, my name\'s %s' % self._name        def __repr__(self):        return 'hello, my name\'s %s' % self._name

    但其实上只需要这样就行了:

    class Dog():    def __init__(self, name):        self._name = name        def __str__(self):        return 'hello, my name\'s %s' % self._name        __repr__ = __str__

    实现__repr__()是针对开发者,也就是说,__repr__()是为调试服务的;而__str__()是针对用户的,返回用户能够靠的字符串。

  2. __iter__()。在第一篇文章中讲到过:凡是能用于for循环的就是Iterable类型,凡是能够调用__next__()方法的就是Iterator类型,Iterator也属于是Iterable类型。如果想要自己写的类也能用于for...in,那就要在类中实现__iter__()方法,这样,在使用for循环时就会不断调用__next__()方法不断生成下一个元素,直到抛出StopIteration为止

    class Fib():    def __init__(self):        self.a, self.b = 0, 1        def __iter__(self):        return self        def __next__(self):        if self.b >= 500:            raise StopIteration()        self.a, self.b = self.b, self.a + self.b        return self.b

    现在来试试:

    In [117]: f = Fib()In [118]: for x in f:     ...:     print(x)     ...:     123581321345589144233377610

    那么f是否是Iterable和Iterator类型的呢:

    In [119]: from collections import IterableIn [123]: from collections import IteratorIn [124]: isinstance(f, Iterable)Out[124]: TrueIn [125]: isinstance(f, Iterator)Out[125]: True

     

  3. __getitem__()。如果要让我们的类能像list,str那样能用下标对元素进行访问,那么就要实现__getitem__()方法:

    class Fib():    def __getitem__(self, idx):        a, b = 1, 1        for i in range(idx):            a, b = b, a + b        return a
    In [139]: f = Fib()In [140]: f[10]Out[140]: 89In [141]: f[100]Out[141]: 573147844013817084101

    如果要能够像list,str那样能使用切片呢?那么就要对__getitem__()再进行修改:

    class Fib():    def __getitem__(self, oj):        if isinstance(oj, int): #判断传入的对象是否是int类型            a, b = 1, 1            for i in range(oj):                a, b = b, a + b            return a        elif isinstance(oj, slice): #判断传入的类型是否是slice类型            a, b = 1, 1            L = []           #定义一个list用于保存结果            start = oj.start            stop = oj.stop            if start == None:                start = 0            if stop == None: #默认设置stop最大为1000                stop = 1000            for i in range(start):                a, b = b, a + b            for i in range(start, stop):                L.append(a)                a, b = b, a + b            return L

    试试看:

    In [147]: f[0]Out[147]: 1In [148]: f[2]Out[148]: 2In [149]: f[:20]Out[149]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

    这里只是实现了简单的切片操作,没有对步长进行处理,也没有考虑一些异常情况。

  4. __getattr__()。我们先定义一个类,并创建个实例:

    class Dog():    def __init__(self, name):        self._name = named = Dog('Dolly')

    当访问该实例已绑定的属性时自然没有问题:

    In [186]: d._nameOut[186]: 'Dolly'

    而访问不存在的属性时就会抛出错误:

    In [187]: d._genderTraceback (most recent call last):  File "
    ", line 1, in
    d._genderAttributeError: 'Dog' object has no attribute '_gender'

    为此,python中提供了__getattr__()应对此问题。我们可以在类中实现该方法,这样,在访问到不存在的属性时就会自动调用该方法生成属性,比如:

    class Dog():    def __init__(self, name):        self._name = name        def __getattr__(self, attr): #调用该方法,自动生成属性,注意!这里的attr是字符串(经传入的属性名转成了字符串形式)        if attr == '_gender':            return 'male' #会将_gender赋值为'male'
    In [226]: Dog('Dolly')._genderOut[226]: 'male'

    该方法默认响应所有的属性,如果属性在__getattr__()中没经过处理,默认返回的是None,试试看:

    In [239]: hasattr(d, 'age') #测试是否有age属性Out[239]: TrueIn [240]: hasattr(d, 'color') #测试是否有color属性Out[240]: TrueIn [241]: d.age #默认返回None,在控制台None是不显示的In [242]: d.color #默认返回None,在控制台None是不显示的In [243]: d._gender #经过处理,返回给定的值Out[243]: 'male'

    若要规定只响应规定的几个属性,那么就要这样做

    class Dog():    def __init__(self, name):        self._name = name        def __getattr__(self, attr):        if attr == '_gender':            return 'male'        raise AttributeError("'Dog'的实例没有绑定该属性!")
    In [246]: d = Dog('Mike')In [247]: d._genderOut[247]: 'male'In [248]: hasattr(d, 'color') #测试是否有color属性Out[248]: FalseIn [249]: d.color #访问未绑定的color属性Traceback (most recent call last):  File "
    ", line 1, in
    d.color File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的实例没有绑定该属性!")AttributeError: 'Dog'的实例没有绑定该属性!In [250]: d.age #访问未绑定的age属性Traceback (most recent call last): File "
    ", line 1, in
    d.age File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的实例没有绑定该属性!")AttributeError: 'Dog'的实例没有绑定该属性!

     

  5. __call__()。python中还提供了一个很有趣的方法__call__(),我们在类中实现这个方法就能够像调用函数一般来调用对象了:

    class Dog():    def __call__(self, breed):        print('I\'m %s!' % breed)
    In [268]: d = Dog()In [269]: d('Husky')I'm Husky!

    这样看起来好像对象和函数就没什么区别了,所以我们就可以将对象看成是函数,将函数看成是对象,实际上这两者本来就没啥区别!既然如此,我们怎么判断一个对象是否能够像函数那样被调用呢?可以通过判断该对象是否是Callable类型,能被调用就是Callable类型,比如函数和实现了__call__()方法的类实例:

    In [270]: callable(d)Out[270]: TrueIn [271]: callable(abs)Out[271]: TrueIn [272]: callable(123)Out[272]: FalseIn [273]: callable('hello')Out[273]: False

     

  6. 在本节的最后来看个链式调用的例子:

    class Chain():    def __init__(self, info):        self._info = info        def __str__(self):        return self._info        def __call__(self):        return Chain(self._info)        def __getattr__(self, attr):        return  Chain(self._info)        def test(self):        return Chain(self._info)
    In [283]: print(Chain('链式调用测试!').test().user1.user2())链式调用测试!

    相信大家已经看出来了,就这么一句代码将类中定义的方法全部都用上了!!现在我们来对这句代码进行剖析。首先“Chain('链式调用测试!')”是创建个Chain实例(为了方面叙述,将这里的实例记为INST1),“.test()”是调用该实例的test的方法,而该方法返回的是Chain实例(将该实例记为INS2),这里相信大家都能够看得懂。接下来“.user1”是访问INS2的属性user1,但是INS2并没有预先绑定该属性,因此这里就调用了INS2的__getattr__()方法,该方法得到的也是一个Chain实例(将该实例记为INS3),接着,同样也先是访问INS3的user2属性,但INS3没有预先绑定该属性,则调用__getattr__()返回一个实例(记为INS4),大家发现没,这里对该实例是以函数的方式调用的,因此调用INS4的__call__()方法,同样返回一个实例(记为INS5),最后回到最外层的print()函数对INS5进行打印调用INS5的__str__()方法,该方法返回INS5的info属性。至此代码运行完毕!!简单的一行代码藏了很深的知识!!!理解这个链式调用栗子,那么这一节的知识就算是弄通了!!!!

 

 使用枚举类

  1. 枚举类型可以看成是一些标签的集合,比如:月份包含一到十二个月,星期包含周一到周日,颜色包含红橙黄绿青蓝紫等等。python中提供了Enum类来实现枚举类型,而我们可以通过继承Enum来定制我们自己的枚举类型,比如:
    from enum import Enum, uniqueclass Month(Enum):    Jan =  1    Feb = 2    Mar = 3    Apr = 4    May = 5    Jun = 6    Jul = 7    Aug = 8    Sep = 9    Oct = 10    Nov = 11    Dec = 12

    这里要注意,①我们无法直接实例化枚举类,枚举类中的成员称为单例,都是枚举类型的;②每个标签都被赋予一个固定的值,标签的值也是不能够更改的

    In [336]: m = Month() #尝试实例化枚举类Traceback (most recent call last):  File "
    ", line 1, in
    m = Month()TypeError: __call__() missing 1 required positional argument: 'value'In [337]: isinstance(Month.Jan, Month) Out[337]: TrueIn [338]: Month.Jan = 13Traceback (most recent call last): File "
    ", line 1, in
    Month.Jan = 13 File "E:\Anaconda3\lib\enum.py", line 386, in __setattr__ raise AttributeError('Cannot reassign members.')AttributeError: Cannot reassign members.

    标签中的值可以重复,值重复的多个标签会被认为是具有多个名字的同一个标签,若要限定不同标签值不能重复则要使用@unique装饰器

    from enum import Enum, unique@uniqueclass Month(Enum):    Jan =  1    Feb = 2    Mar = 3    Apr = 4    May = 5    Jun = 6 #值重复    Jul = 6 #值重复    Aug = 6 #值重复    Sep = 9    Oct = 10    Nov = 11    Dec = 12

    由于标签Jun、Jul、Aug三个标签的值重复,抛出以下错误:

    ValueError: duplicate values found in 
    : Jul -> Jun, Aug -> Jun

    枚举类型访问方式有多种:

    In [354]: Month.Feb #由标签访问值Out[354]: 
    In [355]: Month['Nov'] #由标签访问值Out[355]:
    In [356]: Month(12) #由值访问标签Out[356]:
    In [357]: for m in Month: #这种方式对于值相同的标签只打印一次 ...: print(m) ...: Month.JanMonth.FebMonth.MarMonth.AprMonth.MayMonth.JunMonth.JulMonth.AugMonth.SepMonth.OctMonth.NovMonth.DecIn [358]: for m in Month.__members__: #这种方式会打印出所有标签,包括值相同的标签 ...: print(m) ...: JanFebMarAprMayJunJulAugSepOctNovDecIn [359]: for name, member in Month.__members__.items(): ...: print(name, '=>', member) ...: Jan => Month.JanFeb => Month.FebMar => Month.MarApr => Month.AprMay => Month.MayJun => Month.JunJul => Month.JulAug => Month.AugSep => Month.SepOct => Month.OctNov => Month.NovDec => Month.Dec

    这里的特殊属性__members__是一个将名称映射到成员的有序字典,也可以通过它来完成遍历,大家可以自己试试遍历__members__.keys()、__members__.values()和__members__.items()看看会得到什么结果。

使用元类

  1. 前面我们用到过type()这个函数,可以用来判断一个对象的类型:
    In [43]: type(1)Out[43]: intIn [44]: type('hello')Out[44]: strIn [45]: type((1,))Out[45]: tupleIn [46]: type({})Out[46]: dict

    它还有另一个功能——能够创建类。之前我们创建类都是用下面这种形式:

    class Dog():    def __init__(self, name):        self._name = name

    其实在运行过程中,解释器只是扫描了下语法,最终还是要通过调用type()函数来创建,下面来看下如何用type来创建类:

    In [63]: def fun(self): #先定义函数    ...:    print('hello world!')    ...:    In [64]: x = 1 In [66]: A = type('A', (object,), dict(show = fun, _x = x)) #调用type函数创建类AIn [67]: A._x #访问A的类属型Out[67]: 1In [68]: A().show() #调用方法hello world!

    type()需要传入三个参数:①类名;②所继承的父类; ③绑定的类属型和方法。

  2. 除了用type函数创建类,我们还可用元类(metaclass)定制我们自己的类,metaclass就是类的模板。我们知道,对象是类的实例,其实我们也可以将类理解为metaclass的“实例”。下面看看元类怎么使用:

    class MyMetaclass(type): #定义元类    def __new__(cls, name, bases, attr):        attr['show'] = lambda self: print('hello world!') #给类添加show方法        attr['x'] = 7 #给类添加属性x        bases = (A,) #让类继承A        return type.__new__(cls, name, bases, attr)class A(): #类A    def __print__(self):        print('I\'m \'A\'')

    定义元类和定义一般的类是一样的方式,但是记得要继承type(其实所有的类都是type类型的),接着我们要在元类中实现一个__new__方法,__new__方法中的参数含义分别是:①当前要创建的类,相当于我们在定义一般类时的self; ②类名; ③类需要继承的父类集合;④dict类型,类方法和集属性集合。接下来创建类B,同时传入关键字参数metaclass,表明使用我们定义的MyMetaclass来定制类:

    class B(metaclass = MyMetaclass):    pass

    接下来进行测试:

    In [78]: b = B()In [79]: B.xOut[79]: 7In [80]: b.show()hello world!In [81]: b.__print__()I'm 'A'

     

 

转载地址:http://hmhzb.baihongyu.com/

你可能感兴趣的文章
解决Ubuntu14.04 - 16.10版本 cheese摄像头灯亮却黑屏问题
查看>>
解决Ubuntu 64bit下使用交叉编译链提示error while loading shared libraries: libz.so.1
查看>>
Android Studio color和font设置
查看>>
Python 格式化打印json数据(展开状态)
查看>>
Centos7 安装curl(openssl)和libxml2
查看>>
Centos7 离线安装RabbitMQ,并配置集群
查看>>
Centos7 or Other Linux RPM包查询下载
查看>>
运行springboot项目出现:Type javax.xml.bind.JAXBContext not present
查看>>
Java中多线程向mysql插入同一条数据冲突问题
查看>>
Idea Maven项目使用jar包,添加到本地库使用
查看>>
FastDFS集群架构配置搭建(转载)
查看>>
HTM+CSS实现立方体图片旋转展示效果
查看>>
FFmpeg 命令操作音视频
查看>>
问题:Opencv(3.1.0/3.4)找不到 /opencv2/gpu/gpu.hpp 问题
查看>>
目的:使用CUDA环境变量CUDA_VISIBLE_DEVICES来限定CUDA程序所能使用的GPU设备
查看>>
问题:Mysql中字段类型为text的值, java使用selectByExample查询为null
查看>>
程序员--学习之路--技巧
查看>>
解决问题之 MySQL慢查询日志设置
查看>>
contOS6 部署 lnmp、FTP、composer、ThinkPHP5、docker详细步骤
查看>>
TP5.1模板布局中遇到的坑,配置完不生效解决办法
查看>>