23. 描述符和设计模式

Hi, 大家好,我是茶桁。

上一节课中,我们讲解了面向对象中的一些高阶应用,给大家介绍了一些魔术方法。并在最后,我们预告这节课内容会讲解描述符和设计模式。

好了,让我们开始吧。

描述符

这个玩意,怎么讲合适呢?这么说吧,当某一个类中,包含了三个魔术方法(__get__, __set__, __delete__)中的任意一个,或者全部都有时,那么这个类就被称为是「描述符类」。

描述符的作用就是对一个类中的某一个成员进行一个详细的惯例操作,包括获取、赋值以及删除。也就是,描述符代理了一个类中的成员的操作,描述符属于类,只能定义为类的属性。

魔术方法咱们前面有提到,这里让咱们先来看看三个特殊的魔术方法,:

  1. __get__(self, instance, owner)

触发机制:在访问对象成员属性时自动触发(当该成员已经交给描述符管理时) 作用:设置当前属性获取的值 参数:1. self 描述符对象 2.被管理成员的类的对象。3.被管理成员的类 返回值:返回值作为成员属性获取的值 注意事项:无

  1. __set__(self, instance, value)

触发机制:在设置对象成员属性时自动触发(当该成员已经交给描述符管理时) 作用:对成员的赋值进行管理 参数:1. self 描述符对象 2.被管理成员的类的对象。3.要设置的值 返回值:注意事项:无

  1. __delete__(self, instance)

触发机制:在删除对象成员属性时自动触发(当该成员已经交给描述符管理时) 作用:对成员属性的删除进行管理 参数:1. self 描述符对象 2.被管理成员的类的对象。 返回值:无 注意事项:无

让我们先来看一个基本的类和实例化:

1
2
3
4
5
6
7
8
9
class Person():
name = 'name'

# 实例化对象
zs = Person()
print(zs.name)

---
name

然后我们定义一个「描述符类」

1
2
3
4
5
6
7
8
9
10
11
12
# 定义描述符类
class PersonName():
__name = 'abc'

def __get__(self, instance, owner):
pass

def __set__(self, instance, value):
pass

def __delete__(self, instance):
pass

接着我们重新更改一下刚才定义的普通类, 将其中的name成员属性交给刚定义的描述符类来实现:

1
2
3
4
# 定义的普通类
class Person():
# 把类中的一个成员属性交给一个描述符类来实现
name = PersonName()

这个时候我们实例化之后打印其中的成员属性会如何?

1
2
3
4
5
6
# 实例化对象
zs = Person()
print(zs.name)

---
None

我们可以看到,结果为None

现在让我们依次将类中的__get__方法的参数都打印出来观察一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 修改其中的`__get__`方法
def __get__(self, instance, owner):
print(self)
print(instance)
print(owner)

# 实例化后打印
print(zs.name)

---
<__main__.PersonName object at 0x108931d20>
<__main__.Person object at 0x108930250>
<class '__main__.Person'>
None

现在,具体self, instance, owner各自分别是什么,就非常清楚了。

那,既然selfPersonName类本身,那我们在其中定义的name成员属性是不是就可以拿到了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定义描述符类
class PersonName():
__name = 'abc'

def __get__(self, instance, owner):
return self.__name

def __set__(self, instance, value):
pass

def __delete__(self, instance):
pass

# 定义的普通类
class Person():
# 把类中的一个成员属性交给一个描述符类来实现
name = PersonName()

# 实例化对象
zs = Person()
print(zs.name)

---
abc

没错,我们确实拿到了PersonName中的__name

我们现在可以这么理解,普通类中的一个成员属性交给了一个描述符类来实现,类中的成员的值是另一个描述符类的对象, 那么当对这个类中的成员进行操作时,可以理解为就是对另一个对象的操作。现在的PersonName这个描述符类,相对于一个代理人的角色。把当前的描述符类赋值给了一个需要代理的类中的成员属性。

既然我们看到了get方法的结果之后,那么剩下两个魔术方法的作用也就很容易想到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义描述符类
class PersonName():
...
def __set__(self, instance, value):
self.__name = value
...
# 定义的普通类
class Person():
...

# 实例化对象
...
zs.name = '张三丰'
print(zs.name)

---
abc
张三丰

这里容易理解吧?当我们执行zs.name = ‘张三丰’这个赋值操作的时候,其就是走到了__set__方法内。其中的self不言而喻,就是PersonName, 而value就是刚才我们进行赋值操作的那个值。这个时候,我们可以设置self.__name = value,那就是满足了这个赋值操作。当然,我们也可以不这样给,来让我们调戏一下这个赋值。

1
2
3
4
5
6
7
8
9
10
# 只改动`__set__`
def __set__(self, instance, value):
# self.__name = value
self.__name = '茶桁'

zs.name = '张三丰'
print(zs.name)

---
茶桁

当我们这样去改的时候,那么无论我们怎样去赋值,最终的结果都是打印出茶桁

那么__del__怎么用呢?我们接着看:

1
2
3
4
5
6
7
8
# 前面的代码都不做改动

del zs.name
print(zs.name)

---
茶桁
茶桁

那么第一个茶桁是刚才我们赋值后的打印结果,第二个茶桁呢?就是我们在执行del zs.name之后的打印结果。按道理来说,我们执行了del命令之后。zs这个对象的name成员已经被删除了,现在应该是打印出类中的原始值,也就是abc, 那为什么这里打印出来的还是茶桁呢?

原因就在于我们的__del__方法内没有任何操作。我们来改一下__del__内部:

1
2
3
4
5
6
7
8
9
10
# 只改动`__del__`
def __delete__(self, instance):
# print('我就是不行删除,气死你')
del self.__name

del zs.name
print(zs.name)

---
abc

这样我们就执行了del本该有的操作。不过大家也看到了,我中间有一段代码注释了,现在让我们替换一下注释:

1
2
3
4
5
6
7
8
9
# 只改动`__del__`
def __delete__(self, instance):
print('我就是不行删除,气死你')
# del self.__name

del zs.name

---
我就是不行删除,气死你

当我们执行del zs.name的时候,触发方法内的打印命令。那么,这个时候再让我们打印一下zs.name来看看:

1
2
3
4
print(zs.name)

---
茶桁

毫无意外的,茶桁还在,并没有变成abc

需要注意的是,同时具备三个魔术方法的类才是「数据描述符类」,没有同时具备三个魔术方法的类呢?很简单,就是「非数据描述符类」。两者的区别就是一个是完整的,一个是不完整的。可以不可以应用呢?部分可以,但是不完整,__get__, __set__, __delete__中总有某些功能无法实现。

一个描述符应用

了解了描述符的概念以及怎么使用之后,我们来试试实现一个应用:定义一个学生类,需要记录学员的 id, 名字和分数。

让我们先来起一个框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Student():
def __init__(self, id, name, score):
self.id = id
self.name = name
self.score = score
def __repr__(self):
return f'学员编号:{self.id}\n 学员姓名:{self.name}\n 学员分数:{self.score}'

# 实例化对象
zs = Student(37, '张三丰', 98)
print(zs)

---
学员编号:37
学员姓名:张三丰
学员分数:98

这里,我们对这个方法有一个要求,就是学员的分数只能在 0-100 范围中, 那其实很简单了对吧?

我们先来看看第一种最普通的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student():
def __init__(self, id, name, score):
self.id = id
self.name = name
# 检测分数范围
if score >= 0 and score <= 100:
self.score = score
else:
print('当前分数不符号要求。')
def __repr__(self):
return f'学员编号:{self.id}\n 学员姓名:{self.name}\n 学员分数:{self.score}'

# 实例化对象
zs = Student(37, '张三丰', 101)
print(zs)

---
当前分数不符号要求。
AttributeError: 'Student' object has no attribute 'score'

尝试一下,确实打印了“分数不符合要求”,同时报错。

先不说怎么解决报错的问题,这简单的解决方案只能适用于对象初始化的时候有效。如果我们是中间单独对成员属性进行赋值,那么就会失效了

1
2
3
4
5
6
7
8
...
zs.score = -1
print(zs)

---
学员编号:37
学员姓名:张三丰
学员分数:-1

那这个时候,大家还记不记得咱们之前学过的一个魔术方法setattr? 我们来给中间加一个__setattr__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def __setattr__(self, key, value):
# 检测是否给 score 进行赋值操作
if key == 'score':
print(key, value)
# 检测分数范围
if value >= 0 and value <= 100:
object.__setattr__(self, key, value)
else:
print('当前分数不符号要求。')
else:
object.__setattr__(self, key, value)

def __repr__(self):
info = f'学员编号:{self.id}\n 学员姓名:{self.name}\n 学员分数:{self.score}'
return info

...
zs.score = -1

---
score -1
当前分数不符合要求

我们这样就使用__setattr__方法,检测如果score分数进行赋值时候,进行了分数的检测判断。

那我们在看,现在的一个问题是,假如学员的分数不止一个,我需要赋值多个分数怎么办?当前学员有:语文,数学,英语分数。

另外就是当前这个类中的代码是否比较繁杂?

现在,我们再来看,思考一下使用描述符来代理我们的分数这个成员属性。让我们先来实现一下框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Score():
__score = None
def __get__(self, instance, owner):
pass

def __set__(self, instance, value):
pass

def __delete__(self, instance):
del self.__score

class Student():
score = Score()
def __init__(self, id, name, score):
self.id = id
self.name = name
self.score = score

def returnSelf(self):
info = f'学员编号:{self.id}\n 学员姓名:{self.name}\n 学员分数:{self.score}'
return info

框架就实现好了,我们将原始的普通类中的score代理给了描述符类Score()

那现在让我们来完善一下整个类中的方法。

首先,当我们进行获取的时候,直接return现有的值就可以了,当我们进行设置的时候,就需要进行判断,如果不符合要求就打印一个不符合要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 定义描述符类,代理分数的管理
class Score():
__score = None
def __get__(self, instance, owner):
return self.__score

def __set__(self, instance, value):
if value >= 0 and value <= 100:
self.__score = value
else:
print('分数不符合要求')

def __delete__(self, instance):
del self.__score

class Student():
score = Score()
def __init__(self, id, name, score):
self.id = id
self.name = name
self.score = score

def returnSelf(self):
info = f'学员编号:{self.id}\n 学员姓名:{self.name}\n 学员分数:{self.score}'
return info

让我们对其进行一下检测,看看是不是符合要求:

1
2
3
4
5
6
7
# 实例化对象
zs = Student(37, '张三丰', 132)
zs.returnSelf()

---
分数不符合要求
'学员编号:37\n 学员姓名:张三丰\n 学员分数:None'

没毛病,被告知了当前赋值不符合要求,并且最后分数上也为None,并未进行赋值。

在看看单独赋值:

1
2
3
4
5
6
7
8
zs.score = -1
zs.score = 88
zs.returnSelf()

---
分数不符合要求

'学员编号:37\n 学员姓名:张三丰\n 学员分数:88'

当赋值为-1的时候也是提示不符合要求,再次赋值88之后,正确赋值。然后我们打印出来的结果也正确。

那么我们的代理就正确的完成了它的工作。基本工作流程如下:

  1. 定义Score描述符类
  2. 把学生类中的score这个成员交给描述符类进行代理
  3. 只要在代理的描述符中对分数进行判断和赋值就可以了。

那么现在,我们就完成了一个描述符的应用案例。不知道大家是否都理解了?那么在下面,我给大家介绍一下描述符的三种定义格式:

  • 格式一: 通过定义描述符来实现(推荐)
1
2
3
4
5
6
7
8
9
10
class ScoreManage():
def __get__(self, instance, owner):
pass
def __set__(self, instance, value):
pass
def __delete__(self, instance):
pass

class Student():
score = ScoreManage()
  • 格式二: 使用property函数来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Student():
def __init__(self, id, name, score):
self.id = id
self.name = name
self._score = score

def getScore(self):
return self._score
def setScore(self, score):
self._score = score
def delScore(self):
del self._score

# 在 property 函数中指定对应的三个方法
# 对应的方法 1. `__get__`,2. `__set__`, 3. `__delete__`
# 当然,名称不是固定的,也可以定义成其他的方法名
# 不管定义成什么,`property`中的方法名必须一致。
# 注意在类中将成员属性重新定义,可以为受保护的或者私有属性,避免递归调用。
score = property(getscore,setscore,delscore)

  • 格式三:使用@property装饰器语法来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Student():
__score = None

@property
def score(self):
print('get')
return self.__score

@score.setter
def score(self,value):
print('set')
self.__score = value

@score.deleter
def score(self):
print('delete')
del self.__score

设计模式

我们谈设计模式的时候,实际上是一个比较抽象的东西。

设计模式,就是前人完成某个功能或者需求,根据经验和总结,对实现的代码步骤和代码设计进行了总结及归纳。成为了实现某个需求的经典模式。

设计模式可以说并不是什么固定的代码格式,而是一种面向对象编程的设计。

让我们先从单例开始。

单例(单态)设计模式

在当前脚本中,同一个类只能创建一个对象去使用,这种情况就称为单例(单态)。

我们以一个实际的思考案例来进行讲解,现在让我们来想:

单例和婚姻法的关系,特别像,就是一个人只能有一个结婚对象。在社会中是如何完成一夫一妻制的?如果想要结婚,必须要到民政局登记,民政局需要检测两个人的户口本,看看上面是否属于已婚的状态。如果是已婚,肯定就被撵出去了对吧。如果没有结婚,就可以盖章登记了。

那么按照这样的思路,我们又该如何去实现 Python 中的单例设计模式呢?来看哈:

  1. 需要一个方法,可以去控制当前对象的创建过程: 构建方法__new__

  2. 需要有一个标识来存储和表示是否有对象:创建一个私有属性进行存储,默认为None

  3. 在创建对象的方法中去检测和判断是否有对象: 如果没有对象,则创建对象,并且将对象存储起来,返回对象。那如果存储的是对象,则直接返回对象,就不需要创建新的对象了。

让我们依照这样一个思路来完成代码,让我们还是从框架开始:

1
2
3
4
5
class Demo():

# 定义构造方法
def __new__(cls, *args, **kwargs):
return cls.obj

第一步我们完成了,现在让我们来看第二部,我们需要定义一个私有属性用于存储对象。

1
2
3
4
5
6
7
8
class Demo():

# 定义私有属性存储对象
__obj = None

# 定义构造方法
def __new__(cls, *args, **kwargs):
return cls.__obj

接着,就要进入判断了:

1
2
3
4
5
6
7
8
9
10
11
12
class Demo():

# 定义私有属性存储对象
__obj = None

# 定义构造方法
def __new__(cls, *args, **kwargs):
# 创建对象的过程中,判断是否有对象
if not cls.__obj:
# 如果没有,则创建,并且存储起来
cls.__obj = object.__new__(cls)
return cls.__obj

类完成了,让我们来证实一下看看,是否只会创建一个对象。

1
2
3
4
5
6
7
8
9
# 实例化对象
a = Demo()
b = Demo()
print(a)
print(b)

---
<__main__.Demo object at 0x10434e950>
<__main__.Demo object at 0x10434e950>

看到打印结果中,两次实例化对象不同,但是地址相同。可以证明确实为一个对象。

Mixin 类

  • Mixin 必须是表示一种功能,而不是一个对象。
  • Mixin 的功能必须单一,如果有多个功能,那就多定义Mixin
  • python 中的Mixin是通过多继承实现的
  • Mixin 这个类通常不单独使用,而是混合到其它类中,去增加功能的
  • Mixin 类不依赖子类的实现,即便子类没有继承这个Mixin,子类也能正常运行,可能就是缺少了一些功能。。

那使用Mixin混入类有什么好处呢?

这个混入类的设计模式,在不对类的内容修改的前提下,扩展了类的功能。也提高代码的重用性,使的代码结构更加的简单清晰。可以根据开发需要任意调整功能(也就是创建新的Mixin混入类),避免设计多层次的复杂的继承关系。

我们之前学习继承,知道继承需要有一个必要的前提,就是继承应该是一个is-a的关系。

比如,苹果可以去继承水果,因为苹果is a水果, 那苹果是不能继承午饭的,因为午饭可以有苹果,也可以没有。

再比如,汽车可以继承交通工具,又是因为汽车本身is a交通工具。

遵循这样的一个规律,我们来思考,交通工具都有哪些呢?

汽车、飞机、直升飞机,这些都属于交通工具对吧?当然,高铁什么的也是,我们无法穷举出来,那样就太多了。

那么如何去设计这些类的关系呢?我们可以创建一个交通工具类,然后属于交通工具的都来继承,再去实现... 等等,我们再来思考一个问题:飞机、直升飞机都可以飞,可是汽车呢?汽车并不能飞行。那么交通工具中如果去定义飞行这个功能,是不是就不合适了?

你们现在是不是在想:那就在飞机和直升飞机类中分别实现飞行这个功能。可以是可以,但是重复代码是不是过多了?代码无法重用。

那该怎么办?其实,让我们分别去定义交通工具和飞行器这两个父类,这样飞机和直升飞机就可以去继承这两个类。对吧?

来,让我们开始实现,一样的,先来个框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义交通工具
class vehicle():
# 运输货物
def cargo():
print('货物')

# 搭载乘客
def person():
print('人')

# 定义飞行器
class flying():
def fly(self):
print('可以飞')

现在刚才的思考得以实现,我们定义了两个父类。接着是不是就要考虑继承了?

1
2
3
4
5
6
7
8
9
10
11
# 定义飞机
class airplane(vehicle, flying):
pass

# 定义直升机
class helicopter(vehicle, flying):
pass

# 定义汽车
class car(vehicle):
pass

根据我们之前学习的继承关系,这样就完成了子类对父类的继承关系。来让我们分析下:

此时去定义一个飞行器的类 Flying, 让需要飞行的交通工具直接继承这个类,可以解决问题。但是又两个问题,出现的类多继承,就违背了is-a, 飞行器这个类很容易被误解。那怎么办?

其实解决方案还是使用多继承,但是给飞行器这个类定义为一个Mixin混合类,此时就是等于把飞行器这个类,作为一个扩展的功能,来扩展其他类。

让我们来改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 定义交通工具
class vehicle():
# 运输货物
def cargo():
print('货物')

# 搭载乘客
def person():
print('人')

# 定义飞行器
class flyingMixin():
def fly(self):
print('可以飞')

# 定义飞机
class airplane(vehicle, flyingMixin):
pass

# 定义直升机
class helicopter(vehicle, flyingMixin):
pass

# 定义汽车
class car(vehicle):
pass

嗯,你没看错,就是这么简单,改一下名称。

那么在这段代码中,虽然直升机和飞机都是用了多继承,也就是继承了flyingMixin,但是由于flyingMixin类加了Mixin这个名,就告诉了后面阅读代码的人,这个类是一个Mixin类。

我知道你们在想什么,这个是不是太随便了?其实并不是,我们目前在谈论的是「设计模式」,这个flyingMixin类中除了名称之外,还要遵循一些特定的惯例规则,就是这个类中的功能必须是单一的。

在名称的含义上,Mixin表示混入(mix-in), Mixin必须是表示一种功能,而不是一个对象。Mixin的功能必须单一,如果有多个功能,那就需要多定义几个Mixin类。在 Python 中的Mixin是通过多继承实现的。Mixin类通常不单独使用,而是混合到其他类中,去增加功能的。Mixin类不依赖子类的实现,即便子类没有继承这个Mixin类,子类也能正常运行,只是可能缺少一些功能。

抽象类

首先我们要明白,抽象类也是一个类。但是,这又是一个特殊的类,抽象类不能用,不能直接实例化称为一个对象。抽象类包含了抽象方法,抽象方法就是没有实现代码的方法。抽象类需要子类继承,并重写父类的抽象方法,才可以使用。

抽象类,一般应用在程序设计,程序设计中一般是要对功能和需求进行规划,其中有一些需求是明确的并且可以完成的,但是也可能会有一些需求是不明确的,或者不确定具体需要怎么实现,此时就可以把这个不确定怎么实现或者需要后面再去实现的方法,定义为抽象方法(只定义方法名,不写具体代码)。

我们还是拿一个实例来讲解:

比如公司有一项新的产品需要开发,交给了开发部门的大拿,也就是你。那么你就开始去规划设计怎么去完成这个产品的开发。比如项目需要用到不同的技术,不同的人来完成。这样,你作为老大,自己完成了一部分功能,但是依然有一部分定义了需求,但是还没有具体实现,需要其他人来进行实现。

那么此时,你已经写完的部分就是普通方法,定义了需求但是未完成的就可以理解为是抽象方法。

还是来直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import abc

# 必须使用 metaclass, 属性必须是 abc.ABCMeta
class WriteCode(metaclass=abc.ABCMeta):

# 需要抽象方法,使用装饰器进行装饰
@abc.abstractmethod
def write_swift(self):
pass

def write_java(self):
print('实现了 Java 代码的开发')

def write_python(self):
print('实现了 Python 代码的开发')

这样我们就在一个抽象类中定义好了一个抽象方法,和几个普通方法。至于为什么必须metaclass=abc.ABCMeta,那就又要扩展着去讲了。这里先记住,就跟背单词一样,到这里了就这么用就可以了。

前面我们讲了,抽象类是不能直接实例化的,让我们试试看:

1
2
3
4
5
# 抽象类不能直接实例化对象
obj = WriteCode()

---
TypeError: Can't instantiate abstract class WriteCode with abstract method write_swift

报错了,直接告诉我们无法实例化抽象类。

那么我们到底要怎么用呢?

我们可以定义一个子类来继承,并实现抽象类中的抽象方法。

1
2
3
4
# 定义子类,继承抽象类,并实现抽象类中的抽象方法
class Demo(WriteCode):
def write_swift(self):
print('实现了 swift 代码的开发')

好了,现在让我们实例化子类试试:

1
2
3
4
5
obj = Demo()
print(obj)

---
<__main__.Demo object at 0x104ade8c0>

没有报错,似乎是完成了继承和实现。接着当然是一次执行子类中的方法来看看:

1
2
3
4
5
6
7
8
obj.write_java()
obj.write_python()
obj.write_swift()

---
实现了 Java 代码的开发
实现了 Python 代码的开发
实现了 swift 代码的开发

没毛病,现在我们完成了整个代码。

那小伙伴们现在估计最大的疑问是:抽象类我要应用在什么地方呢?

比如说,我们现在要开发一个框架,这个框架要有一大堆的功能,包括a,b,c(哎,我忽然理解导入的为什么是abc这样起名了)。但是呢,具体用这个框架开发什么样的产品我们并不清楚,因此这个框架中能否知道你要做什么样的开发吗?肯定不知道。

框架具备了一定的功能即可,剩下的,需要具体开发项目的人来实现自己的业务逻辑。

那这个时候,我们就要用到抽象类了。

好了,到目前为止,我们关于面向对象编程也就介绍的差不多了。而我们的 Python 课程基本上也到了尾声,在后面的课程中,我们会介绍一下 Python 中的装饰器。然后去学习一下几个用的特别广的库,包括matplotlib,numpy以及pandas

小伙伴们,大家对于此前的 Python 基础一定要好好的理解,好好练习。这样,越往后我们才能越轻松的开展后面的学习。

行,本节课到这里就结束了,下课。

作者

Hivan Du

发布于

2023-08-18

更新于

2024-01-16

许可协议

评论