api是什么意思(API)
admin
2023-10-26 22:00:05

负责任的图书馆作者与用户之间的十项协议。

——A杰西(作者)

想象你是一个创造者,为一个生物设计身体。出于善意,你希望它随时间进化:一是因为它必须对环境的变化做出反应;其次,因为你的智慧在增长,你为这个小东西想出了更好的设计。它不应该永远保持不变。

狡猾的人

但是,这种生物可能取决于它目前的解剖特征。你不能肆无忌惮地添加翅膀或改变它的比例。它需要一个有序的过程来适应新的身体。作为一个负责任的设计师,你如何温和地引导这种生物走向更大的进步?

负责任的库维护者也是如此。我们向依赖我们代码的人保证,我们将发布错误修复和有用的新功能。如果对图书馆的未来有好处,我们有时会删除某些功能。我们会不断创新,但不会破坏使用我们库的人的代码。怎样才能一次实现所有这些目标?

添加有用的特性

您的库不应该永远保持不变:您应该添加一些功能,使您的库更适合用户。比如你有一只爬行动物,有一只飞翼很有用,那就把它加进去。

class爬虫: @ PropertyDefteeth(self):返回' Sharpfang' #如果翅膀有用就加!@ PropertyDefwings (self) :返回‘雄壮之翼’但是要注意功能有风险。考虑Python标准库中的以下特性,看看它有什么问题。

Bool(日期时间。time (9,30))==True Bool (datetime。time (0,0))==False这很奇怪:将任何time对象转换为布尔值都会得到true,除了午夜。更糟糕的是,时区感知的规则更加奇怪。)

我写Python十几年了,直到上周才发现这个规律。这种奇怪的行为会在用户代码中造成什么样的bug?

例如,具有创建事件功能的日历应用程序。如果一个事件有一个结束时间,那么函数也应该要求它有一个开始时间。

defcreate_event(day,start_time=None,end _ time=None): ifend _ time and not start _ time : raisevalueerror(' Can ' tpassend _ time without start _ time ')# switches的会议从午夜持续到凌晨4点。约会。今天(),日期时间。时间(0,0),日期时间。time (4,0))不幸的是,对于女巫来说,从午夜开始的事件无法通过验证。当然,懂得午夜怪癖的细心程序员也能正确写出这个函数。

Def create _ event (day,start _ time=none,end _ time=none)3360 ifend _ timeisnotnonendstart _ time is none 3360 RaiseValueError('不能传递_ timewithoutstart _ time '),但这种微妙之处令人担忧。如果一个库的作者想要创建一个伤害用户的API,那么& quot功能& quot像午夜布尔转换是非常有效的。

人类在追逐智慧

然而,一个负责任的创作者的目标是让你的库易于正确使用。

这个函数是由TimPeters在2002年首次编写datetime模块时引起的。即使是像Tim这样为Python打下基础的高手,也有可能出错。这种怪异后来被消除了,现在所有的布尔值都是真的。

在#Python3.5之后,bool (datetime。time (9,30))==true Bool (datetime。time (0,0))==真正的程序员不知道午夜怪癖的怪癖,现在可以摆脱这个晦涩难懂的bug了,但是一想到现在任何依赖怪异的旧行为的代码都不注意变化,我就感到紧张。如果这个不好的特性从来没有实现过就更好了。这导致了库维护者的第一个承诺:

第一个约定:避免糟糕的特性

最痛苦的变化就是你要删除一个特征。一般来说,避免不良特性的一个方法就是少添加特性!没有充分的理由,不要使用公共方法、类、函数或属性。因此:

第二个约定:最小化特性

特征就像孩子:在激情时刻孕育,但必须支撑多年。不要因为你能做傻事就去做傻事。不要画蛇添足!

无翼蛇

但是,当然,在许多情况下,用户需要你的图书馆没有提供的东西。你如何为他们选择合适的功能?这是另一个警告故事。

一个来自asyncio的警示故事

您可能知道,当您调用协程函数时,它将返回一个协程对象:

asyncd

efmy_coroutine():passprint(my_coroutine())

你的代码必须“等待(await)”这个对象以此来运行协程。人们很容易忘记这一点,所以asyncio的开发人员想要一个“调试模式”来捕捉这个错误。当协程在没有等待的情况下被销毁时,调试模式将打印一个警告,并在其创建的行上进行回溯。

当YurySelivanov实现调试模式时,他添加了一个“协程装饰器”的基础特性。装饰器是一个函数,它接收一个协程并返回任何内容。Yury使用它在每个协程上接入警告逻辑,但是其他人可以使用它将协程转换为字符串“hi!”。

importsysdefmy_wrapper(coro):return'hi!'sys.set_coroutine_wrapper(my_wrapper)asyncdefmy_coroutine():passprint(my_coroutine())hi!

这是一个地狱般的定制。它改变了“异步(async)“的含义。调用一次set_coroutine_wrapper将在全局永久改变所有的协程函数。正如NathanielSmith所说:“一个有问题的API”很容易被误用,必须被删除。如果asyncio开发人员能够更好地按照其目标来设计该特性,他们就可以避免删除该特性的痛苦。负责任的创建者必须牢记这一点:

第三个约定:保持特性单一

幸运的是,Yury有良好的判断力,他将该特性标记为临时,所以asyncio用户知道不能依赖它。Nathaniel可以用更单一的功能替换set_coroutine_wrapper,该特性只定制回溯深度。

importsyssys.set_coroutine_origin_tracking_depth(2)asyncdefmy_coroutine():passprint(my_coroutine())RuntimeWarning:'my_coroutine'wasneverawaitedCoroutinecreatedat(mostrecentcalllast)File"script.py",line8,inprint(my_coroutine())

这样好多了。没有可以更改协程的类型的其他全局设置,因此asyncio用户无需编写防御代码。造物主应该像Yury一样有远见。

第四个约定:标记实验特征“临时”

如果你只是预感你的生物需要犄角和四叉舌,那就引入这些特性,但将它们标记为“临时”。


Serpentwithhorns


你可能会发现犄角是无关紧要的,但是四叉舌是有用的。在库的下一个版本中,你可以删除前者并标记后者为正式的。

删除特性

无论我们如何明智地指导我们的生物进化,总会有一天想要删除一个正式特征。例如,你可能已经创建了一只蜥蜴,现在你选择删除它的腿。也许你想把这个笨拙的家伙变成一条时尚而现代的蟒蛇。


Lizardtransformedtosnake


删除特性主要有两个原因。首先,通过用户反馈或者你自己不断增长的智慧,你可能会发现某个特性是个坏主意。午夜怪癖的古怪行为就是这种情况。或者,最初该特性可能已经很好地适应了你的库环境,但现在生态环境发生了变化,也许另一个神发明了哺乳动物,你的生物想要挤进哺乳动物的小洞穴里,吃掉里面美味的哺乳动物,所以它不得不失去双腿。


Amouse


同样,Python标准库会根据语言本身的变化删除特性。考虑asyncio的Lock功能,在把await作为一个关键字添加进来之前,它一直在等待:

lock=asyncio.Lock()asyncdefcritical_section():awaitlocktry:print('holdinglock')finally:lock.release()

但是现在,我们可以做“异步锁”:

lock=asyncio.Lock()asyncdefcritical_section():asyncwithlock:print('holdinglock')

新方法好多了!很短,并且在一个大函数中使用其他try-except块时不容易出错。因为“尽量找一种,最好是唯一一种明显的解决方案”,旧语法在Python3.7中被弃用,并且很快就会被禁止。

不可避免的是,生态变化会对你的代码产生影响,因此要学会温柔地删除特性。在此之前,请考虑删除它的成本或好处。负责任的维护者不会愿意让用户更改大量代码或逻辑。(还记得Python3在重新添加会u字符串前缀之前删除它是多么痛苦吗?)如果代码删除是机械性的动作,就像一个简单的搜索和替换,或者如果该特性是危险的,那么它可能值得删除。

是否删除特性


Balancescales


反对支持代码必须改变改变是机械性的逻辑必须改变特性是危险的

就我们饥饿的蜥蜴而言,我们决定删除它的腿,这样它就可以滑进老鼠洞里吃掉它。我们该怎么做呢?我们可以删除walk方法,像下面一样修改代码:

classReptile:defwalk(self):print('stepstepstep')

变成这样:

classReptile:defslither(self):print('slideslideslide')

这不是一个好主意,这个生物习惯于走路!或者,就库而言,你的用户拥有依赖于现有方法的代码。当他们升级到最新库版本时,他们的代码将会崩溃。

#用户的代码,哦,不!Reptile.walk()

因此,负责任的创建者承诺:

第五条预定:温柔地删除

温柔地删除一个特性需要几个步骤。从用腿走路的蜥蜴开始,首先添加新方法slither。接下来,弃用旧方法。

importwarningsclassReptile:defwalk(self):warnings.warn("walkisdeprecated,useslither",DeprecationWarning,stacklevel=2)print('stepstepstep')defslither(self):print('slideslideslide')

Python的warnings模块非常强大。默认情况下,它会将警告输出到stderr,每个代码位置只显示一次,但你可以禁用警告或将其转换为异常,以及其它选项。

一旦将这个警告添加到库中,PyCharm和其他IDE就会使用删除线呈现这个被弃用的方法。用户马上就知道该删除这个方法。

Reptile().walk()

当他们使用升级后的库运行代码时会发生什么?

$python3script.pyDeprecationWarning:walkisdeprecated,useslitherscript.py:14:Reptile().walk()stepstepstep

默认情况下,他们会在stderr上看到警告,但脚本会成功并打印“stepstepstep”。警告的回溯显示必须修复用户代码的哪一行。(这就是stacklevel参数的作用:它显示了用户需要更改的调用,而不是库中生成警告的行。)请注意,错误消息有指导意义,它描述了库用户迁移到新版本必须做的事情。

你的用户可能会希望测试他们的代码,并证明他们没有调用弃用的库方法。仅警告不会使单元测试失败,但异常会失败。Python有一个命令行选项,可以将弃用警告转换为异常。

>python3-Werror::DeprecationWarningscript.pyTraceback(mostrecentcalllast):File"script.py",line14,inReptile().walk()File"script.py",line8,inwalkDeprecationWarning,stacklevel=2)DeprecationWarning:walkisdeprecated,useslither

现在,“stepstepstep”没有输出出来,因为脚本以一个错误终止。

因此,一旦你发布了库的一个版本,该版本会警告已启用的walk方法,你就可以在下一个版本中安全地删除它。对吧?

考虑一下你的库用户在他们项目的requirements中可能有什么。

#用户的requirements.txt显示reptile包的依赖关系reptile

下次他们部署代码时,他们将安装最新版本的库。如果他们尚未处理所有的弃用,那么他们的代码将会崩溃,因为代码仍然依赖walk。你需要温柔一点,你必须向用户做出三个承诺:维护更改日志,选择版本化方案和编写升级指南。

第六个约定:维护变更日志

你的库必须有更改日志,其主要目的是宣布用户所依赖的功能何时被弃用或删除。

版本1.1中的更改

新特性

新功能Reptile.slither()

弃用

Reptile.walk()已弃用,将在2.0版本中删除,请使用slither()

负责任的创建者会使用版本号来表示库发生了怎样的变化,以便用户能够对升级做出明智的决定。“版本化方案”是一种用于交流变化速度的语言。

第七个约定:选择一个版本化方案

有两种广泛使用的方案,语义版本控制和基于时间的版本控制。我推荐任何库都进行语义版本控制。Python的风格在PEP440中定义,像pip这样的工具可以理解语义版本号。

如果你为库选择语义版本控制,你可以使用版本号温柔地删除腿,例如:

1.0:第一个“稳定”版,带有walk()1.1:添加slither(),废弃walk()2.0:删除walk()

你的用户依赖于你的库的版本应该有一个范围,例如:

#用户的requirements.txtreptile>=1,<2

这允许他们在主要版本中自动升级,接收错误修正并可能引发一些弃用警告,但不会升级到下个主要版本并冒着更改破坏其代码的风险。

如果你遵循基于时间的版本控制,则你的版本可能会编号:

2017.06.0:2017年6月的版本2018.11.0:添加slither(),废弃walk()2019.04.0:删除walk()

用户可以这样依赖于你的库:

#用户的requirements.txt,基于时间控制的版本reptile==2018.11.*

这非常棒,但你的用户如何知道你的版本方案,以及如何测试代码来进行弃用呢?你必须告诉他们如何升级。

第八个约定:写一个升级指南

下面是一个负责任的库创建者如何指导用户:

升级到2.0

从弃用的API迁移

请参阅更改日志以了解已弃用的特性。

启用弃用警告

升级到1.1并使用以下代码测试代码:

python-Werror::DeprecationWarning

​​​​​​现在可以安全地升级了。

你必须通过向用户显示命令行选项来教会用户如何处理弃用警告。并非所有Python程序员都知道这一点——我自己就每次都得查找这个语法。注意,你必须发布一个版本,它输出来自每个弃用的API的警告,以便用户可以在再次升级之前使用该版本进行测试。在本例中,1.1版本是小版本。它允许你的用户逐步重写代码,分别修复每个弃用警告,直到他们完全迁移到最新的API。他们可以彼此独立地测试代码和库的更改,并隔离bug的原因。

如果你选择语义版本控制,则此过渡期将持续到下一个主要版本,从1.x到2.0,或从2.x到3.0以此类推。删除生物腿部的温柔方法是至少给它一个版本来调整其生活方式。不要一次性把腿删掉!


Askink


版本号、弃用警告、更改日志和升级指南可以协同工作,在不违背与用户约定的情况下温柔地改进你的库。Twisted项目的兼容性政策解释的很漂亮:

“先行者总是自由的”

运行的应用程序在没有任何警告的情况下都可以升级为Twisted的一个次要版本。

换句话说,任何运行其测试而不触发Twisted警告的应用程序应该能够将其Twisted版本升级至少一次,除了可能产生新警告之外没有任何不良影响。

现在,我们的造物主已经获得了智慧和力量,可以通过添加方法来添加特性,并温柔地删除它们。我们还可以通过添加参数来添加特性,但这带来了新的难度。你准备好了吗?

添加参数

想象一下,你只是给了你的蛇形生物一对翅膀。现在你必须允许它选择是滑行还是飞行。目前它的move功能只接受一个参数。

#你的库代码defmove(direction):print(f'slither{direction}')#用户的应用move('north')

你想要添加一个mode参数,但如果用户升级库,这会破坏他们的代码,因为他们只传递了一个参数。

#你的库代码defmove(direction,mode):assertmodein('slither','fly')print(f'{mode}{direction}')#一个用户的代码,出现错误!move('north')

一个真正聪明的创建者者会承诺不会以这种方式破坏用户的代码。

第九条约定:兼容地添加参数

要保持这个约定,请使用保留原始行为的默认值添加每个新参数。

#你的库代码defmove(direction,mode='slither'):assertmodein('slither','fly')print(f'{mode}{direction}')#用户的应用move('north')

随着时间推移,参数是函数演化的自然历史。它们首先列出最老的参数,每个都有默认值。库用户可以传递关键字参数以选择特定的新行为,并接受所有其他行为的默认值。

#你的库代码defmove(direction,mode='slither',turbo=False,extra_sinuous=False,hail_lyft=False):#...#用户应用move('north',extra_sinuous=True)

但是有一个危险,用户可能会编写如下代码:

#用户应用,简写move('north','slither',False,True)

如果在你在库的下一个主要版本中去掉其中一个参数,例如turbo,会发生什么?

#你的库代码,下一个主要版本中"turbo"被删除defmove(direction,mode='slither',extra_sinuous=False,hail_lyft=False):#...#用户应用,简写move('north','slither',False,True)

用户的代码仍然能编译,这是一件坏事。代码停止了曲折的移动并开始招呼Lyft,这不是它的本意。我相信你可以预测我接下来要说的内容:删除参数需要几个步骤。当然,首先弃用trubo参数。我喜欢这种技术,它可以检测任何用户的代码是否依赖于这个参数。

#你的库代码_turbo_default=object()defmove(direction,mode='slither',turbo=_turbo_default,extra_sinuous=False,hail_lyft=False):ifturboisnot_turbo_default:warnings.warn("'turbo'isdeprecated",DeprecationWarning,stacklevel=2)else:#Theolddefault.turbo=False

但是你的用户可能不会注意到警告。警告声音不是很大:它们可以在日志文件中被抑制或丢失。用户可能会漫不经心地升级到库的下一个主要版本——那个删除turbo的版本。他们的代码运行时将没有错误、默默做错误的事情!正如Python之禅所说:“错误绝不应该被默默pass”。实际上,爬行动物的听力很差,所有当它们犯错误时,你必须非常大声地纠正它们。


Womanridinganalligator


保护用户的最佳方法是使用Python3的星型语法,它要求调用者传递关键字参数。

#你的库代码#所有“*”后的参数必须以关键字方式传输。defmove(direction,*,mode='slither',turbo=False,extra_sinuous=False,hail_lyft=False):#...#用户代码,简写#错误!不能使用位置参数,关键字参数是必须的move('north','slither',False,True)

有了这个星,以下是唯一允许的语法:

#用户代码move('north',extra_sinuous=True)

现在,当你删除turbo时,你可以确定任何依赖于它的用户代码都会明显地提示失败。如果你的库也支持Python2,这没有什么大不了。你可以模拟星型语法(归功于BrettSlatkin):

#你的库代码,兼容Python2defmove(direction,**kwargs):mode=kwargs.pop('mode','slither')turbo=kwargs.pop('turbo',False)sinuous=kwargs.pop('extra_sinuous',False)lyft=kwargs.pop('hail_lyft',False)ifkwargs:raiseTypeError('Unexpectedkwargs:%r'%kwargs)#...

要求关键字参数是一个明智的选择,但它需要远见。如果允许按位置传递参数,则不能仅在以后的版本中将其转换为仅关键字。所以,现在加上星号。你可以在asyncioAPI中观察到,它在构造函数、方法和函数中普遍使用星号。尽管到目前为止,Lock只接受一个可选参数,但asyncio开发人员立即添加了星号。这是幸运的。

#Inasyncio.classLock:def__init__(self,*,loop=None):#...

现在,我们已经获得了改变方法和参数的智慧,同时保持与用户的约定。现在是时候尝试最具挑战性的进化了:在不改变方法或参数的情况下改变行为。

改变行为

假设你创造的生物是一条响尾蛇,你想教它一种新行为。


Rattlesnake


横向移动!这个生物的身体看起来是一样的,但它的行为会发生变化。我们如何为这一进化步骤做好准备?


ImagebyHCA[CCBY-SA4.0],viaWikimediaComm


当行为在没有新函数或新参数的情况下发生更改时,负责任的创建者可以从Python标准库中学习。很久以前,os模块引入了stat函数来获取文件统计信息,比如创建时间。起初,这个时间总是整数。

>>>os.stat('file.txt').st_ctime1540817862

有一天,核心开发人员决定在os.stat中使用浮点数来提供亚秒级精度。但他们担心现有的用户代码还没有做好准备更改。于是他们在Python2.3中创建了一个设置stat_float_times,默认情况下是False。用户可以将其设置为True来选择浮点时间戳。

>>>#Python2.3.>>>os.stat_float_times(True)>>>os.stat('file.txt').st_ctime1540817862.598021

从Python2.5开始,浮点时间成为默认值,因此2.5及之后版本编写的任何新代码都可以忽略该设置并期望得到浮点数。当然,你可以将其设置为False以保持旧行为,或将其设置为True以确保所有Python版本都得到浮点数,并为删除stat_float_times的那一天准备代码。

多年过去了,在Python3.1中,该设置已被弃用,以便为人们为遥远的未来做好准备,最后,经过数十年的旅程,这个设置被删除。浮点时间现在是唯一的选择。这是一个漫长的过程,但负责任的神灵是有耐心的,因为我们知道这个渐进的过程很有可能于意外的行为变化拯救用户。

第十个约定:逐渐改变行为

以下是步骤:

添加一个标志来选择新行为,默认为False,如果为False则发出警告将默认值更改为True,表示完全弃用标记删除该标志

如果你遵循语义版本控制,版本可能如下:

库版本库API用户代码1.0没有标志预期的旧行为1.1添加标志,默认为False,如果是False,则警告设置标志为True,处理新行为2.0改变默认为True,完全弃用标志处理新行为3.0移除标志处理新行为

你需要两个主要版本来完成该操作。如果你直接从“添加标志,默认为False,如果是False则发出警告”变到“删除标志”,而没有中间版本,那么用户的代码将无法升级。为1.1正确编写的用户代码必须能够升级到下一个版本,除了新警告之外,没有任何不良影响,但如果在下一个版本中删除了该标志,那么该代码将崩溃。一个负责任的神明从不违反扭曲的政策:“先行者总是自由的”。

负责任的创建者


Demeter


我们的10个约定大致可以分为三类:

谨慎发展

避免不良功能最小化特性保持功能单一标记实验特征“临时”温柔删除功能

严格记录历史

维护更改日志选择版本方案编写升级指南

缓慢而明显地改变

兼容添加参数逐渐改变行为

如果你对你所创造的物种保持这些约定,你将成为一个负责任的造物主。你的生物的身体可以随着时间的推移而进化,一直在改善和适应环境的变化,而不是在生物没有准备好就突然改变。如果你维护一个库,请向用户保留这些承诺,这样你就可以在不破坏依赖该库的代码的情况下对库进行更新。


这篇文章最初是在A.JesseJiryuDavis的博客上'出现的,经允许转载。

插图参考:

《世界进步》,DelphianSociety,1913《走进蛇的历史》,CharlesOwen,1742关于哥斯达黎加的batrachia和爬行动物,关于尼加拉瓜和秘鲁的爬行动物和鱼类学的记录,EdwardDrinkerCope,1875《自然史》,RichardLydekkeret.al.,1897MesPrisons,SilvioPellico,1843Tierfotoagentur/m.blue-shadow洛杉矶公共图书馆,1930

via:https://opensource.com/article/19/5/api-evolution-right-way

作者:A.Jesse选题:lujun9972译者:MjSeven校对:wxy

本文由LCTT原创编译,Linux中国荣誉推出

点击“了解更多”可访问文内链接

相关内容

热门资讯

金花创建房间/微信金花房卡怎么... 1.微信渠道:(荣耀联盟)大厅介绍:咨询房/卡添加微信:88355042 2.微信游戏中心:打开微...
金花房间卡/金花房卡如何购买/... 金花房间卡/金花房卡如何购买/新超圣金花房卡正版如何购买新超圣是一款非常受欢迎的游戏,咨询房/卡添加...
牛牛创建房间/金花房卡批发/神... 微信游戏中心:神牛大厅房卡在哪里买打开微信,添加客服【88355042】,进入游戏中心或相关小程序,...
链接牛牛/牛牛房卡游戏代理/鸿... 鸿运大厅房卡更多详情添加微:33549083、 2、在商城页面中选择房卡选项。 3、根...
科技实测!牛牛房卡怎么获得/乐... 微信游戏中心:乐酷大厅房卡在哪里买打开微信,添加客服【88355042】,进入游戏中心或相关小程序,...