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

Python程序员最常犯的十个错误

时间:2023-03-16 12:39:04 科技观察

常见错误一:错误地使用表达式作为函数的默认参数在Python中,我们可以为函数的一个参数设置一个默认值,使该参数成为可选参数。虽然这是一个很好的语言特性,但当默认值是可变类型时,它也会导致一些令人困惑的情况。我们来看看下面的Python函数定义:>>>deffoo(bar=[]):#bar是一个可选参数,如果不提供bar的值,则默认为[],...bar。append("baz")#但是后面我们会看到这行代码会有问题。...returnbarPython程序员常犯的一个错误是想当然地认为:每次调用函数时,如果没有为可选参数传入值,那么这个可选参数将被设置为指定的默认值。在上面的代码中,你可能认为重复调用foo()函数应该总是返回'baz',因为默认情况下,每次执行foo()函数时(没有指定bar变量的值),bar变量设置为[](即新的空列表)。然而实际运行结果是这样的:>>>foo()["baz"]>>>foo()["baz","baz"]>>>foo()["baz","baz",“baz”]奇怪,对吧?为什么每次调用foo()函数时都会将默认值“baz”添加到现有列表中,而不是创建一个新的空列表?答案是可选参数默认值的设置在Python中只进行一次,即定义函数时。因此,只有在定义foo()函数时,bar参数才会被初始化为默认值(即空列表),但每次调用foo()函数时,都会继续使用原来的初始化生成该列表的bar参数。当然,一个常见的解决方案是:>>>deffoo(bar=None):...ifbarisNone:#orifnotbar:...bar=[]...bar.append("baz")...returnbar。..>>>foo()["baz"]>>>foo()["baz"]>>>foo()["baz"]FAQ2:错误使用类变量让我们看下面的例子:>>>A类(对象):...x=1...>>>B类(A):...通过...>>>C类(A):...通过...>>>printA.x,B.x,C.x111这个结果是正常的。>>>B.x=2>>>printA.x,B.x,C.x121嗯,结果符合预期。>>>A.x=3>>>printA.x,B.x,C.x323在Python语言中,类变量以字典的形式进行处理,并遵循方法解析顺序(MethodResolutionOrder,MRO)。因此,在上面的代码中,由于类C中没有属性x,所以解释器会去寻找它的基类(baseclass,虽然Python支持多重继承,但是在这个例子中,C的基类只有A)。也就是说,C并没有一个独立于A而真正属于自己的x属性。所以,引用C.x实际上是引用A.x。如果处理不好这里的关系,就会导致例子中的问题。常见错误三:错误指定异常块(exceptionblock)的参数请看下面代码:>>>try:...l=["a","b"]...int(l[2])...exceptValueError,IndexError:#Tocatchbothexceptions,right?...pass...Traceback(mostrecentcallast):File"",line3,inIndexError:listindexoutofrange这段代码的问题在于,except语句不支持以这种方式指定异常。在Python2.x中,需要使用变量e将异常绑定到可选的第二个参数,以便进一步查看异常。因此,在上面的代码中,except语句并没有捕获到IndexError异常;相反,它将发生的异常绑定到名为IndexError的参数。为了在except语句中正确捕获多个异常,应该将第一个参数指定为一个元组,然后在元组中写入要捕获的异常类型。此外,为了可移植性,请使用as关键字,它在Python2和Python3中均受支持。>>>try:...l=["a","b"]...int(l[2])...except(ValueError,IndexError)ase:...pass...>>>常见错误四:Python中对变量名解析的误解Python中的变量名解析遵循所谓的LEGB原则,即“L:localscope;E:前面结构中def或lambda的局部作用域;G:全局作用域;B:内置作用域”(Local,Enclosing,Global,Builtin),按顺序查找。看起来是不是很简单?然而,事实上,这个原则的运作方式有一些特别之处。说到这里,就不得不提到以下常见的Python编程错误。请看下面的代码:>>>x=10>>>deffoo():...x+=1...printx...>>>foo()Traceback(mostrecentcallast):File"",第1行,在文件“”中,第2行,infooUnboundLocalError:分配前引用的局部变量“x”出了什么问题?出现上述错误是因为当你给某个作用域内的变量赋值时,Python解释器会自动将该变量视为该作用域的局部变量,并会替换之前作用域内的任何同名变量。也正是因为这样,一开始还好的代码出现了,但是在某个函数内部加了赋值语句后出现了UnboundLocalError。难怪很多人都感到惊讶。Python程序员在使用列表时特别容易陷入这个陷阱。请看下面的代码示例:>>>lst=[1,2,3]>>>deffoo1():...lst.append(5)#Noproblemhere...>>>foo1()>>>lst[1,2,3,5]>>>lst=[1,2,3]>>>deffoo2():...lst+=[5]#...但这是错误的!...>>>foo2()Traceback(最近一次调用最后一次):文件“”,第1行,在文件“”,第2行,infooUnboundLocalError:localvariable'lst'referencedbeforeassignment嗯?为什么函数foo1工作正常,但foo2失败了?答案与前面的例子相同,但更难以捉摸。foo1函数不会为lst变量赋值,但foo2会。我们知道lst+=[5]只是lst=lst+[5]的简写,从中我们可以看出foo2函数试图为lst赋值(因此,它被认为是局部变量Python解释器的函数范围)。但是,我们要给lst赋值是根据lst变量本身(此时也算是函数局部作用域内的变量),也就是说这个变量还没有被定义。这是错误发生的地方。常见错误#5:在遍历列表时更改列表此代码的问题应该相当明显:>>>odd=lambdax:bool(x%2)>>>numbers=[nforninrange(10)]>>>foriinrange(len(numbers)):...ifodd(numbers[i]):...delnumbers[i]#BAD:Deletingitemfromalistwhileiteratingoverit...Traceback(mostrecentcallast):File"",line2,inIndexError:listindexoutofrange,在迭代时从列表或数组中删除元素,这是任何有经验的Python开发人员都会知道的。但是,尽管上面的示例很明显,但有经验的开发人员在编写更复杂的代码时很可能会无意中犯同样的错误。幸运的是,Python语言包含许多优雅的编程范式,如果使用得当,可以大大简化您的代码。简化代码的另一个好处是,在遍历列表时,不容易犯删除元素的错误。可以做到这一点的一种编程范例是列表理解。此外,列表理解对于避免这个问题特别有用。下面是使用列表理解对上述代码的重新实现:>>>odd=lambdax:bool(x%2)>>>numbers=[nforninrange(10)]>>>numbers[:]=[nforninnumbersifnotodd(n)]#ahh,thebeautyofitall>>>numbers[0,2,4,6,8]常见错误6:不明白Python如何在闭包中绑定变量,请看下面这段代码:>>>defcreate_multipliers():...return[lambdax:i*xforiinrange(5)]>>>formmultiplierincreate_multipliers():...printmultiplier(2)...你可能认为输出应该是这样的The:02468然而,实际输出是:88888惊讶!这个结果主要是由于Python中的后期绑定机制,即只有在调用内部函数时才会查询闭包中变量的值。因此,在上面的代码中,每次调用create_multipliers()返回的函数时,都会在附近的范围内查询变量i的值(此时,循环已经结束,因此变量i***被赋值值为4)。为了解决这个常见的Python问题,需要使用一些技巧:>>>defcreate_multipliers():...return[lambdax,i=i:i*xforiinrange(5)]...>>>formmultipliersincreate_multipliers():...printmultiplier(2)...02468注意!这里我们利用默认参数来实现这个lambda匿名函数。有人觉得优雅,有人觉得巧妙,还有人嗤之以鼻。但是,如果您是一名Python程序员,您无论如何都应该知道这个解决方法。常见错误七:模块之间出现循环依赖假设你有两个文件a.py和b.py,它们相互引用,如下图:a.py文件中的代码:importbdeff():returnb。xprintf()b.py文件中的代码:importax=1defg():printa.f()首先,我们尝试导入a.py模块:>>>importa1代码运行良好。也许这出乎你的意料。毕竟我们这里有循环引用的问题,所以肯定有问题,不是吗?答案是循环引用本身并不会引起问题。如果一个模块已经被引用过,Python可以不再引用它。但是,如果每个模块都在错误的时间尝试访问其他模块定义的函数或变量,那么您很可能会遇到麻烦。那么回到我们的例子,当我们导入a.py模块的时候,它引用b.py模块是不会有问题的,因为b.py模块在引用的时候不需要访问a.py模块的任何变量或者中定义的函数。模块b.py中对模块a的唯一引用就是调用模块a的foo()函数。但是该函数调用发生在g()函数中,并且在a.py或b.py模块中均未调用g()函数。所以,没问题。但是,如果我们尝试导入b.py模块(即在之前没有引用过a.py模块的前提下):>>>importbTraceback(mostrecentcalllast):File"",line1,inFile"b.py",line1,inimportaFile"a.py",line6,inprintf()File"a.py",line4,infreturnb.xAttributeError:'module'对象没有属性'x'哎呀。事情进展不顺利!这里的问题是,在导入b.py的过程中,它尝试引用a.py模块,然后a.py模块调用foo()函数,然后foo()函数尝试访问b.x变量。但是此时b.x变量还没有定义,所以会出现AttributeError异常。有一个很简单的方法可以解决这个问题,就是简单修改b.py模块,在g()函数内部只引用a.py:x=1defg():importa#Thiswillbeevaluatedonlywheng()iscalledprinta.f()现在我们再导入b.py模块就没有问题了:>>>importb>>>b.g()1#Printedafirsttimesincemodule'a'calls'printf()'attheend1#Printedasecondtime,thisoneisourcallto'g'常见错误八:Python语言的一大优势就是自带强大的标准库。但是,正因为如此,如果你不注意,你也可以给你的模块取一个与Python的标准库模块相同的名字(例如,如果你的代码中有一个名为email.py的模块,那么这会与Python标准库中的同名模块。)这可能会给您带来麻烦。比如在导入模块A时,如果模块A试图引用Python标准库中的模块B,但是因为你已经有了一个同名的模块B,模块A会在你自己的代码中错误地引用模块B,而不是Python标准库中的模块B。这也是一些严重错误的原因。因此,Python程序员应该格外小心,避免使用与Python标准库模块相同的名称。毕竟自己的模块改名比起PEP提案去改上游模块的名字让提案通过要容易的多。常见错误9:未能解决Python2和Python3之间的差异假设您有以下代码:importsysdefbar(i):ifi==1:raiseKeyError(1)ifi==2:raiseValueError(2)defbad():e=Nonetry:bar(int(sys.argv[1]))exceptKeyErrorase:print('keyerror')exceptValueErrorase:print('valueerror')print(e)bad()如果是Python2,那么代码可以正常工作:$pythonfoo.py1keyerror1$pythonfoo.py2valueerror2但现在,让我们切换到Python3并再次运行它:,line17,inbadprint(e)UnboundLocalError:localvariable'e'referencedbeforeassignment这是怎么回事?这里的“问题”是在Python3中,异常对象在except块的范围之外是不可访问的。(这样做的原因是,否则,堆栈帧将保持其引用循环,直到垃圾收集器运行并从内存中清除引用。)避免此问题的一种方法是使用except块范围之外的代码,维护对异常对象的引用(reference),以便可以访问异常对象。下面这段代码使用了这个方法,所以在Python2和Python3中的输出结果是一致的:importsysdefbar(i):ifi==1:raiseKeyError(1)ifi==2:raiseValueError(2)defgood():exception=Nonetry:bar(int(sys.argv[1]))exceptKeyErrorase:exception=eprint('keyerror')exceptValueErrorase:exception=eprint('valueerror')print(exception)good()下Python3运行代码:$python3foo.py1keyerror1$python3foo.py2valueerror2太棒了!常见错误10:错误使用del方法假设你在mod.py文件中写了如下代码:importfooclassBar(object):...def__del__(self):foo.cleanup(self.myhandle)之后,你在另一个_mod。py文件,执行以下操作:importmodmybar=mod.Bar()如果运行another_mod.py模块,将发生AttributeError异常。为什么?因为当解释器结束运行时,模块的全局变量将被设置为None。因此,在上面的示例中,foo在调用__del__方法之前已经设置为None。解决这个有点棘手的Python编程问题的一种方法是使用atexit.register()方法。这样,当您的程序完成执行时(即,在程序正常退出的情况下),您指定的处理程序将在解释器关闭之前运行。应用上述方法后,修改后的mod.py文件可能如下所示:)此实现支持在程序正常终止时干净地调用任何必要的清理函数。显然,在上面的例子中,foo.cleanup函数将决定如何处理绑定到self.myhandle的对象。概述Python是一种强大而灵活的编程语言,它提供了许多编程机制和范式,可以极大地提高工作效率。但与任何软件工具或语言一样,您有时可能会因为对语言功能的有限理解或欣赏而受阻而不是受益。正如一句谚语所说,“知道足够危险”(knowingenoughtobedangerous)。(译者注:这句谚语的意思是你自以为对某件事了解得够多,但当你真正去实施或实施它时,会给自己和他人带来危险。)不断地熟悉Python语言的一些精妙之处。这些优点,尤其是本文提到的前10个常见错误,将帮助您有效地使用该语言,同时避免犯一些更常见的错误。