Python学习笔记(七)--面向对象高级编程

来源:互联网 发布:矩阵潜袭 基础 编辑:程序博客网 时间:2024/06/06 03:04

Python学习笔记(七)--面向对象高级编程

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

 

定义Class:

classStudent(object):

    pass

s= Student()

比如尝试给实例s绑定一个方法:

>>>def set_age(self, age): # 定义一个函数作为实例方法

...     self.age = age

 

>>> from types import MethodType

>>> s.set_age =MethodType(set_age, s)# 给实例绑定一个方法

>>>s.set_age(25) # 调用实例方法

>>>s.age # 测试结果

25

但是,给一个实例绑定的方法,对另一个实例是不起作用的。

 

为了给所有实例都绑定方法,可以给class绑定方法:

>>>def set_score(self, score):

...     self.score = score

...

>>>Student.set_score = set_score

给class绑定方法后,所有实例均可调用:

>>>s.set_score(100)

>>>s.score

100

>>>s2.set_score(99)

>>>s2.score

99

动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。

 

 

 

 

 

一、           使用__slots__

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

 

class Student(object):

    def __init__(self,name,score):
        self.name = name
        self.score = score
    __slots__ = ('name', 'score')
    def print_score(self):
        print(self.score)

s = Student('aaa aa',80)
s.print_score()
print(s.name)
s.age = 20     # AttributeError: 'Student'object has no attribute 'age'

 

 

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

ClassGraduate(Student):

       Pass

>>>m= Graduate()

>>>m.age= 20           # right

 

 

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

 

 

第一,slots只能限制添加属性,不能限制通过添加方法来添加属性:

defset_city(self, city):

    self.city=city

 

classStudent(object):

    __slots__ = ('name', 'age', 'set_city')

    pass

 

Student.set_city= MethodType(set_city, Student)

 

a= Student()

a.set_city(Beijing)

a.city

上段代码中,Student类限制两个属性name 和 age,但可以通过添加方法添加一个city属性(甚至可以添加很多属性,只要set_city方法里有包括)

 

第二,属性分实例属性和类属性,多个实例同时更改类属性,值是最后更改的一个

defset_age(self, age):

    self.age=age

 

classStu(object):

    pass

 

s=Stu()

a=Stu()

 

fromtypes import MethodType

Stu.set_age=MethodType(set_age,Stu)

 

a.set_age(15)       \\通过set_age方法,设置的类属性age的值

s.set_age(11)     \\也是设置类属性age的值,并把上个值覆盖掉

print(s.age,a.age) \\由于a和s自身没有age属性,所以打印的是类属性age的值

 

a.age= 10  \\给实例a添加一个属性age并赋值为10

s.age= 20  \\给实例b添加一个属性age并赋值为20

\\这两个分别是实例a和s自身的属性,仅仅是与类属性age同名,并没有任何关系·

 

print(s.age,a.age)  \\打印的是a和s自身的age属性值,不是类age属性值

所以, 1,slots并不能严格限制属性的添加,可通过在方法里定义限制之外的属性来添加本不能添加的属性(当然,前提是方法没有被限制) 2,类属性是公共属性,所有实例都可以引用的,前提是实例自身没有同名的属性,因此类属性不能随意更改(别的实例可能在引用类属性的值),就是说不能随便用a.set_age()更改age的值(因为调用此方法更改的是类属性age的值,不是实例a自身的age属性值)

 

 

 

 

 

 

二、           使用@property

@property装饰器:负责把一个方法变成属性调用。

 

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改:

s = Student()

s.score = 9999

 

为了限制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 mustbe an integer!')

        if value < 0 or value> 100:

            raise ValueError('score mustbetween 0 ~ 100!')

        self._score = value

 

对于类的方法,装饰器decorator一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):
    @property
    def score
(self):
        return self._score

    @score.setter

    def score(self, value):
        if not isinstance(value,int):
            raise ValueError('scoremust be an integer!')
        if value < 0 or value> 100:
            raise ValueError('scoremust be 0~100!')
        self._score = value


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

 

>>> s = Student()

>>> s.score = 60 # OK,实际转化为s.set_score(60)

>>> s.score # OK,实际转化为s.get_score()

60

>>> s.score = 9999

Traceback (most recent calllast):

 ...

ValueError: score must between 0~ 100!

 

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

 

 

注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。

 

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

 

class Student(object):

    @property

    def birth(self):   #getter

        return self._birth

    @birth.setter      #setter

    def birth(self, value):

        self._birth = value

 

    @property   #getter

    def age(self):

        return 2015 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。

 

 

 

# coding: utf-8
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
# test:
s = Screen()
s.width = 1024
s.height = 768
print(s.resolution)
assert s.resolution == 786432, '1024 * 768 = %d ?' % s.resolution       #assert后面的表达式为false的时候才会返回预设的字符串。并且还会给你标注出错的行。

 

 

三、           多重继承

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

 

classBat(Mammal, Flyable):

    pass

 

 

MixIn

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

classDog(Mammal, RunnableMixIn, CarnivorousMixIn):

    pass

 

 

 

举个例子,Python自带了TCPServer和UDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn和ThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。

比如,编写一个多进程模式的TCP服务,定义如下:

classMyTCPServer(TCPServer, ForkingMixIn):

    pass

编写一个多线程模式的UDP服务,定义如下:

classMyUDPServer(UDPServer, ThreadingMixIn):

    pass

如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:

classMyTCPServer(TCPServer, CoroutineMixIn):

    pass

这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

 

由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。

只允许单一继承的语言(如Java)不能使用MixIn的设计。

 

 

四、           定制类

这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。

1、  __slots__

2、  __len__能让class作用于len()函数。

3、  __str__

>>>class Student(object):

...     def__init__(self, name):

...         self.name = name

>>>print(Student('Michael'))

<__main__.Studentobject at 0x109afb190>

打印出一堆<__main__.Studentobject at 0x109afb190>,不好看。

怎么才能打印得好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了:

>>> classStudent(object):

...     def __init__(self, name):

...         self.name = name

...     def __str__(self):

...         return 'Student object (name:%s)' % self.name

>>>print(Student('Michael'))

Student object (name: Michael)

 

 

但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:

>>> s =Student('Michael')

>>> s

<__main__.Student object at0x109afb310>

这是因为直接显示变量调用的不是__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__

 

4、  __iter_ _:

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

# eg斐波那契数列

class Fib(object):

    def __init__(self):

        self.a, self.b = 0, 1 # 初始化两个计数器a,b

 

    def __iter__(self):

        return self # 实例本身就是迭代对象,故返回自己

 

    def __next__(self):

        self.a, self.b = self.b, self.a +self.b # 计算下一个值

        if self.a > 100000: # 退出循环的条件

            raise StopIteration()

        return self.a # 返回下一个值

 

 

 

5、  __getitem_ _

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

 

>>> Fib()[5]

Traceback (most recent calllast):

 File "<stdin>", line 1, in <module>

TypeError: 'Fib' object does notsupport indexing

 

要表现得像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()

>>> f[0]

1

 

 

但是list有个神奇的切片方法:

>>>list(range(100))[5:10]

[5, 6, 7, 8, 9]

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

class Fib(object):

    def __getitem__(self, n):

        if isinstance(n, int): # n是索引

            a, b = 1, 1

            for x in range(n):

                a, b = b, a + b

            return a

        if isinstance(n, slice): # n是切片

            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

现在试试Fib的切片:

>>> f = Fib()

>>> f[0:5]

[1, 1, 2, 3, 5]

>>> f[:10]

[1, 1, 2, 3, 5, 8, 13, 21, 34,55]

但是没有对step参数作处理:

>>> f[:10:2]

[1, 1, 2, 3, 5, 8, 13, 21, 34,55, 89]

也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。

此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,例如str。

与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

 

 

6、  __getattr_ _

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

class Student(object):

 

    def __init__(self):

        self.name = 'Michael'

 

    def__getattr__(self, attr):

        if attr=='score':

            return 99

>>> s = Student()

>>> s.name

'Michael'

>>> s.score

99

当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self,'score')来尝试获得属性,这样,我们就有机会返回score的值。

返回函数也是完全可以的:

def__getattr__(self, attr):

        if attr=='age':

            return lambda: 25

只是调用方式要变为:

>>> s.age()

25

 

注意:只有在没有找到属性的情况下,才调用__getattr__。

 

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

class Student(object):

 

    def __getattr__(self, attr):

        if attr=='age':

            return lambda: 25

        raise AttributeError('\'Student\' objecthas no attribute \'%s\'' % attr)

 

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。作用就是,可以针对完全动态的情况作调用。

 

7、  Call

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

class Student(object):

    def __init__(self, name):

        self.name = name

 

    def __call__(self):

        print('My name is %s.' % self.name)

调用方式如下:

>>> s =Student('Michael')

>>> s() # self参数不要传入

My name is Michael.

 

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别

 

通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

 

 

 

 

 

 

五、           使用枚举类

from enum importEnum

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

得到Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员

 

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

 

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

fromenum 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装饰器可以帮助我们检查保证没有重复值。

 

访问这些枚举类型可以有若干种方法:

>>> day1 = Weekday.Mon

>>> print(day1)

Weekday.Mon

>>> print(Weekday.Tue)

Weekday.Tue

>>> print(Weekday['Tue'])

Weekday.Tue

>>> print(Weekday.Tue.value)

2

>>> print(day1 ==Weekday.Mon)

True

>>> print(day1 ==Weekday.Tue)

False

>>> print(Weekday(1))

Weekday.Mon

>>> print(day1 ==Weekday(1))

True

>>> Weekday(7)

Traceback (most recent calllast):

 ...

ValueError: 7 is nota valid Weekday

>>> for name,member in Weekday.__members__.items():

...     print(name, '=>', member)

Sun => Weekday.Sun

Mon => Weekday.Mon

Tue => Weekday.Tue

Wed => Weekday.Wed

Thu => Weekday.Thu

Fri => Weekday.Fri

Sat => Weekday.Sat

 

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

 

1. 枚举的定义

  1. 首先,定义枚举要导入enum模块。
  2. 枚举定义用class关键字,继承Enum类。
  3. 用于定义枚举的class和定义类的class是有区别【下一篇博文继续分享】。

  示例代码:

from enum import Enum


class Color(Enum):

    red = 1

    orange = 2

    yellow = 3

    green = 4

    blue = 5

    indigo = 6

    purple = 7

  代码分析:

  1. 上面的代码,我们定义了颜色的枚举Color.
  2. 颜色枚举有7个成员,分别是Color.red、Color.orange、Color.yellow等。
  3. 每一个成员都有它们各自名称和值,Color.red成员的名称是:red,值是:1。
  4. 每个成员的数据类型就是它所属的枚举。【*注:用class定义的类,实际上就是一种类型】

1.1 定义枚举时,成员名称不允许重复   

from enum import Enum


class Color(Enum):

    red = 1

    red = 2

  上面的代码,就无法执行。提示错误:TypeError:Attempted to reuse key: 'red'

 1.2 默认情况下,不同的成员值允许相同。但是两个相同值的成员,第二个成员的名称被视作第一个成员的别名  

from enum import Enum


class Color(Enum):

    red = 1

    red_alias = 1

  成员Color.red和Color.red_alias具有相同的值,那么成员Color.red_alias的名称red_alias就被视作成员Color.red名称red的别名。

   1.3 如果枚举中存在相同值的成员,在通过值获取枚举成员时,只能获取到第一个成员

from enum import Enum


class Color(Enum):

    red = 1

    red_alias = 1

 

print(Color(1))

  输出结果为:Color.red

 1.4如果要限制定义枚举时,不能定义相同值的成员。可以使用装饰器@unique【要导入unique模块】

from enum import Enum, unique

 

 

@unique

class Color(Enum):

    red = 1

    red_alias = 1

  再执行就会提示错误:ValueError:duplicate values found in <enum 'Color'>: red_alias -> red

 

2. 枚举取值 

 2.1 通过成员的名称来获取成员

Color['red']

 2.2 通过成员值来获取成员

Color(2)

 2.3 通过成员,来获取它的名称和值

red_member = Color.red

red_member.name

red_member.value

 

3. 迭代器

 3.1枚举支持迭代器,可以遍历枚举成员

for color in Color:

    print(color)

  输出结果是,枚举的所有成员。Color.red、Color.orange、Color.yellow、Color.green、Color.blue、Color.indigo、Color.purple。

 3.2 如果枚举有值重复的成员,循环遍历枚举时只获取值重复成员的第一个成员

 

from enum import Enum

 

 

class Color(Enum):

    red = 1

    orange = 2

    yellow = 3

    green = 4

    blue = 5

    indigo = 6

    purple = 7

    red_alias = 1

 

for color in Color:

    print(color)

 

输出结果是:Color.red、Color.orange、Color.yellow、Color.green、Color.blue、Color.indigo、Color.purple。但是Color.red_alias并没有出现在输出结果中。

 

 3.3 如果想把值重复的成员也遍历出来,要用枚举的一个特殊属性__members__

 

from enum import Enum

class Color(Enum):

    red = 1

    orange = 2

    yellow = 3

    green = 4

    blue = 5

    indigo = 6

    purple = 7

    red_alias = 1

 

 

for color inColor.__members__.items():

    print(color)

  输出结果:('red',<Color.red: 1>)、('orange',<Color.orange: 2>)、('yellow',<Color.yellow: 3>)、('green',<Color.green: 4>)、('blue',<Color.blue: 5>)、

       ('indigo',<Color.indigo: 6>)、('purple',<Color.purple: 7>)、('red_alias',<Color.red: 1>)

 

4. 枚举比较

 4.1 枚举成员可进行同一性比较

Color.red is Color.red

  输出结果是:True

Color.red is not Color.blue

  输出结果是:True

 4.2 枚举成员可进等值比较

Color.blue == Color.red

  输出结果是:False

Color.blue != Color.red

  输出结果是:True

 4.3 枚举成员不能进行大小比较

Color.red < Color.blue

  输出结果出错:TypeError:unorderable types: Color() < Color()

+

 

六、           使用元类metaclass

type()函数:1、可以查看一个类型或变量的类型;2、又可以创建出新的类型(如类class):

>>> def fn(self, name='world'):     # 先定义函数

     print('Hello, %s.' % name)

>>> Hello = type('Hello', (object,), dict(hello=fn))# 创建Hello class(名称,继承,函数绑定)

>>> h = Hello()

>>> h.hello()

Hello, world.

>>> print(type(Hello))

<class 'type'>

>>> print(type(h))

<class '__main__.Hello'>

 

 

Hello = type('Hello', (object,),dict(hello=fn))

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

  1. class的名称
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

 

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

 

 

metaclass

控制类的创建行为,可以使用metaclass元类

 

过程:先定义metaclass,就可以创建类,最后创建实例。

# metaclass是类的模板,所以必须从`type`类型派生:

class ListMetaclass(type):

    def __new__(cls,name, bases, attrs):

        attrs['add'] = lambdaself, value: self.append(value)

        returntype.__new__(cls, name,bases, attrs)

 

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:

class MyList(list, metaclass=ListMetaclass):

    pass

当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

 

returntype.__new__(cls,  name,  bases,  attrs)

__new__()方法接收到的参数依次是:

  1. 当前准备创建的类的对象;
  2. 类的名字;
  3. 类继承的父类集合;
  4. 类的方法集合。

 

 

测试一下MyList是否可以调用add()方法:

>>> L = MyList()

>>> L.add(1)

>> L

[1]

而普通的list没有add()方法:

>>> L2 = list()

>>> L2.add(1)

Traceback (most recent call last):

 File "<stdin>", line 1, in <module>

AttributeError: 'list' object has noattribute 'add'

 

0 0