0
点赞
收藏
分享

微信扫一扫

python:面向对象的设计原则之开闭原则

路西法阁下 2022-03-11 阅读 79

python作为一门面向对象的语言,它所具备的封装、继承、多态、抽象和组合这五大特点赋予了它极大的使用灵活性。于实践中,前人又总结出非常有用的编码风格、原则和模式,这里简单介绍一下“开放封闭原则”。

文章目录

一,python中的多态

多态是编程中一个非常重要的概念,是指单一类型的实体(方法、运算符或对象)在不同场景下使用公共接口而具有不同形式的能力。举个例子🌰:张三在不同的环境下可以用不同的身份来表示自己。

常见的内置的len()函数、+ 等运算符,都是对多态特性的应用。

多态通常会和继承放在一起来说,举个例子🌰:

class AudioFil:
    def __init__(self, filename):
        if not filename.endswith(self.ext):
            raise Exception("invalid file format!")

        self.filename = filename

    def play(self):
        pass


class Mp3File(AudioFil):
    ext = ".mp3"

    def play(self):
        print(f"Mp3File: playing {self.filename} as mp3.")


class OggFile(AudioFil):
    ext = ".ogg"

    def play(self):
        print(f"OggFile: playing {self.filename} as ogg.")

if __name__ == '__main__':
    filename_one = '金刚葫芦娃.mp3'
    file_one = Mp3File(filename_one)
    file_one.play()

    filename_two = '舒克与贝塔.ogg'
    file_two = OggFile(filename_two)
    file_two.play()

    filename_three = '金刚葫芦娃.mp3'
    file_three = OggFile(filename_three)
    file_three.play()
>>>
Mp3File: playing 金刚葫芦娃.mp3 as mp3.
OggFile: playing 舒克与贝塔.ogg as ogg.
Traceback (most recent call last):
  File "F:/python基础/OOP.py", line 88, in <module>
    file_three = OggFile(filename_three)
  File "F:/python基础/OOP.py", line 56, in __init__
    raise Exception("invalid file format!")
Exception: invalid file format!

这段代码做了三件事:

  1. 父类 AudioFile 用构造方法初始化类属性,并封装了一个播放接口。
  2. 子类继承了父类,添加了 ext 属性,并重写了播放接口。
  3. 实例化子类并调用实例的播放方法,打印出包含各自文件格式的信息。

刚开始的时候,父类构造方法中的 self.ext 并没有任何引用,但它确实又能访问到子类中的共同的同名属性。而且子类化时,子类重写了父类中的方法,并在调用中有不同的表现。都依靠多态来实现。

多态切实地增强了程序的的可扩展性,但多态起作用的前提之一,就是存在继承关系,只有当实体被判断为某类型时才能确保它有正确的接口。否则,调用错误的依赖的错误接口将直接报错:

print(3 + 2)
print("foo" + "span")
print("foo" + 3)
>>>
5
foospan
Traceback (most recent call last):
  File "F:/python基础/OOP.py", line 81, in <module>
    print("foo" + 3)
TypeError: can only concatenate str (not "int") to str
  • 这也恰好就证明python是一门强类型语言——强弱并不是指是否需要直接指定类型,而是强调不同类型之间是否允许进行隐式的类型转换。

总之,多态强调类型检查的。

二,EAFP 和 LBYL

EAFP 和 LBYL 是两种编程风格:

  • EAFP(Easier to ask for forgiveness than Permission)——请求原谅比请求允许容易:更关注能不能。
  • LBYL(Look Before You Leap)——三思而后行:更关注是不是。

两种风格的简单直接的区别就是对潜在错误的容忍度:

  • EAFP:事中由try...except语句处理能不能,表明异常应该是不常见的。
  • LBYL:事前由if...else语句判断是不是,表明必须实现确定可能的异常情况。

EAFP style:

user = {"name": "Jack", "age": None}

print("What key do you want to access?")

key = input(" >> ")
try:
    print(user[key])
except KeyError:
    print(f"Cannot find key '{key}'")

LBYL style:

user = {"name": "Jack", "age": None}

print("What key do you want to access?")

key = input(">> ")
if key in user:			
    print(user[key])
else:
    print(f"Cannot find key '{key}'")

实际上,你选哪一种都行,如果细究的话,将引出更大、更复杂、更有争议性的话题来——什么是干净整洁的代码?

但是,最好还是根据你所使用的语言的约定来决定。

  • python doc 的Glossary认为 EAFP 是更加 pythonic 的风格,这显然与python是一门动态语言有关。

三,鸭子类型

鸭子类型是从多态发展而来的一种编程思想,后者在内部判断子类实体类型的基础上根据 MRO 来获取属性与方法,而前者更加强调接口而非特定类型:

class Animal:
    def eat(self):
        print("I can eat something.")


class Duck(Animal):
    def quack_and_swim(self):
        print("I am a duck and I quack.")


class Goose:
    def quack_and_swim(self):
        print("I am a goose and I quack.")


class Frog(Animal):
    def croak_and_swim(self):
        print("I am a Frog and I croak.")


# class DuckType:
#     def quack_and_swim(self, animal):
#         animal.quack_and_swim()


def duck_type(animal):
    animal.quack_and_swim()


if __name__ == '__main__':
    duck = Duck()
    goose = Goose()
    frog = Frog()

    duck_type(duck)
    duck_type(goose)
    duck_type(frog)
    
>>>
I am a duck and I quack.
I am a goose and I quack.
Traceback (most recent call last):
  File "F:/python基础/OOP.py", line 112, in <module>
    duck_type(frog)
  File "F:/python基础/OOP.py", line 102, in duck_type
    animal.quack_and_swim()
AttributeError: 'Frog' object has no attribute 'quack_and_swim'

再配合 EAFP 风格进行异常捕获:

if __name__ == '__main__':
    duck = Duck()
    goose = Goose()
    frog = Frog()
    try:
        duck_type(duck)
        duck_type(goose)
        duck_type(frog)
    except AttributeError as e:
        print(e.args[0])

    # duck_type = DuckType()
    # 
    # try:
    #     duck_type.quack_and_swim(duck)
    #     duck_type.quack_and_swim(goose)
    #     duck_type.quack_and_swim(frog)
    # except AttributeError as error:
    #     print(error.args[0])

>>>
I am a duck and I quack.
I am a goose and I quack.
'Frog' object has no attribute 'quack_and_swim'

尽管 Goose 并没有与 Duck 继承相同的父类,但二者都有相同的接口 quack_and_swim,而一旦它们有相同的接口,就可以基于此情况将它们视为都继承了拥有该接口的父类。

总之,鸭子类型是对是对多态的扩展,在可以使用多态特性的情况下,却不要求有所依赖。

  • 鸭子类型更加 pythonic。

再重复一遍:

四,抽象基类

如果我们想在 python 中实现类似于 Java 中“接口”的概念——列出行为清单,具体行为内容具体实现——作为对以鸭子类型使用接口的方式的补充, python 提供了“抽象基类”这个概念来以另一种方式规范化公共接口的定义与使用。

python并不直接提供抽象基类,需要使用abc — Abstract Base Classes中的工具进行定义与使用。

定义抽象基类,最简单的方法就是定义一个继承abc.ABC类的类,同时定义公共接口并以@abc.abstractmethod装饰器将它们装饰为抽象方法。举个例子🌰:

from abc import ABC, abstractmethod


class AudioFil(ABC):
    def __init__(self, filename):
        self.filename = filename

    def check_type(self):
        if not self.filename.endswith(self.ext):
            raise TypeError("invalid file format!")

    @abstractmethod
    def play(self):
        pass

    @abstractmethod
    def stop(self):
        pass
  • 自定义的 AudioFil 因继承 ABC 类而成为一个抽象类。
  • 抽象类中必须有抽象方法,这些方法必须由 @abstractmethod 进行装饰,但不具体实现。
  • 抽象类中也允许有普通方法或属性。

正是因为抽象类是“抽象的”,在使用时有如下的要求:

  • 无法实例化,继承 ABC 类的抽象类只能子类化。
  • 子类必须实现所有抽象方法,否则会报错。
    在这里插入图片描述

上面的五条注解基本就表明了抽象类实现公共接口定义与使用的理念:在抽象父类中定义公共的抽象方法,再由子类继承并具体实现。举个例子🌰:

class Mp3File(AudioFil):
    ext = ".mp3"

    def play(self):
        print(f"Mp3File: playing {self.filename} as mp3...")

    def stop(self):
        print(f"Mp3File: stop playing {self.filename}...")


class OggFile(AudioFil):
    ext = ".ogg"

    def play(self):
        print(f"OggFile: playing {self.filename} as ogg...")

    def stop(self):
        print(f"Mp3File: stop playing {self.filename}...")


if __name__ == '__main__':
    audio_file_name = '金刚葫芦娃.mp3'
    mp3_file = Mp3File(audio_file_name)
    mp3_file.play()
    mp3_file.stop()

>>>
Mp3File: playing 金刚葫芦娃.mp3 as mp3...
Mp3File: stop playing 金刚葫芦娃.mp3...

抽象基类通过事先约定公共接口的方式实现接口管理。

五,开闭原则

因为无法保证需求不发生变化,所以软件实体必须为将来可能的新需求提供足够的扩展性,而不是为新需求去修改老代码。

举个例子🌰:一个扩展性不足的电影搜索功能:

class Movie:
    # Setting up attributes
    def __init__(self, name, genre):
        self.name = name
        self.genre = genre


class MovieBrowser:
    # Movie filter by Name
    def search_movie_by_name(self, movies, name):
        return [movie for movie in movies if movie.name == name]

    # Movie filter by Genre
    def search_movie_by_genre(self, movies, genre):
        return [movie for movie in movies if movie.genre == genre]

假设出现了需求变更:

  • 如果我们想同时按名称和流派进行搜索(多条件过滤器)会发生什么?
  • 如果我们新增发布年份,并以此作为过滤过滤条件时又会发生什么?

答案是,我们每次都必须在现有的 MovieBrowser 类中编写一个新功能函数。面对各种各样的需求,在旧代码内实现新的处理逻辑的同时,也会增加对代码整体重新进行测试时的各方面的成本。

可以用抽象基类与鸭子类型来遵循开闭原则以提高扩展性与可重用性:

from abc import ABC, abstractmethod


class Movie:
    # Setting up attributes
    def __init__(self, name, genre):
        self.name = name
        self.genre = genre


# Abstract base class
class SearchBy(ABC):
    @abstractmethod
    def is_matched(self, movie):
        pass

    # Overloaded and operator
    def __and__(self, other):
        return AndSearchBy(self, other)


# Inheriting SearchBy and Filter on Genre
class SearchByGenre(SearchBy):
    def __init__(self, genre):
        self.genre = genre

    def is_matched(self, movie):
        return movie.genre == self.genre


# Inheriting SearchBy and Filter on Name
class SearchByName(SearchBy):
    def __init__(self, name):
        self.name = name

    def is_matched(self, movie):
        return movie.name == self.name


# Added AndSearchBy method without altering existing classes
class AndSearchBy(SearchBy):
    def __init__(self, name, release_date):
        self.name = name
        self.release_date = release_date

    def is_matched(self, movie):
        return self.name.is_matched(movie) and self.release_date.is_matched(movie)


class MovieBrowser:
    def search(self, movies, searchby):
        return [movie for movie in movies if searchby.is_matched(movie)]

总之,建议使用抽象基类和鸭子类型来管理公共接口以实现更好的扩展性。

举报

相关推荐

0 条评论