本文摘自朱雷老师所著《Python工匠》一书第9章内容,因为很多内容,阅读后依然一知半解,特做笔记予以记录而进一步加强认知。
Python是一门面向对象的编程语言,它为面向对象编程提供了非常全面的支持。但和其他编程语言相比,Python中的面向对象有很多细微区别。比如Python并没有严格的私有成员,大多数时候,只是给变量加上下划线_前缀。
和许多静态类型语言不同,Python遵循“鸭子类型”编程风格,极少对变量进行严格的类型检查。“鸭子类型”是一种非常实用的编程风格,但也有缺乏标准、过于隐式的缺点。为了弥补这些确定,可以用抽象类来实现更灵活的子类化检查。
在创建类时,除了可以同时继承多个基类,Mixin模式正是依赖这种技术实现的。但多重继承非常复杂、容易搞砸,使用时无比当心。
继承是面向对象的基本特征之一,但它也很容易被误用。应学会判断何时该使用继承,何时该用组合代替继承。
一、下面是学习第九章知识要点
(1)语言基础知识
(2)面向对象高级特性
(3)鸭子类型与抽象类
(4)面向对象设计
(5)函数与面向对象的配合
(6)代码编写细节
Python的成员私有协议并不严格,如果你想标识某个属性为私有,使用你想标识某个属性私有,使用下划线前缀就可以了。
编写类时,类方法排序应该遵循某种特殊规则,把读者最关心的内容摆在前面
多态是面向对象编程的基本概念,同时也是最强大的思维工具之一
多态可能的介入时机:许多类似的条件分支判断、许多针对类型isinstance()判断
二、知识与技巧
(1)单例模式(singleton pattern)与预绑定方法模式(prebound methon pattern)
假设你在开发一个程序,它的所有配置项都保持在一个特定的文件中。在项目启动时,程序需要从配置文件中读取所有配置项,然后将其加载进内存供其他模块使用。
由于程序执行时只需要一个全局的配置对象,因此你觉得这个场景非常适合使用经典设计模式:单例模式(singleton pattern)。
下面的代码就应用了单例模式的配置类AppConfig:
class AppConfig:
"""程序配置类,使用单例模式"""
_instance = None
def __new__(cls):
if cls._instance is None:
inst = super().__new__(cls)
# 省略,从配置外部配置文件中读取配置
#...
pass
cls._instance = inst
return cls._instance
def get_database(self):
"""读取数据库配置"""
pass
def reload(self):
"""重新读取配置文件,刷新配置"""
pass
c1 = AppConfig()
c2 = AppConfig()
print(c1 is c2) # 输出结果:True
在Python中,实现单例模式的方式有很多,而上面这种最为常见,它通过重写类的__new__方法来接管实例创建行为。当__new__方法被重新后,类的每次实例化返回的不再是新实例,而是同一个已经初始化的旧实例cls._instance:
>>>c1 = AppConfig()
>>>c2 = AppConfig()
>>> c1 is C2
True
从上代码看,调用AppConfig()总是会返回同一个对象,基于上面设计,如果其他人想读取数据库配置,代码这样写:
from project.config import Appconfig
db_conf = AppConfig().get_database()
# 重新加载配置
AppConfig.reload()
虽然在处理这种全局配置对象时,单例模式是一种行之有效的解决方案,但在Python中,有一种更简单的做法——与绑定方法模式。
与绑定方法模式是一种将对象方法绑定为函数的模式。要实现该模式,第一步就是完全删除AppConfig里的单例设计模式。因为在Python里,实现单例压根儿不用这么麻烦,有一个随手可得的单例对象——模块(module)
当我们在Python中执行import语句导入模块时,无论import执行了多少次,每个被导入的模块在内存中只会存在一份(保存在sys.modules中)。因此要实现单例模式,只需要在模块里创建一个全局对象即可。
class AppConfig:
"""程序配置类,使用单例模式"""
def __init__(self):
# 已省略:从外部配置文件读取配置
...
def get_database(self):
"""读取数据库配置"""
...
def reload(self):
"""重新读取配置文件,刷新配置"""
...
上面代码完善删掉了单例模式的相关代码,只实现了__init__方法,_config就是我们的“单例AppConfig对象”,它以下划线开头命名,表面自己是一个私有全局变量,以免其他人直接操作。
下一步,为了给其它模块提供好用的API,我们需要将单例对象_config的共有方法绑定到config模块上:
# file: project/config.py
class AppConfig:
"""程序配置类,使用单例模式"""
def __init__(self):
# 已省略:从外部配置文件读取配置
...
def get_database(self):
"""读取数据库配置"""
...
def reload(self):
"""重新读取配置文件,刷新配置"""
...
# 私有全局变量
_config = AppConfig()
get_database_conf = _config.getdatabase
reload_conf = _config.reload
之后,其它模块就可以像调用普通函数一样操作应用配置对象了:
from project.config import get_database_conf
# 载入数据库配置
db_conf = get_database_conf()
# 重新载入配置
reload_conf()
通过使用“预绑定方法模式”,既避免了复杂的单例设计模式,又有了更易使用的函数API。
(2)多态:在分支中寻找多态的应用时机
多态(polymorphism)是面向对象编程的基本概念之一。它表示同一个方法调用,在运行时会因为对象类型的不同,产生不同效果。比如一个animal类有一个方法bark(),在animal类实例化是Cat类型时,bark()方法发出“喵喵”叫,在animal类实例化是Dog类型时则发出“汪汪”叫。
多态很好理解,当我们看到设计合理的多态代码时,很轻松就能明白代码的意图。但面向对象编程的新手有时会处在一种状态:理解多态,但不知道何时该创建多态。
下面的类FancyLogger是一个记录日志的类:
# -*- coding: utf-8 -*-
from enum import Enum, auto
class OutputType(int, Enum):
"""输出类型:枚举类型
使用enum模块中的auto()方法,自动为枚举常量分配连续的整数。
"""
FILE = auto()
REDIS = auto()
ES = auto()
class FancyLogger:
"""日志类:支持向文件、Redis、ES等服务输出日志"""
_redis_max_length = 1024
def __init__(self, ouput_type=OutputType.FILE):
self.ouput_type = ouput_type
...
def log(self,message):
"""打印日志"""
if self.ouput_type == OutputType.FILE:
...
elif self.ouput_type == OutputType.REDIS:
...
elif self.ouput_type == OutputType.ES:
...
else:
raise TypeError('非法的输出类型')
def pre_process(self,message):
"""预处理日志"""
# Redis对日志最大长度有限制,需要进行裁剪
if self.ouput_type == OutputType.REDIS:
return message[:self._redis_max_length] # 切片,裁剪message信息
FancyLogger类接收一个实例化参数:output_type,代表当前的日志输出类型。当输出类型不同时,log()和pre_process()方法会做不同的事情。
上面的FancyLogger类代码就是一个典型的应该使用多态的例子。
FancyLogger类在日志输出类型不同时,需要有不同的行为。因此,我们完全可以为“输出日志”行为建模一个新的类型:LogWriter,然后把每个类型的不同逻辑封装到各自的Writer类中。
对上面的3种输出类型,创建下面的3个Writer类,并对FancyLogger进行了简化,代码如下:
class FileWriter:
def write(self, message):
...
class RedisWriter:
max_length = 1024
def _pre_process(self, message):
# REDIS 对日志最大长度有限制,需要进行裁剪
return message[: self.max_length]
def write(self, message):
message = self._pre_process(message)
...
class EsWriter:
def write(self, message):
...
class FancyLogger:
"""日志类:支持往文件、Redis、ES 等服务输出日志"""
def __init__(self, output_writer=None):
self._writer = output_writer or FileWriter() # 默认输出类型:FileWriter
...
def log(self, message):
self._writer.write(message)
上面代码,FancyLogger类使用多态特性,完全消除了原来的条件判断语句。最大的意义在于,利用多态的新代码扩展性更好。
假如想增加一种新的输出类型,在原来实现的FancyLogger类的代码中,需要修改其中Log()和pre_process()等方法,在代码中增加新的类型的判断逻辑。而在新的代码中,只需要增加一个新的Writer类即可,调用FancyLogger类,根据传入的output_type不同,多态会调用相关的Writer做匹配的动作。
注意:增加新的输出类型及相关功能,主类FancyLogger代码不用做任何修改,只需要增加新的Writer类,实现新Writer的实现逻辑即可。
另外,上面这些Writer类都没有继承任何基类,这是因为在Python中多态并不需要使用继承。如果你感觉不好,也可以选择创建一个LogWriter抽象基类。
深入思考多态时,会发现它是一种思维的杠杆,是一种“以少胜多”的过程。
比起把所有的分支和可能行,一股脑地塞进程序员的脑子里,多态思想驱使我们更积极地寻找有效的抽象,以此隔离各个模块,让它们之间通过规范的接口来通信。模块因此变得更容易扩展,代码也更容易理解。
找到使用多态的时机,当你发现自己的代码出现以下特征时:
问自己一个问题:代码是不是缺少了某种抽象?如果增加这个抽象,这些分布在各处的条件分支,是不是可以用多态来表现?如果答案是肯定的,就去找到那个抽象吧。