Python学习笔记--面向对象编程基础知识

本文摘自朱雷老师所著《Python工匠》一书第9章内容,因为很多内容,阅读后依然一知半解,特做笔记予以记录而进一步加强认知。

Python是一门面向对象的编程语言,它为面向对象编程提供了非常全面的支持。但和其他编程语言相比,Python中的面向对象有很多细微区别。比如Python并没有严格的私有成员,大多数时候,只是给变量加上下划线_前缀。

和许多静态类型语言不同,Python遵循“鸭子类型”编程风格,极少对变量进行严格的类型检查。“鸭子类型”是一种非常实用的编程风格,但也有缺乏标准、过于隐式的缺点。为了弥补这些确定,可以用抽象类来实现更灵活的子类化检查。

在创建类时,除了可以同时继承多个基类,Mixin模式正是依赖这种技术实现的。但多重继承非常复杂、容易搞砸,使用时无比当心。

继承是面向对象的基本特征之一,但它也很容易被误用。应学会判断何时该使用继承,何时该用组合代替继承。

一、下面是学习第九章知识要点

(1)语言基础知识

  • 类与实例的数据,都保存在一个名为__dict__的字典属性中
  • 灵活利用__dict__属性,能帮助做到常规做法难以完成的一些事情
  • 使用@classmethod可以定义类方法,类方法常用做工厂方法
  • 使用@staticmethod可以定义静态方法,静态方法不依赖实例状态,是一种无状态方法
  • 使用@property可以定义动态属性对象,该属性对象的获取、设置和删除行为都支持自定义

(2)面向对象高级特性

  • Python使用MRO算法来确定多重继承时的方法优先级
  • super()函数获取的并不是当前类的父类,而是当前MRO链条里的下一个类
  • Mixin是一种基于多重继承的有效变成方式,用好Mixin需要精心的设计
  • 元类的功能相当强大,但同时也相当复杂,除非开发一些框架类工具,否则极少需要使用元类
  • 元类有许多更简单的替代品,比如类装饰器、子类化钩子方法等
  • 通过定义__init__subclass__钩子方法,你可以在某个类被继承时执行自定义逻辑

(3)鸭子类型与抽象类

  • “鸭子类型”是Python语言最鲜明特点之一,在该风格下,一般不做任何严格的类型检查
  • 虽然“鸭子类型”非常实用,但是它有两个明显的缺点——缺乏标准和过于隐式
  • 抽象类提供了一种更灵活的子类化机制,我们可以通过定义抽象类来改变isinstance()的行为
  • 通过@abstractmethod装饰器,可以要求抽象类的子类必须实现某个方法

(4)面向对象设计

  • 继承提供了相当强大的代码复用机制,但同时也带来了非常紧密的耦合关系
  • 错误使用继承容易导致代码失控
  • 对事务的行为而不是事务本身建模,更容易孵化出好的面向对象设计
  • 在创建继承关系时应当谨慎。用组合来替代继承有时候是更好的方法

(5)函数与面向对象的配合

  • Python里的面向对象不必特别纯粹,假如用函数打一点儿配合,可以设计出更好的代码
  • 可以像requests模块一样,用函数为自己的面向对象模块实现一些更易用的API
  • 在Python中,极少使用真正的“单例模式”,大多数情况下,一个简单的模块级全局变量对象就够了
  • 使用“预绑定方法模式”,你可以快速为普通实例包装出普通函数的API

(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抽象基类。

深入思考多态时,会发现它是一种思维的杠杆,是一种“以少胜多”的过程。

比起把所有的分支和可能行,一股脑地塞进程序员的脑子里,多态思想驱使我们更积极地寻找有效的抽象,以此隔离各个模块,让它们之间通过规范的接口来通信。模块因此变得更容易扩展,代码也更容易理解。

找到使用多态的时机,当你发现自己的代码出现以下特征时:

  • 有许多if/else判断,并且这些判断语句的条件都非常类似;
  • 有许多针对类型的isinstance()判断逻辑。

问自己一个问题:代码是不是缺少了某种抽象?如果增加这个抽象,这些分布在各处的条件分支,是不是可以用多态来表现?如果答案是肯定的,就去找到那个抽象吧。

你可能感兴趣的:(学习,笔记,python)