Pyhton学习笔记八:面向对象高级编程

1. 使用__slots__

# 定义一个class:

class Student(object):
	pass

# 尝试给实例绑定一个属性:

>>> s = Student()
>>> s.name = 'Michael'  # 动态给实例绑定一个属性
>>> print(s.name)
Michael

# 尝试绑定一个方法:

>>> 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

# 给一个实例绑定的方法,对另外一个实例是不起作用的:

>>> s2 = Student()   # 创建新的实例
>>> s2.set_age(25)   # 尝试调用方法
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

# 使用给class绑定方法,可以给所有实例都绑定方法

>>> def set_score(self, score):
...     self.score = score
...     
>>> Student.set_score = set_score

>>> s.set_score(100)   # 给class绑定后,所有实例均可调用
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99
  • 通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行中的过程中动态给class加上功能,这在静态语言中很难实现;

1.1 使用__slots__

  • 怎样限制实例的属性?
# 比如只允许对Student实例添加name和age属性,通过在定义class的时候,定义一个特殊的__slots__变量,来达到限制的功能:

class Student(object):
	__slots__ = ('name', 'age')   # 用tuple定义允许绑定的属性名称

>>> s = Student()
>>> s.name = 'Michael'
>>> s.age = 25
>>> s.score = 99   # 由于score没有被放到__slots__中,所以不能绑定score属性,故报错
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
  • 使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
>>> class GraduateStudent():
...     pass
... 
>>> g = GraduateStudent()
>>> g.score = 9999
  • 除非在子类中也定义__slots__,这样,子类就允许定义的属性就是自身的__slots__加上父类的__slots__

2. 使用@property

  • 在绑定属性时,直接把属性暴露出去,虽然写起来简答,但是没办法检查函数,导致可以把成绩随便修改:
s = Student()
s.score = 9999

# 这显然不合逻辑,可通过以下方法来限制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 > 1000:
            raise ValueError('score must between 0 ~ 1000!')
        self._score = value

# 这样就可以随心所欲设置score了:

>>> s = Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/@property.py", line 9, in set_score
    raise ValueError('score must between 0 ~ 1000!')
ValueError: score must between 0 ~ 1000!
  • 但是这种方法有有点复杂,没有直接用属性这么简单,有没有既能检查参数,又能用类似属性这种简单的方式来访问类的变量呢?
# 前面装饰器(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('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
# 把一个getter方法变成属性,只需加上@property即可,此时@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就有了一个可控的属性操作:

>>> s = Student()
>>> s.score = 60   # 实际上转化为s.set_score(60)
>>> s.score   # 实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "E:\4_Programe\1_Python\3_Code\1_LiaoDaDa\property.py", line 28, in score
    raise ValueError('score must between 0 ~ 100!')
ValueError: score must between 0 ~ 100!
  • 这个@property,我们对实例属性操作的时候,就知道该属性不是直接暴露的,而是通过getter和settter方法来实现的
# 还可以定义只读属性:只定义getter方法,不定义setter方法

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth

# birth是可读写属性,而age只是可读属性,因为age可根据birth和当前时间计算出来;

2.1 练习

  • 请利用@property给一个Screen对象加上widthheight属性,以及一个只读属性resolution
class Screen(object):
    @property
    def width(self):
        return self._width
    
    @property
    def height(self):
        return self._height

    @width.setter
    def width(self, value):
        self._width = value

    @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('测试失败!')

resolution = 786432 
测试通过!

3. 多重继承

  • 回忆一下Animal类层次的设计,假设我们要实现下面4中动物:

    • Dog - 狗狗
    • Bat - 蝙蝠
    • Parrot - 鹦鹉
    • Ostrich - 鸵鸟
  • 按照哺乳动物和鸟类归类,可以设计出这样的类的层次:
    在这里插入图片描述

  • 按照‘能跑’和‘能飞’归类,可以设计出这样的类的层次:
    在这里插入图片描述

  • 如果把上面的分类都包括进来,就得设计很多的层次:

    • 哺乳类:能跑的哺乳类,能飞的哺乳类;
    • 鸟类:能跑的鸟类,能飞的鸟类。

在这里插入图片描述

  • 如果再增加宠物类和非宠物类,类的数量就会呈现指数级增长,这样设计明显是不行的;
# 正确的方法是采用多重继承,首先,主要的类层次依旧按照哺乳类和鸟类设计

class Animal(object):
    pass

# 大类

class Mammal(Animal):
    pass

class Bird(Animal):
    pass

# 各种动物

class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

class Parrot(Bird):
    pass

class Ostrich(Bird):
    pass

# 给动物加上Runnable和Flyable的功能:

class Runnable(object):   # 只需先定义好Runnable和Flyable的类:
    def run(self):
        print('Running...')

class Flyable(object):
    def fly(self):
        print('Flying...')

# 对于需要Runnable功能的动物,就多继承一个Runnable,如:

class Dog(Mammal, Runnable):
    pass

# 对于需要Flyable功能的动物,就多继承一个Flyable,如:

class Bat(Mammal, Flyable):
    pass

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

3. 1 Mixln

  • 通常设计类的继承关系时,主线都是单一继承下来的,如:Ostrich继承自Bird,但是多重继承,可以让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。
  • 可以把RunnableFlyable改为RunnableMixInFlyableMixIn。类似的,还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
	pass
  • MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
  • Python自带的很多库也使用了MixIn。如:Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。
# 编写一个多进程的TCP服务:

class MyTCPServer(TCPServer, ForkingMixIn):
	pass

# 编写一个多线程模式的UDP服务:

class MyUDPServer(UDPServer, ThreadingMixIn):
	pass

# 编写一个协程模型:

class MyTCPServer(TCPServer, CoroutineMixIn):
	pass
  • 这样不需要复杂而庞大的继承链,只需选择组合不同的类的功能,就可以快速的构造出所需的子类

4. 定制类

  • 看到类似__slots__这种形如__xxx__的变量名或者函数名就要注意,在Python中他们是有特殊用途的,python的class中还有很多这样有特殊用途的函数,来帮助我们定制类;

4.1 __str__

# 先定义一个Student类,打印一个实例:
>>> class Student(object):
...     def __init__(self, name):
...             self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x000001B73BE877F0>   # 打印出一大堆这个东西,并不好看

# 定义__init__()方法,返回一个好看的字符串
>>> class Student(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   # 这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别就是前者返回的是用户看待的字符串,后者返回的是开发者看到的字符串,也就是说__repr__()是为调试服务的
<__main__.Student object at 0x000001B73BE877F0>

# 解决办法就是再定义一个__repr__(),但是通常这两个代码是一样的,但是有个偷懒的方法:
class Student(object):
	def __init__(self, name):
		self.name = name
	def __str__(self):
		return 'Student object (name=%s)' % self.name
	__repr__ = __str__

4.2 __iter__

  • 如果一个类想要被用于for...in循环,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用带迭代对象的__next__()方法拿到循环的下一个值,知道遇到StopIteration错误时退出循环。
# 使用__iter__方法,以斐波那契数列,写一个Fib类,用于for循环:

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 StopAsyncIteration()
        return self.a   # 返回下一个值


#试试将Fib实例作用于for循环:
>>> for n in Fib():
...     print(n)
...
1
1
2
3
5
...
46368
75025

4.3 __getitem__

# Fib实例作用于for循环,看起来和list有点像,但是当做list来使用还真不行:
>>> Fib()[5]   # 取第5个元素
Traceback (most recent call last):
  File "E:\1_Install_Total\8_Anaconda\lib\site-packages\IPython\core\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-3-14290f619470>", line 1, in <module>
    Fib()[5]
TypeError: 'Fib' object does not support indexing

# 通过__getitem__方法,来实现像list按照下标取出元素:
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
>>> f[1]
1
>>> f[100]
573147844013817084101
  • list中有个神奇的切片,对于Fib却报错,原因是__getiten__传入的可能是一个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

>>> 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没什么区别,这完全归功于动态语言的鸭子类型,不需要强制继承某个接口

4.4 __getattr__

# 当我们调用不存在的类的方法或者属性时,就会报错,如定义Student类
class Student(object):
	def __init__(self):
		self.name = 'Michael'
>>> s = Student()   # 调用name属性,没问题
>>> print(s.name)
Michael
>>> print(s.score)   # 调用不存在的score属性,报错
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'score'
  • 要避免这个错误,除了加上一个score属性外,还可以另外写一个__getattr__()方法,动态返回一个属性:
class Student(object):
	def __init__(self):
		self.name = 'Michael'
	def __getattr__(self, attr):
		if attr = 'score':
		    return 99
# 当调用不存在的属性时,比如score,python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样就能获得返回score的值:
>>> s = Student()
>>> s.name
'Michael'
>>> s,score
99

# 返回函数也是完全可以的:
class Student(object):
	def __getattr__(self, attr):
		if attr == 'age':
			return lambda: 25
# 调用方式变为:
>>> a.age()
25
# 注意到任意调用s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None,要让class只响应特定的几个属性,就要抛出AttributeError的错误:
class Student(object):
	def __getattr__(self, attr):
		if attr == 'age':
			return lambda: 25
		raise AttributeError('\'Student\' has no attribute \'%s\'' % attr)
  • 这实际上可以把一个类的所有属性和方法调用全部动态化处理了,那这种完全动态调用的处理有什么实际作用呢?那就是可以针对完全动态的情况作调用;

4.5 __call__

  • 当我们在调用实例方法时,用instance.method()来调用,同时也可以直接在实例本身上调用:
# 任何类,只要定义一个__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对象,比如函数和我们上面定义的带有__call__()的类实例:
# 通过callable()函数,就可以判断一个对象是否是“可调用”对象
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

5. 使用枚举类

  • 定义常量时,方法之一是用大写变量通过整数定义,如下,好处是简单,但缺点是类型是int,并且仍然是变量:

JAN = 1
FEB = 2
MAR = 3

NOV = 11
DEC = 12

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

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

# 这样就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举他的所有成员:
for name, member in Month.__members__.items():
	print(name, '=>', member, ',', member.value)   # value属性则是自动赋给成员的int常量,默认从1开始计数

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

# 同时如果需要更精确的枚举类型,可以从Enum派生出自定义类:
from enum import Enum, unique

@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)
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 call last):
  ...
ValueError: 7 is not a 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的值获得枚举常量

5.1 练习

  • Studentgender属性改造为枚举类型,可以避免使用字符串:
# -*- coding: utf-8 -*-
from enum import Enum, unique

@unique
class Gender(Enum):
    Male = 0
    Female = 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('测试失败!')

6. 使用元类

6.1 type()

  • 动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
# 定义一个Hello的class,写一个hello.py模块:
class Hello(object):
	def hello(self, name='world'):
		print('Hello, %s.' % name)

# 当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.

>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>
  • type()函数可以查看一个函数或变量的类型,Hello是一个class,类型就是type,而h是一个实例,他的类型就是calss Hello
  • calss的定义是运行时通过type()函数创建的,type()函数既可以返回一个对象的类型,又可以创建新的类型,比如,通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:
>>> 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'>

  • 要创建一个class对象,type()函数依次传入3个参数:
    1. class的名称;
    2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
    3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello

6.1 metaclass

  • 当定义类以后,可以根据这个类创建出实例,即:先定义类,后创建实例;
  • 如果想创建出类,就是:先定义metaclass,然后创建类;即:先定义metaclass,创建类,然后创建实例;

猜你喜欢

转载自blog.csdn.net/zhao416129/article/details/84191157
今日推荐