Haste makes waste

Python-Liao-05-面向对象(2)

Posted on By lijun

[Python-Liao-XX…]系列,系列根据廖雪峰的python3初级教程学习整理。

41. 实例属性和类属性

与java等中概念类似,如果一个成员变量是类共有(能通过类名访问,也能通过类的所有实例访问),这个成员变量就是类属性。

如果一个成员变量是对象被创建时生成,该成员变量在不同的对象中值不同,则该成员变量就是实例属性。

类属性与java中的public static final变量类似,实例属性就是类中定义的一般成员。

给实例动态绑定属性

因为python是动态语言,对象在被创建后,还能再追加属性。如下:

class Student(object):
    def __init__(self,name):
        self.name = name

bob = Student("bob")
bob.score = 99

类属性

上面给实例绑定了属性,但是只有该实例可以访问,如果要绑定一个属性,让所有类的实例都可以访问呢?

class Student(object):
    age_range = [7,8,9,10,11,12]
    def __init__(self,name):
        self.name = name
wangling = Student("wangling")
lijun = Student("lijun")

类的属性,可以通过类的实例,以及类名来访问。

wangling.age_range
[7, 8, 9, 10, 11, 12]
lijun.age_range
[7, 8, 9, 10, 11, 12]
Student.age_range
[7, 8, 9, 10, 11, 12]

注意:类属性名,与实例属性名,最好不要相同,属性名相同时,实例的属性优先。

通过del,可以删除某个实例对象的属性。

del wangling.name
wangling.name
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-9-97fa5287875b> in <module>()
----> 1 wangling.name


AttributeError: 'Student' object has no attribute 'name'

del 也可以删除类的属性。

del Student.age_range
Student.age_range
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-11-8ab09a787e02> in <module>()
----> 1 Student.age_range


AttributeError: type object 'Student' has no attribute 'age_range'

42. 面向对象高级编程-使用__slots__

通过上面的介绍,我们了解了在python中可以动态给类的实例添加属性,其实除了添加属性外,还能添加方法。

# 定义一个函数
def set_age(self,age):
    self.age = age
from types import MethodType
# 给实例绑定一个方法
wangling.set_age = MethodType(set_age, wangling)
# 实例调用方法
wangling.set_age(23)
wangling.age
23

注意,该实例绑定的方法,只能在该实例内使用。如下会出错:

lijun.set_age(24)
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-17-21bb8f58fbce> in <module>()
----> 1 lijun.set_age(24)


AttributeError: 'Student' object has no attribute 'set_age'

如果要在该类的所有实例中使用,需要绑定到类上,绑定到类上之后,所有实例都可以访问该方法:

# 给类绑定一个方法
Student.set_age = MethodType(set_age, Student)
lijun.set_age(24)

使用 __slots__ 限定动态绑定的属性

class Student(object):
    __slots__ = ("age","sex","name")
    def __init__(self,name):
        self.name = name
wangling2 = Student("wangling")
wangling2.age = 18
wangling2.sex = "female"

如果试图绑定没有在 __slots__中绑定的属性的话,会出现如下错误:

wangling2.hometown = "wuhan"
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-30-ae3267b09c2f> in <module>()
----> 1 wangling2.hometown = "wuhan"


AttributeError: 'Student' object has no attribute 'hometown'

44. 使用@property

如果通过实例直接绑定属性的话,是无法对属性进行错误检查的,为了进行错误检查,常见的方式就是在类中添加setter和getter方法。如下:

class Student(object):
    def get_score(self):
        return self._score
    def set_score(self,score):
        if not isinstance(score,int):
            raise ValueError("must be an int")
        if score < 0 or score > 100:
            raise ValueError("must be 0-100")
        self._score = score
wangling3 = Student()
wangling3.set_score(0)
wangling3.get_score()
0

通过上面的方法实现了属性的检查,但是使用的时候比较繁琐,需要去调用setter和getter方法,通过装饰器,实现通过实例.属性进行值的存取。

class Student(object):
    @property
    def score(self):
        return self._score
    
    @score.setter
    def score(self,score):
        if not isinstance(score,int):
            raise ValueError("must be an int")
        if score < 0 or score > 100:
            raise ValueError("must be 0-100")
        self._score = score
wangling4 = Student()
wangling4.score = 98 # 实际上转化为set_score(98)
wangling4.score # 实际上转化为get_score()
98
wangling4.score = 101
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-44-5bc5deb55c9e> in <module>()
----> 1 wangling4.score = 101


<ipython-input-38-530c14b67ef6> in score(self, score)
      9             raise ValueError("must be an int")
     10         if score < 0 or score > 100:
---> 11             raise ValueError("must be 0-100")
     12         self._score = score


ValueError: must be 0-100

如果只给一个属性设定getter,不设定setter的话,那这个属性就是只读属性。

@property广泛应用在类定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查。

45. 多重继承

通过多重继承,一个子类可以同时获得多个父类的功能。如下:

class Mammal(object):
    pass
class Runnable(object):
    pass
class Dog(Mammal,Runnable):
    pass

例如上面的子类Dog,继承了两个父类,则拥有了两个父类的功能。

MixIn

在设计类的继承关系时,通常,主线都是单一继承下来,如上面的Dog继承于Mammal,另外通过Runnable扩展了功能。如果还要混入额外的功能,通过多重继承可以实现,比如Dog如果还要混入 肉食动物功能的话,再定义一个父类 EatMeatMixIn用于继承,这种设计方式通常称之为 MixIn。

通过这种设计方式,我们不需要复杂的继承链条,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

在python中允许多重继承,因为MixIn是一种常见的设计方式,只允许单一继承的语言,比如java就不能使用MixIn的设计。 (虽然java不能多重继承,但是可以扩展某个接口,以达到类似的功能)

46. 定制类

在上面针对实例限制动态属性绑定时,用到了__slots__变量,类似_xxx_的变量或函数名要注意,这些在python中有特殊用途。 之前已经了解了__slots____len__()的用法,除此之外,python的class中还有很多这样特殊用途的函数,可以用来定制类。 与其他语言类似,覆盖掉基类中的默认方法,定制属于该类的方法和变量。

__str__自定义

class Dog2(Mammal,Runnable):
    def __len__(self):
        return 999
    def __str__(self):
        return "Dog2"
utasuke2 = Dog2()
print(utasuke2)
Dog2
len(utasuke2)
999

上面相当于重载了object中的len和str方法,如果没有重载的话,结果如下:

dog = Dog()
print(dog)
<__main__.Dog object at 0x0401F390>
len(dog)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-58-246878d32b07> in <module>()
----> 1 len(dog)


TypeError: object of type 'Dog' has no len()

但是还有个问题,如果不用print,直接输出utasuke2的话,仍是其内存地址,因为print调用的是str,而直接输出变量使用的是__repr__()

utasuke2
<__main__.Dog2 at 0x401f430>

简单一点的方式,即直接将__repr__指向__str__

class Dog3(Mammal,Runnable):
    def __str__(self):
        return "Dog3"
    __repr__ = __str__
utasuke3 = Dog3()
utasuke3
Dog3

__iter__自定义

如果一个类想被用于 for…in 循环,类似于list或tuple那样,就需要实现一个__iter__()方法,该方法返回一个迭代对象。然后python的循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个,知道遇到StopIteration错误时推出循环。

以斐波那契数为例,写一个Fib类,可以作用于for循环。

class Fib(object):
    def __init__(self):
        print("① init")
        self.a,self.b = 0,1

    def __iter__(self):
        print("② iter")
        return self

    def __next__(self):
        print("next")
        self.a,self.b = self.b,self.a + self.b
        if self.a > 100:
            print("③ 100 over")
            raise StopIteration()
        return self.a
for n in Fib():
    print(n)
① init
② iter
next
1
next
1
next
2
next
3
next
5
next
8
next
13
next
21
next
34
next
55
next
89
next
③ 100 over

__getitem__自定义

Fib实例虽然能作用于for 循环,看起来与list有点像,但是,把它当成list来使用还是不行的。 比如Fib()[5]

如果要实现像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
Fib()[10]
89

上面实现了下标索引,下面的代码实现了类似list的切片功能。

class Fib2(object):
    def __getitem__(self,n):
        if isinstance(n,int):
            a,b = 1,1
            for x in range(n):
                a,b = b,a+b
            return a
        if isinstance(n,slice):
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a,b = 1,1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a,b = b,a+b
            return L
Fib2()[0:5]
[1, 1, 2, 3, 5]

但是没有对step参数做处理,负数即反向切片也没有处理,可见要完整实现__getitem__还是有很多工作要做的。

上面举了__getitem__()的例子,类似的还有 setitem()和delitem()等,通过这些方法,使得自己定义的类表现得与python自带的list/dict等没有什么差别,这完全归功于动态语言的鸭子特性,即不需要强制继承某个接口

__getattr__自定义

class Student(object):
    def __init__(self):
        self.name = "uta"
utasuke = Student()
utasuke.name
'uta'
utasuke.score
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-99-4cd6c680a386> in <module>()
----> 1 utasuke.score


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

试图访问不存在的属性后,就出现了上述的错误,如果要避免上述错误,需要自定义__getattr__函数。

class Student2(object):
    def __init__(self):
        self.name = "uta"
    def __getattr__(self,attr):
        if attr == "score":
            return 999
utasuke = Student2()
utasuke.score
999

也可以返回一个函数,比如:

class Student3(object):
    def __init__(self):
        self.name = "uta"
    
    def __getattr__(self,attr):
        if attr == "age1":
            return lambda:18
utasuke = Student3()
utasuke.age1()
18

__getattr__ 的应用(REST API)

__call__自定义

47. 使用枚举类

python中,通常如果我们需要定义一个常量,会使用大写字母的方式,这种方式简单,但是缺点是类型为int,而且本质上仍然是个变量。

JAN = 1
FEB = 2

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

类名为Month,每个月份如Jan,都是类Month的一个实例。

from enum import Enum

Month = Enum("Month",("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"))

value 属性则是自动赋给成员的 int 常量,默认从 1 开始计数。

for name,member in Month.__members__.items():
 print(name,"=>",member,",",member.value)
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
type(Month)
enum.EnumMeta

每个常量都是class的一个唯一实例:

type(Month.Feb)
<enum 'Month'>

更精确地控制枚举类型

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

@unique 装饰器可以帮助我们检查保证没有重复值。

for name,member in Weekday.__members__.items():
 print(name,"=>",member,",",member.value)
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
Weekday.Mon 
<Weekday.Mon: 1>
Weekday["Mon"]
<Weekday.Mon: 1>
Weekday(1)
<Weekday.Mon: 1>
Weekday["Mon"].value
1

小结

Enum可以把一组相关常量定义在一个 class 中,且 class 不可变,而且成员可以直接比较。

48. 使用元类

1. 使用type()创建类

class Hello(object):
    def hello(self,name="world"):
        print("hello,%s" %name)
h = Hello()
h.hello()
hello,world
print(type(Hello))
<class 'type'>
print(type(h))
<class '__main__.Hello'>

上面Hello这个class,本质上也是一个对象,该对象由type()创建。 type()函数既可以返回一个对象的类型,有可以创建一个新的类型,比如:

  • 通过type()创建一个Hello类,而无需通过class Hello(object)…定义:
def fn(self,name="world"):
    print("Hello,%s" %name)

Hello = type("Hello",(object,),dict(hello=fn))
h = Hello()
h.hello()
Hello,world
print(type(Hello))
<class 'type'>
print(type(h)) 
<class '__main__.Hello'>

要使用type()创建一个class对象,type()函数要依次传入三个参数:

  1. class名词
  2. 继承的父类名,通过tuple可以定义多继承
  3. class 的方法名称与函数绑定,这里把函数 fn 绑定到方法名 hello 上

通过type()函数创建的类和直接写 class 是完全一样的,因为 Python 解释器遇到class定义时, 仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

2. 使用metaclass元类

一般要创建一个实例(即对象),需要先定义一个类,如果要创建一个类呢,必须要先定义metaclass。 所以最终的顺序就是,先定义metaclass,就可以创建类,创建了类就可以创建对象实例。

可以将类class看作是metaclass创建出的实例。

metaclass 是 Python 面向对象里最难理解,也是最难使用的魔术代码。 现阶段也不需要使用,暂时跳过。