python 面向对象高级编程

学习自廖雪峰https://www.liaoxuefeng.com/wiki/1016959663602400/1017496679217440

一、使用__slots__

通常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。

from types import MethodType 
class Student():
    pass
s = Student()
s.name = 'peter'#给实例绑定一个属性
print(s.name)
def set_age(self, age):#定义一个方法
    self.age = age
    
s.set_age = MethodType(set_age, s)#给实例绑定一个方法
s.set_age(25)#调用该方法
s.age#测试结果

---------------------------------------------------------------
Output:
peter
25

但是,给一个实例绑定的方法,对另一个实例是不起作用的。为了给所有实例都绑定方法,可以给class绑定方法。给class绑定方法后,所有实例均可调用。

class Student():
    pass
def set_score(self, score):
    self.score = score
    
Student.set_score = set_score#给class绑定方法
s = Student()
s.set_score(100)
print(s.score)
s2 = Student()
s2.set_score(200)#新建实例也可以调用该方法。
print(s2.score)
-------------------------------------------------------------
Output:
100
200

通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加nameage属性。

为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

class Student():
    __slots__ = ('name', 'age')
    
s = Student()
s.name = "peter"
s.age = 25
s.score = 64
-------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-38f55b9f0233> in <module>()
      5 s.name = "peter"
      6 s.age = 25
----> 7 s.score = 64

AttributeError: 'Student' object has no attribute 'score

由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。

使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:

class Student():
    __slots__ = ('name', 'age')
    
class GraduateStudent(Student):
    pass
g = GraduateStudent()
g.score = 25
g.score
------------------------------------------------------------
Output:
25

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

二、使用@property

为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数:

class Student(object):

    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
        
s = Student()
s.set_score(69)
print(s.get_score())
s.set_score(999)

Output:

69
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-22-d9fbe83b0781> in <module>()
     14 s.set_score(69)
     15 print(s.get_score())
---> 16 s.set_score(999)

<ipython-input-22-d9fbe83b0781> in set_score(self, value)
      8             raise ValueError('score must be an integer!')
      9         if value < 0 or value > 100:
---> 10             raise ValueError('score must between 0 ~ 100!')
     11         self._score = value
     12 

ValueError: score must between 0 ~ 100!

但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?

Python内置的@property装饰器就是负责把一个方法变成属性调用的.@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

class Student(object):
    
    @property
    def score(self):
        return self._score
    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0~100!')
        self._score = value

s = Student()
s.score = 69
print(s.score)
s.score = 999

Output:

69
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-26-1523b6c8acb2> in <module>()
     15 s.score = 69
     16 print(s.score)
---> 17 s.score = 999
     18 
     19 

<ipython-input-26-1523b6c8acb2> in score(self, value)
      9             raise ValueError('score must be an integer!')
     10         if value < 0 or value > 100:
---> 11             raise ValueError('score must between 0~100!')
     12         self._score = value
     13 

ValueError: score must between 0~100!

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student():
    
    @property
    def birth(self):
        return self._birth
    @birth.setter
    def birth(self, value):
        self._birth = value
        
    @property
    def age(self):
        return 2019 - self._birth
    
s =Student()
s.birth = 2018
print(s.birth)
s.age

------------------------------------------------------------
Output:
2018
1

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。只读属性通过其他属性计算出来,而不可被直接赋值。

作业:

class Screen(object):
    
    @property
    def width(self):
        return self._width
    @width.setter
    def width(self, value):
        self._width = value
        
    @property
    def height(self):
        return self._height
    @height.setter
    def height(self, value):
        self._height = value
        
    @property
    def resolution(self):
        return self._width * self._height
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('测试通过!')
else:
    print('测试失败!') 
---------------------------------------------------------
Output:
resolution = 786432
测试通过!

三、多重继承

继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。通过多重继承,一个子类就可以同时获得多个父类的所有功能。

MixIn

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

为了更好地看出继承关系,我们把RunnableFlyable改为RunnableMixInFlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。只允许单一继承的语言(如Java)不能使用MixIn的设计。

四、定制类

__str__ \ __repr__

class Student(object):
    
    def __init__(self, name):
        self.name = name
        
    def __str__(self):
        return 'Student object (name: %s)' %self.name
    
s = Student('Peter')
print(s)
----------------------------------------------------------------
Output:
Student object (name: Peter)

样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看.

这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

class Student(object):
    
    def __init__(self, name):
        self.name = name
        
    def __str__(self):
        return 'Student object (name: %s)' %self.name
    
    __repr__ = __str__

s = Student('Peter')
s
-------------------------------------------------
Output:
Student object (name: Peter)

__iter__

如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

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 > 10:
            raise StopIteration
        return self.a
for n in Fib():
    print(n)
---------------------------------------------------------------
Output:
1
1
2
3
5
8

__getitem__

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行。要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

class Fib(object):
    def __getitem__(self, key):
        a, b = 1, 1
        for n in range(key):
            a, b = b, a + b
        return a
f = Fib()
print(f[0],f[1],f[2],f[3],f[4],f[5],f[6])
---------------------------------------------------------
Output:
1 1 2 3 5 8 13

但是list有个神奇的切片方法,对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:

__getattr__

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。

注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。

此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

class Student(object):
    
    def __init__(self,):
        self.name = 'Peter'
        
    def __getattr__(self, attr):
        if attr == 'age':
            return lambda: 25
        raise AttributeError('\'Student\' object has no attibute \'%s\'' % attr)

s = Student()
print(s.name)
print(s.age())
print(s.score)
        

Output:

Peter
25
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-39-9b805140b98a> in <module>()
     12 print(s.name)
     13 print(s.age())
---> 14 print(s.score)
     15 
     16 

<ipython-input-39-9b805140b98a> in __getattr__(self, attr)
      7         if attr == 'age':
      8             return lambda: 25
----> 9         raise AttributeError('\'Student\' object has no attibute \'%s\'' % attr)
     10 
     11 s = Student()

AttributeError: 'Student' object has no attibute 'score'

这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。利用完全动态的__getattr__,我们可以写出一个链式调用:

class Chain(object):
    
    def __init__(self, path=''):
        self._path = path
        
    def __getattr__(self, path):
        return Chain("%s/%s" % (self._path, path))
    
    def __str__(self):
        return self._path
    
    __repr__ = __str__
    
    
Chain().status.users.list
-------------------------------------------
Output:
/status/users/list 

这块有点懵。。。

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。

任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。

class Student(object):
    
    def __init__(self, name):
        self.name = name
        
    def __call__(self):
        print('My name is %s.' % self.name)
        
s = Student('peter')
s()#直接调用了实例,就调用了该方法
---------------------------------------------------------
Output:
My name is peter.

五、使用枚举类

Python提供了Enum类。这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)
    
-----------------------------------------------------------------------------
Output:
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12

value属性则是自动赋给成员的int常量,默认从1开始计数。如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:@unique装饰器可以帮助我们检查保证没有重复值。

from enum import Enum, unique
@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

day1 = Weekday.Mon
print(day1)
print(Weekday(1))
print(Weekday['Tue'])
print(Weekday.Tue.value)
for name, member in Weekday.__members__.items():
    print(name, '=>', member, '=>', member.value)

Output:

Weekday.Mon
Weekday.Mon
Weekday.Tue
2
Sun => Weekday.Sun => 0
Mon => Weekday.Mon => 1
Tue => Weekday.Tue => 2
Wed => Weekday.Wed => 3
Thu => Weekday.Thu => 4
Fri => Weekday.Fri => 5
Sat => Weekday.Sat => 6

可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。

六、使用元类

真叫人头大。。。这章主要讲metaclass,直译为元类。先定义metaclass,就可以创建类,最后创建实例。所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况。所以这里也不继续往下写了,就稍微看看教程,以后遇到再说。。。

 

posted @ 2019-08-29 16:14  椰汁软糖  阅读(278)  评论(0编辑  收藏  举报