`
huangz
  • 浏览: 322978 次
  • 性别: Icon_minigender_1
  • 来自: 广东-清远
社区版块
存档分类
最新评论

Python API 设计(1):关于 OORedis 中的类继承

阅读更多

嘿,让我们换种方式


当我刚开始关注API设计的时候,我决定先找一些相关的资料来看,比如博客日志、PPT还有书,这方面的资料很少,而且最后我发现他们很多都只是单调地列举一些有用的规则,并没有仔细地展开讨论,这些规则可能是有用的,但读起来让人感觉相当乏味,所以我决定自己来写一篇(可能是几篇)关于API设计的文章。


于是我列了一个提纲,把我认为重要的设计原则记录下来,然后对着每条要点准备虚构一个声色俱全的故事,然后我发现我自己的文章变成了之前我看过的八股文格式。。。


于是我决定换种方式,拿之前写的ooredis项目作为引子,来谈谈Python API设计方面的事情,有时候我也引用一些Python方面著名的项目比如Django来说事儿,但大多数时候,这篇文章看上去更像是“ooredis开发记事”。


文章里所说的都是在写ooredis时真实遇到的问题,我想这样比起总结两条基本原则再虚构一些例子强多了,当然我在这方面的经验也不多,主要是就这个话题抛砖引肉一下,希望大家注意到API设计的重要性。

 

 

缘起

 

大概在七月份的时候,我译完了Redis的命令参考前几章,那时候我刚开始学习Redis不久,当时用的是redis-py库,这个库是面向过程的,只是Redis命令的简单包装,比如一个HSET命令,在Redis里是:

 

 

hset key field value
 


而在redis-py里则是:

 

 

from redis import Redis

client = Redis()
client.hset(key, field, value)
 


这样的库有几个问题:


第一,大量的命令聚在在一起,污染了客户端的命名空间。


如果你用dir(Redis())查看redis-py的对象,你会发现数十个方法聚集在了这个客户端对象里面,用眼睛检索这种对象的方法实在是太累人了,很难在命令行中使用这个库。


第二,因为redis-py只有一个对象,所有命令都是通过给方法传不同的参数来执行的。


这样的问题就是你很可能在执行命令的时候犯错。


比如你想执行一系列hset命令,来保存个人信息,你执行

 

 

client.hset('person', 'name', 'peter')
client.hset('person', 'age', 25)
client.hset('perso', 'phone', 10086)

 


但是后来你却发现'person'哈希表里面没有'phone'这个域,你仔细看了看,发现原来前面的命令最后一行,你错误地将'person'写成了'perso',你将'phone'保存到了'perso'哈希表里,噢。


如果有一个对象实例作为句柄,绑定'person'作为对象的参数,你是绝对不会犯这样的错误的。


第三,面向过程式的库没有利用Python语言的机制。


redis-py单纯的方法调用方式没有利用到Python语言的机制,比如迭代器、字典方法,各类魔法函数,等等,这使得redis-py用起来很不Pythonic。


最后还有缺乏一种方便的类型转换机制(redis中只保存字符串值),以及跨类型之间覆盖而不报错等(试试对一个list结构执行set命令看看)。


为了解决redis-py的以上问题,我决定在redis-py之上写一个Redis的库,称为ooredis,它将是面向对象的、Pythonic的,而且,因为这个库是一个通用库,我希望ooredis能被更多人使用,所以它必须写得比较标准,看上去比较专业——最起码,没有什么特别大的问题,最好有天成为和redis-py一样被Pythoner广为使用的库(现在ooredis还远远没有达到这个目标,唉。。。不过这不太妨碍我们的讨论,大概。。。)。


(平心而论,这样评论redis-py并不是完全正确的,作为一个底层客户端,redis-py已经提供了相当充实的功能,为在其上构造更高层次做好了准备。当然redis-py也还是有一些小问题,后面我会说到。)

 


what's under the box?

 

计算机程序很少(或者说,不可能)是全新地被编写出来的,很多时候,我们只是在一个低层抽象之上写一个更高层的抽象层,用高层抽象包裹低层抽象,并为新层次提供一簇新API,好让这个新层次作为基石,继续构建更高层的抽象:就像硬件包裹电路,操作系统包裹硬件指令,编译器用C语言写出来,然后又作为其他语言(比如Python)的基石一样。


ooredis也一样,不同的是,它的目标不是构建一门新语言那样的高科技,而只是包裹一个Redis客户端而已,不过它们的道理是相同的——要在一个层次之上构建更高层次,你必须先了解(最起码是部分了解)现有的层次,这样才能写出好程序,于是我扎进redis-py和Redis命令参考里面,思考着该如何设计ooredis的类。


第一个跳出脑海的方式就是按照Redis的各类函数来分类(这里我们只考虑Redis的数据结构类命令,忽略事务、Pub/Sub等命令),用一个类包裹一簇命令,比如用BaseKey类包裹Redis的keys类函数,用String类包裹Redis的strings类函数,以此类推:

 

 

class BaseKey:
    pass

class String:
    pass

# 其他类...
 


但是这一完全直觉化的分类并不是完全正确的,比如keys类的expire、ttl、exists等命令,是Redis所有数据结构所共有的,而keys类的sort方法,则是除string结构和hash结构之外,list、set、sorted set才有的,于是我稍稍更改了一下类的设计:

 

 

class BaseKey:
    # 除了sort之外,所有Redis的Key类命令
    pass

class SortableKey(BaseKey):
    def sort():
        pass

# 没有sort方法的类
class String(BaseKey):
    pass

class Hash(BaseKey):
    pass

# 有sort方法的类
class List(SortableKey):
    pass

class Set(SortableKey):
    pass

class SortedSet(SortableKey):
    pass

 


OK,一切顺利,似乎没有什么难的,于是我开始为各个类写相应的方法。


不过很快,我发现,有一种更好的类定义方式,比现在的类定义方式更好,于是我开始修改程序,但这一次,事情就没有那么容易了。。。

 


是一个(is a)和有一个(has a)


就在ooredis第一版中,我将Redis的keys类命令分为了两个类,一个BaseKey类,另一个SortableKey,然后其他数据结构如String、Hash等类继承BaseKey或SortableKey,但是仔细思考一下,就会发现这种类设计并不太正确。


拿BaseKey和SortableKey来说,你会发现其实SortableKey相比BaseKey这个类来说,我们只是想为支持sort方法的数据结构如Hash类提供sort方法而已,这个继承并不合理。


再往后面推一步,BaseKey和SortableKey,对Hash和String这些数据结构类来说,它们其实不是一个“父类”,它们只是一簇方法,我们其实不想要BaseKey和SortableKey,而只是想要一种在数据结构类里重用keys类函数的方法。


用专业点的术语来说,Redis中的string数据结构和keys类命令在ooredis中应该是“有一个(has a)”而不是“是一个(is a)”关系——我需要有一种可以组合使用各个方法的机制。


这个问题其实是相当直观的,但是很遗憾Python似乎没有提供这样的机制,也即是,简单快捷地重用方法的唯一方式,就是抽取出这个方法,比如sort方法,然后给他弄一个SortableKey类,所有要用sort方法的类就继承SortableKey方法,就是这样。


认识到这一事实让我有点难过,不过也只是一点点,“有一个”和“是一个”关系的差别听上去这似乎只是某种理论问题,毕竟多继承一两个类其实关系不大,马照跑,舞照跳——咱们可是实用主义者。说实在的,如果以前有人想跟我讨论这类问题的话,我会跟他说别闹了,拿着你的《JAVA变成死相》离我远点。

 

 

Queue、Stack和Dequeue

 

于是我继续前进,很快就把String类的几个方法搞定了,然后我开始写List类——用来包裹Redis的list数据结构,然后我发现我的老朋友——“是一个和有一个”问题,又拦住了我的去路。


先来分析一下Redis的list数据结构,它是一个双端队列,也即是,push和pop可以在队列的两边进行,包裹这个数据结构的一蹴而就的方式自然就是用一个List类,将所有list结构的相关命令“装”进去,这种方法简单明了,也没有什么大错。


但是我不想这么干,因为我觉得list结构按操作还可以细分为好几个类,像栈(stack,LIFO)、队列(queue,FIFO)和双端队列(dequeue),这些数据结构只有轻微差别,但是实际应用中相当有用,如果我只写一个双端队列的话,想用栈或者队列的人就得自力更生了,我不是一个自私的人,而且为了ooredis将来的蓬勃发展(这一景愿至今仍未实现),多写几行代码也没啥的,于是我决定将原本的List一分为三:

 

 

class Dequeue:
    # 提供表头和表尾两边的push和pop
    pass

class Stack:
    # 只提供表尾一边的push和pop
    pass

class Queue:
    # 只提供表尾的push和表头的pop
    pass

 

 

很明显,这些三个类里面有一些共有的方法,比如获取列表长度的llen命令,以及读取列表项的lrange命令,但也有一些命令是某个类中独有的,比如Stack类就应该只有lpush和lpop(或者rpush和rpop),Queue应该只有lpush和rpop(或者rpush和lpop),而Dequeue则四个方法都可以有。


按照老方法,我们可以用一个GenericQueueProperty之类的类,将列表的通用方法装进去,然后Stack加上lpush和lpop,给Queue加上lpush和rpop,然后Dequeue继承Stack和Queue(只为重用方法)。


最终,我们类成了一团糟:

 

 

class BaseKey:
    pass

class SortableKey(BaseKey):
    pass

class GenericQueueProperty(SortableKey):
    # 提供队列的共有属性和方法
    pass

class Stack(GenericQueueProperty):
    # 只提供表尾一边的push和pop
    pass

class Queue(GenericQueueProperty):
    # 只提供表尾的push和表头的pop
    pass

class Dequeue(Stack, Queue):
    # push和pop可以在两边进行
    pass
 

 

 

解决方法

 

上面Dequeue类的定义让人感觉自己像是错过了京东买100送100活动一样难过,很自然地,你会问,是否有更好的办法在Python中解决重用方法的问题?


有人推荐使用多继承来解决方法重用的问题,这样的话,Dequeue的定义将是这样:

 

 

class SortableKey:
    # 提供sort方法,但不继承BaseKey
    pass

class LeftSideOperation:
    # 提供lpush和lpop
    pass

class RightSideOperation:
    # 提供rpush和rpop
    pass

class Dequeue(BaseKey, SortableKey, GenericQueueProperty, LeftSideOperation, RightSideOperation):
    pass
 

 


这种方法的特点其实就是用继承数量换继承高度,其实复杂性是没有变的,一棵高高的继承树和一串长长的继承列表之间,我真的说不清楚它们到底那个好一些。


而且这种方法有一个很隐晦的危险性,考虑如果你在继承列表中的类A中,定义了foo方法,但是在类B中,你又定义了一个foo方法,这样的话,它们就会互相覆盖,而在Python中这种覆盖是没有任何警告的,你继承的类越多,就可能越出现这种问题,一但这种问题出现,你就要检查所有继承类,如果你只有一个基类,那你就回溯祖先链,看看是那个环节出了问题;如果你有两个类,你的工作量就多了一倍;如果你有很多个基类。。。祝你好运!


Python标准库提供了另外一种思路,就是使用钩子方法:基类定义一些通用操作,比如push方法,push方法调用钩子_push方法,而派生类则通过覆盖_push方法,来提供不同的行为,比如这样:

 

 

class GenericQueue:
    def push():
        pass

    def _push():
        pass

    def pop():
        pass

    def _pop():
        pass

class Stack(GenericQueue):
    def _push():
        # lpush
        pass

    def _pop():
        # lpop
        pass

class Queue(GenericQueue):
    def _push():
        # lpush
        pass

    def _pop():
        # rpop
        pass
 

 

这种方法的问题是你要写很多额外的钩子方法,你必须小心处理,以免遗漏了哪一个或者不小心把_push写成了push,诸如此类。


说实在的,这种方法相当丑陋。

 

 

one more time, one more chance

 

以上两种方法都治标不治本,它们解决一些问题的同时也引入了一些更大的问题,究其原因,是因为Python里没有一种好的方法来重用已有方法,继承是重用方法的唯一简单快捷的手段。


必须承认我写ooredis时思维有点僵硬了,总是想着怎么用Python解决这个问题,而没有想到换一种语言来试试,比如在Ruby中,解决这个问题就相当简单:

 

 

module BaseKey
    # 所有除了sort方法之外的keys类方法
end

module SortableKey
    def sort
    end
end

module GenericQueue
    # 队列共有的属性和方法
end

module LeftSideOperation:
    # lpush & lpop
end

module RightSideOperation:
    # rpop & rpush
end

class Dequeue
    include BaseKey
    include SortableKey
    include GenericQueue
    include LeftSideOperation
    include RightSideOperation
end

 

 

 

这个Dequeue没有继承任何类,因为它本身已经是一个key,它和BaseKey、SortableKey等模块的关系是有一个而不是是一个,这才是正确的语义。(这个类混入的模块有点多,通过混入 Enumerable 模块,实际写起来其实会更简单。


我最终选择了多继承来实现ooredis,而且只用一个List类包裹所有的list数据结构命令,因为redis的命令基本上是正交的,没有相同的方法,所以多继承的风险比较低,如果你的程序多态方法相当多,我强烈建议你不要随便使用多继承,一棵高高的继承树和一大串继承列表之间,我宁愿选择前者。


用各种hack给编程语言“打补丁”是一条不归路,如果当时能想到这个方法的话,ooredis就会是Ruby Gem而不是Python库了。


当然现在的ooredis距离我的预想也不是太远,它只是不太美而已,嗯。。。不太美。。。

 

 

待续


我有一个目标,就是让那些不会用Redis的Python使用者不用学一条Redis命令,就能用我的ooredis。这可能吗?如果可以的话,怎么达到这一目标?


其次,为什么说好的API和好的程序一样,都是重用为主?创造和创新有什么区别?


下一篇文章,我们就来谈谈关于一致性的思考,看看如何达到我们的目标——写出不用学习就能轻松上手的API。

 

分享到:
评论
1 楼 java2ruby 2011-10-15  
很佩服作者对代码质量有这么高的追求!

相关推荐

    Python3.8.2中文API文档

    1. **PythonC API 参考手册**:这是针对那些希望在 C 语言环境中与 Python 解释器交互的开发者的指南。它包含了如何创建 Python 扩展模块、调用 Python 函数、操作 Python 对象等关键信息。C API 提供了底层接口,让...

    3dmax python api

    3dmax python api 绝对猛料3dmax python api 绝对猛料3dmax python api 绝对猛料3dmax python api 绝对猛料3dmax python api 绝对猛料3dmax python api 绝对猛料3dmax python api 绝对猛料

    Python3.8官方中文API参考手册

    1. **library.pdf**:库参考手册,这是Python标准库的详细指南,包含了大量的模块、函数、类和异常定义。例如,它介绍了os模块用于操作系统接口,sys模块用于交互与Python解释器,以及内置的类型如list、dict和str等...

    CarMaker的Python API文件

    4. **仿真控制**:API提供了开始、暂停、停止仿真等功能,如`icm.simulation_start()`和`icm.simulation_stop()`。 5. **数据读取**:在仿真过程中,可以实时获取和处理各种数据,例如车辆速度、加速度、轮胎力等,...

    Python_基础1~31

    基础知识点1: Python 的基本语法 * Python 的缩进规则:在 Python 中,使用缩进来定义代码块,而不是使用大括号或关键字。 * Python 的基本数据类型:整数、浮点数、字符串、布尔值、列表、元组、字典等。 * Python...

    TradeX.dll的Python API 使用演示程序

    在TradeX.dll中,下单功能可能包括买入、卖出、市价单、限价单等多种类型,Python API会提供对应的函数调用来实现。撤单功能则允许在订单未成交之前取消订单,确保交易策略的灵活性。 5. **查询行情**: 通过...

    Python-一个帮助你提供精心设计的PythonAPI的框架

    标题中的“Python-一个帮助你提供精心设计的PythonAPI的框架”表明我们正在讨论的是一个用于构建高效、优雅的Python应用程序接口(API)的框架。API(Application Programming Interface)是软件系统之间交互的一种...

    opencv-python api手册.rar

    opencv-python api手册.rar

    openpose使用pythonApi.zip

    1. **安装PyBind11**:在你的Python环境中,你可以使用`pip`命令来安装PyBind11: ``` pip install pybind11 ``` 2. **构建OpenPose Python模块**:在OpenPose源码目录下,找到并运行`make`命令,确保在编译时...

    探索Python中的继承:构建强大的面向对象应用

    通过本文的探讨,我们了解到了Python中继承的实现方式,包括继承声明、方法重写、多重继承以及如何处理私有属性和方法。合理利用继承,可以使Python代码更加模块化和易于维护。 通过深入理解Python中的继承机制,...

    SL4A之Python_API_中英文参考

    Python API是SL4A中用于与Android系统进行交互的一系列函数和类的集合。这些API提供了丰富的功能,比如发送短信、拨打电话、读取手机状态、控制屏幕亮度、播放音频、获取地理位置等。通过Python API,开发者可以编写...

    3ds Max Python API

    此外,Python API 还支持交互式脚本执行,这意味着用户可以在3ds Max环境中实时测试和调试代码。 总结来说,3ds Max Python API 是一个强大的工具,它极大地扩展了3ds Max的功能,使得艺术家和开发者可以通过Python...

    NXOpen Python API Refence 10.0.CHM

    NXOpen Python API Refence 10.0 在线文档,经过下载整理,制作成了离线文档,便于在断网环境下学习。

    TradeX.dll的Python API 源代码

    7. **订单管理**:API还支持对订单的管理,如查询订单状态、撤单等。这涉及到调用相关接口并处理返回的订单信息。 8. **安全与认证**:在实际使用中,为了保护交易账户的安全,需要对API调用进行用户身份验证,可能...

    Python面向对象编程:继承与多态性的实践指南

    继承和多态性是Python中实现代码重用和灵活设计的关键工具。通过继承,我们可以创建基于现有类的新的类,而多态性则允许我们编写更通用、更灵活的代码。在设计类和对象时,我们应该权衡继承和组合的使用,以实现代码...

    Python-Api签名验证样例

    本文将详细探讨Python中API签名验证的相关知识点,以"Python-Api签名验证样例"为例,结合`flask-apiSign-demo-master`这个压缩包中的示例代码进行解析。 签名验证的主要目的是防止数据被篡改、确保请求的来源可信...

    Python api函数手册

    以下是从提供的文件内容中提炼出的关于Python API的一些主要知识点。 首先,手册提到了Python内置函数,这些是Python语言中的基本功能,无需额外导入模块即可使用。例如,逻辑运算符and、or和not,这些函数用于布尔...

    Python文档和API

    5. **Python API**:API是指程序员可以使用的预定义函数和方法集合。Python的API包括了对标准库和第三方库的详细说明,如requests库用于HTTP请求,matplotlib库用于数据可视化。 6. **Python设计与实现**:对于想要...

    豆包 API 调用示例代码详解-Python版

    在本文中,我们将详细介绍如何使用 Python 调用豆包 API,并提供相关的事前准备和代码执行步骤。 一、事前准备 密钥申请: 要使用豆包 API,首先需要申请一个授权密钥。在上述代码中,密钥存储在 headers 字典的 ...

    photoshop-python-api:适用于Photoshop的Python API

    适用于Photoshop的Python API。 上面的示例是使用Photoshop Python API创建的。 在检查。 经过测试并使用了Photoshop版本: Photoshop版本 支持的 2020年 :check_mark_button: cc2019 :check_mark_button: ...

Global site tag (gtag.js) - Google Analytics