当前位置: 首页 > 科技观察

Python编程中的反模式

时间:2023-03-12 21:50:38 科技观察

这篇文章收集了我在新手Python开发人员编写的代码中看到的非标准但偶尔微妙的问题。这篇文章的目的是帮助那些新手开发者度过编写难看的Python代码的阶段。为了照顾目标受众,本文做了一些简化(例如:在讨论迭代器时忽略了生成器和强大的迭代工具itertools)。对于那些新手开发人员,总是有使用反模式的理由,我已经尽可能地提供给他们。但通常这些反模式会使代码的可读性降低,更容易出现错误,并且不符合Python的编码风格。如果您正在寻找更多介绍性材料,我强烈推荐ThePythonTutorial或DiveintoPython。Python编程初学者喜欢用range来实现简单的迭代,获取迭代器长度范围内的迭代器中的每个元素:foriinrange(len(alist)):printalist[I]应该牢记:rangeandNotforsimpleiterationover序列。与那些用数字定义的for循环相比,用range实现的for循环虽然很自然,但是用在序列迭代时容易出bug,不如直接构造一个迭代器那么清晰:foriteminalist:printitemrange的滥用是容易引起意外的off-by-one错误,这通常是由于编程新手忘记了range生成的对象包括range的第一个参数而不是第二个参数,类似于java中的substring和许多其他此类函数。编程新手认为只有序列的末尾会产生错误:#Iteratetheentiresequencewrongmethodalist=['her','name','is','rio']foriinrange(0,len(alist)-1):#大小相差一(Offbyone)!printi,alist[I]范围使用不当的常见原因:1.需要在循环中使用索引。这不是使用索引的好理由:forindex,valueinenumerate(alist):printindex,value2。需要同时迭代两个循环,使用同一个索引得到两个值。这种情况下可以使用zip来实现:forword,numberinzip(words,numbers):printword,number3。需要迭代部分序列。在这种情况下,只能通过迭代序列切片来实现,注意添加必要的注释以表明意图:forwordinwords[1:]:#不包括第一个元素printword有异常:当你迭代一个大Sequence,切片操作带来的开销比较大。如果序列只有10个元素,那很好;但是当它有1000万个元素时,或者在对性能敏感的内部循环中进行切片时,开销就会变得非常大。在这种情况下,请考虑使用xrange而不是range[1]。除了用于遍历序列之外,range的一个重要用法是当你真的想生成一个数字序列而不是一个索引时:#Printfoo(x)for0<=x<5forxinrange(5):printfoo(x)如果你有这样的循环,正确使用列表理解:#Anugly,slowwaytobuildalistwords=['her','name','is','rio']alist=[]forwordinwords:alist.append(foo(word))您可以使用列表理解重写:words=['her','name','is','rio']alist=[foo(word)forwordinwords]为什么要这样做?一方面,你避免了正确的初始化列表可能导致的错误,另一方面,这样写的代码看起来干净整洁。对于那些有函数式编程背景的人来说,使用map函数可能感觉更熟悉,但在我看来这种方法不太像Pythonic。不使用列表解析的其他一些常见原因:1.需要嵌套循环。此时你可以嵌套整个列表理解,或者在列表理解的多行上使用循环:words=['her','name','is','rio']letters=[]forwordinwords:forletterinword:letters.append(letter)usingalistcomprehension:words=['her','name','is','rio']letters=[letterforwordinwordsforletterinword]注意:在具有多个循环的列表理解中,循环具有相同的order就像你没有使用列表理解一样。2.您需要在循环内进行条件测试。你只需要在列表理解中加入这个条件判断:words=['her','name','is','rio','1','2','3']alpha_words=[wordforwordinwordsifisalpha(word)]不使用列表推导的一个正当理由是你不能在列表推导中使用异常处理。如果迭代中的某些元素可能导致异常,则需要通过列表理解中的函数调用来转移可能的异常处理,或者根本不使用列表理解。性能缺陷在线性时间内检查内容从语法上看,检查列表或集合/字典是否包含元素表面上看是一样的,但在表面下却大不相同。如果需要反复检查某个数据结构是否包含某个元素,最好使用set而不是list。(如果要给要检查的元素关联一个值,可以使用dict;这样也实现了检查时间恒定。)#假设以列表开头lyrics_list=['her','name','is','rio']#避免下面的写法#LinearitycheckTimeprintword,"isinthelyrics"#***像这样写歌词,"isinthelyrics"[译者注:Python中set的元素和dict的键值都是hashable的,所以查找的时间复杂度是O(1)。]应该记住,创建集合会引入一次性开销,创建过程将花费线性时间,即使成员检查需要常数时间。所以如果你需要在循环中检查成员,最好花时间先创建集合,因为你只需要创建一次。#p#Variable泄漏循环通常,变量在Python中的范围比您在其他语言中的预期范围更广。例如:下面的代码不会被Java编译://Gettheindexofthelowest-indexediteminthearray//thatis>maxValuefor(inti=0;imaxValue){break;}}//i在这里是非法的:没有iprocessArray(y,i);然而,在Python中,相同的代码将始终顺利执行并获得预期的结果:foridx,valueinenumerate(y):ifvalue>max_value:breakprocessList(y,idx)这一次,循环永远不会执行,processList函数的调用会抛出NameError异常,因为idx没有定义。如果使用Pylint代码检查工具,会提示:useofvariableidxthatmaynotbedefined。解决方案总是显而易见的,在循环之前将idx设置为某个特殊值,这样您就知道如果循环永远不会执行,您将寻找什么。这种模式称为哨兵模式。那么什么值可以作为哨兵呢?在C和更早的时代,当整数统治编程世界时,返回-1是需要返回预期错误结果的函数的常见模式。例如,当你想返回列表中元素的索引值时:deffind_item(item,alist):#NoneismorePythonicthan-1result=-1foridx,other_iteminenumerate(alist):iother_item==item:result=idxbreakreturnresult一般来说,None在Python中是一个很好的标记值,即使它没有被Python标准类型一致地使用(例如:str。到所谓的外部范围-python文件中未包含的部分通过代码块(例如函数或类)。外部作用域相当于全局命名空间;出于本部分讨论的目的,您应该假设全局作用域的内容可以在单个Python中的任何位置访问文件。外部作用域对于定义在整个模块需要访问的文件顶部声明的常量非常强大。明智的做法是为外部作用域中的任何变量使用独特的名称,例如,常量名称IN_ALL_CAPS。这不会导致以下错误:importsys#Seethebuginth函数声明?defprint_file(filenam):"""Printeverylineofafile."""withopen(filename)asinput_file:forlineininput_file:printline.strip()if__name__=="__main__":filename=sys.argv[1]print_file(filename)如果你仔细观察,您会看到print_file函数的定义使用filenam来命名参数名称,但函数体引用的是filename。但是,程序仍然可以正常运行。为什么?在print_file函数中,当找不到局部变量filename时,下一步就是在全局范围内寻找它。由于对print_file的调用在外部范围内(即使有缩进),因此此处声明的文件名对print_file函数可见。那么如何避免此类错误呢?首先,不要在外部范围[3]中为IN_ALL_CAPS以外的全局变量设置任何值。参数解析最终交给了main函数,所以函数中的任何内部变量都不会在外部作用域中存活。这也提醒人们注意全局关键字global。如果您只是读取全局变量的值,则不需要全局关键字global。当你想改变全局变量名所指的对象时,你只需要使用global关键字。您可以在此处对StackOverflow上的全局关键字的讨论中获得更多相关信息。代码风格向PEP8致敬PEP8是Python代码的通用风格指南,你应该记住它并尽可能地遵循它,尽管有些人有充分的理由不同意一些小风格,比如数字空格缩进或使用空行。如果你不遵循PEP8,你应该有比“我只是不喜欢那种风格”更好的理由。下面的风格指南全部取自PEP8,似乎是程序员经常需要牢记的东西。测试是否为空如果你想检查容器类型(例如列表、字典、集合)是否为空,只需对其进行测试,而不是使用诸如检查len(x)>0:numbers=[-1,-2,-3]#Thiswillbeemptypositive_numbers=[numfornuminnumbersifnum>0]ifpositive_numbers:#Dosomethingawesome如果想在其他地方保存positive_numbers是否为空的结果,可以使用bool(positive_number)作为结果保存;bool用于判断if条件判断语句的真值。测试None如前所述,None可能是一个很好的标记值。那么如何检查呢?如果您明确想要测试None,而不仅仅是其他一些False项目(例如空容器或0),您可以使用:ifxisnotNone:#Dosomethingwithx如果您使用None作为标记,这也是Python风格,例如当你想区分None和0时。如果你只是测试一个变量是否有一些有用的值,一个简单的if模式通常就足够了:ifx:#Dosomethingwithx例如:如果x应该是一个容器类型,但是x可能被用作另一个函数的返回结果值变成None,你应该立即考虑到这一点。您需要注意是否更改了传递给x的值,否则您可能认为True或0.0是有用的值,但程序不会按照您想要的方式执行。译者注:[1]在Python2.x中,range生成list对象,而xrange生成range对象;Python3.x废除了xrange,range对象统一为range对象,可以显式显示,用列表工厂函数生成列表;[2]string.find(str)返回字符串中str开始的索引值,不存在则返回-1;[3]不要在外部动作中为函数中的局部变量名设置任何值,以防止在函数内部调用局部变量而在外部作用域调用同名变量时出错。本文由伯乐在线-小雷翻译自lignos