`
huangz
  • 浏览: 322330 次
  • 性别: 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 提供了底层接口,让...

    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中文api文档+学习笔记

    这不仅可以帮助用户深入理解Python的语法和API,还可以通过学习笔记了解实际应用中的技巧和经验,对于提升Python技能和解决实际问题非常有帮助。无论你是初学者还是资深开发者,这个资源集合都是值得珍藏和参考的...

    3dmax python api

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

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

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

    pythonapi接口开发教程-Python-接口开发入门解析.pdf

    Python API接口开发是一种常见的软件开发任务,特别是在微服务架构中,接口扮演着连接不同系统和服务的重要角色。本教程主要介绍了如何使用Python的Flask框架进行API接口的开发,包括其作用、基本步骤以及项目目录...

    python API

    在Python中,还有元编程的概念,即使用Python来操作Python自身,这在设计和使用API时也十分常见。比如,利用`__getattr__`和`__setattr__`方法可以实现动态属性,`__call__`可以使得对象可像函数一样调用。 Python ...

    openpose使用pythonApi.zip

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

    opencv-python api手册.rar

    opencv-python api手册.rar

    TradeX.dll的Python API 源代码

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

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

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

    SL4A之Python_API_中英文参考

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

    NXOpen Python API Refence 10.0.CHM

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

    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设计与实现**:对于想要...

    Python_API.tar.gz_Python A_pys60 a_python_python api中文_symbian a

    在本案例中,"Python_API.tar.gz" 是一个压缩文件,包含与 Python 相关的 API 文档或代码,特别关注的是针对 pys60 平台的开发。pys60 是一个 Python 的移植版本,专为塞班(Symbian)操作系统设计,使得开发者能够...

    Python课程设计之俄罗斯方块

    在本项目中,"Python课程设计之俄罗斯方块"是一个基于Python编程语言实现的经典游戏——俄罗斯方块。这个课程设计旨在帮助学生理解Python的基本语法、控制结构、对象导向编程以及图形用户界面(GUI)的创建。以下是...

Global site tag (gtag.js) - Google Analytics