Python程序员的10个常见错误(转)

add by zhj:虽然学Python也有两年了,但这些问题的确容易犯,看来对Python的理解还有些地方不深入。先转了,有时间再好好看

译文:http://blog.jobbole.com/68256/

本文由 伯乐在线 - datorhjaelten 翻译。未经许可,禁止转载!
英文出处:toptal。欢迎加入翻译小组

关于Python

Python是一门解释性的,面向对象的,并具有动态语义的高级编程语言。它高级的内置数据结构,结合其动态类型和动态绑定的特性,使得它在快速应用程序开发Rapid Application Development)中颇为受欢迎,同时Python还能作为脚本语言或者胶水语言讲现成的组件或者服务结合起来。Python支持模块(modules)和包(packages),所以也鼓励程序的模块化以及代码重用。

关于本文

Python简单、易学的语法可能会误导一些Python程序员(特别是那些刚接触这门语言的人们),可能会忽略某些细微之处和这门语言的强大之处。

考虑到这点,本文列出了“十大”甚至是高级的Python程序员都可能犯的,却又不容易发现的细微错误。(注意:本文是针对比《Python程序员常见错误稍微高级一点读者,对于更加新手一点的Python程序员,有兴趣可以读一读那篇文章)

常见错误1:在函数参数中乱用表达式作为默认值

Python允许给一个函数的某个参数设置默认值以使该参数成为一个可选参数。尽管这是这门语言很棒的一个功能,但是这当这个默认值是可变对象mutable)时,那就有些麻烦了。例如,看下面这个Python函数定义:


1

2

3

>>> def foo(bar=[]):        # bar是可选参数,如果没有指明的话,默认值是[]

...    bar.append("baz")    # 但是这行可是有问题的,走着瞧…

...    return bar

人们常犯的一个错误是认为每次调用这个函数时不给这个可选参数赋值的话,它总是会被赋予这个默认表达式的值。例如,在上面的代码中,程序员可能会认为重复调用函数foo() (不传参数bar给这个函数),这个函数会总是返回‘baz’,因为我们假定认为每次调用foo()的时候(不传bar),参数bar会被置为[](即,一个空的列表)。

那么我们来看看这么做的时候究竟会发生什么:


1

2

3

4

5

6

>>> foo()

["baz"]

>>> foo()

["baz", "baz"]

>>> foo()

["baz", "baz", "baz"]

嗯?为什么每次调用foo()的时候,这个函数总是在一个已经存在的列表后面添加我们的默认值“baz”,而不是每次都创建一个的列表?

答案是一个函数参数的默认值,仅仅在该函数定义的时候,被赋值一次。如此,只有当函数foo()第一次被定义的时候,才讲参数bar的默认值初始化到它的默认值(即一个空的列表)。当调用foo()的时候(不给参数bar),会继续使用bar最早初始化时的那个列表。

由此,可以有如下的解决办法:


1

2

3

4

5

6

7

8

9

10

11

12

>>> def foo(bar=None):

...    if bar is None:      # 或者用 if not bar:

...        bar = []

...    bar.append("baz")

...    return bar

...

>>> foo()

["baz"]

>>> foo()

["baz"]

>>> foo()

["baz"]

常见错误2:不正确的使用类变量

看下面一个例子:


1

2

3

4

5

6

7

8

9

10

11

>>> class A(object):

...     x = 1

...

>>> class B(A):

...     pass

...

>>> class C(A):

...     pass

...

>>> print A.x, B.x, C.x

1 1 1

看起来没有问题。


1

2

3

>>> B.x = 2

>>> print A.x, B.x, C.x

1 2 1

嗯哈,还是和预想的一样。


1

2

3

>>> A.x = 3

>>> print A.x, B.x, C.x

3 2 3

我了个去。只是改变了A.x,为啥C.x也变了?

在Python里,类变量通常在内部被当做字典来处理并遵循通常所说的方法解析顺序Method Resolution Order (MRO))。因此在上面的代码中,因为属性x在类C中找不到,因此它会往上去它的基类中查找(在上面的例子中只有A这个类,当然Python是支持多重继承(multiple inheritance)的)。换句话说,C没有它自己独立于A的属性x。因此对C.x的引用实际上是对A.x的引用。(B.x不是对A.x的引用是因为在第二步里B.x=2将B.x引用到了2这个对象上,倘若没有如此,B.x仍然是引用到A.x上的。——译者注)

常见错误3:在异常处理时错误的使用参数

假设你有如下的代码:


1

2

3

4

5

6

7

8

9

>>> try:

...     l = ["a", "b"]

...     int(l[2])

... except ValueError, IndexError:  # 想捕捉两个异常

...     pass

...

Traceback (most recent call last):

  File "<stdin>", line 3, in <module>

IndexError: list index out of range

这里的问题在于except语句不会像这样去接受一系列的异常。并且,在Python 2.x里面,语法except Exception, e是用来将异常和这个可选的参数绑定起来(即这里的e),以用来在后面查看的。因此,在上面的代码中,IndexError异常不会被except语句捕捉到;而最终ValueError这个异常被绑定在了一个叫做IndexError的参数上。

在except语句中捕捉多个异常的正确做法是将所有想要捕捉的异常放在一个元组tuple)里并作为第一个参数给except语句。并且,为移植性考虑,使用as关键字,因为Python 2和Python 3都支持这样的语法,例如:


1

2

3

4

5

6

7

>>> try:

...     l = ["a", "b"]

...     int(l[2])

... except (ValueError, IndexError) as e: 

...     pass

...

>>>

常见错误4:误解Python作用域的规则

Python的作用域解析是基于叫做LEGB(Local(本地),Enclosing(封闭),Global(全局),Built-in(内置))的规则进行操作的。这看起来很直观,对吧?事实上,在Python中这有一些细微的地方很容易出错。看这个例子:


1

2

3

4

5

6

7

8

9

10

>>> x = 10

>>> def foo():

...     x += 1

...     print x

...

>>> foo()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "<stdin>", line 2, in foo

UnboundLocalError: local variable ‘x‘ referenced before assignment

这是怎么回事?

这是因为,在一个作用域里面给一个变量赋值的时候,Python自动认为这个变量是这个作用域的本地变量,并屏蔽作用域外的同名的变量。

很多时候可能在一个函数里添加一个赋值的语句会让你从前本来工作的代码得到一个UnboundLocalError。(感兴趣的话可以读一读这篇文章。)

在使用列表lists的时候,这种情况尤为突出。看下面这个例子:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

>>> lst = [1, 2, 3]

>>> def foo1():

...     lst.append(5)   # 这没有问题...

...

>>> foo1()

>>> lst

[1, 2, 3, 5]

>>> lst = [1, 2, 3]

>>> def foo2():

...     lst += [5]      # ... 这就有问题了!

...

>>> foo2()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "<stdin>", line 2, in foo

UnboundLocalError: local variable ‘lst‘ referenced before assignment

嗯?为什么foo2有问题,而foo1没有问题?

答案和上一个例子一样,但是更加不易察觉。foo1并没有给lst赋值,但是foo2尝试给lst赋值。注意lst+=[5]只是lst=lst+[5]的简写,由此可以看到我们尝试给lst赋值(因此Python假设作用域为本地)。但是,这个要赋给lst的值是基于lst本身的(这里的作用域仍然是本地),而lst却没有被定义,这就出错了。

常见错误5:在遍历列表的同时又在修改这个列表

下面这个例子中的代码应该比较明显了:


1

2

3

4

5

6

7

8

9

>>> odd = lambda x : bool(x % 2)

>>> numbers = [n for n in range(10)]

>>> for i in range(len(numbers)):

...     if odd(numbers[i]):

...         del numbers[i]  # 这不对的:在遍历列表时删掉列表的元素。

...

Traceback (most recent call last):

      File "<stdin>", line 2, in <module>

IndexError: list index out of range

遍历一个列表或者数组的同时又删除里面的元素,对任何有经验的软件开发人员来说这是个很明显的错误。但是像上面的例子那样明显的错误,即使有经验的程序员也可能不经意间在更加复杂的程序中不小心犯错。

所幸,Python集成了一些优雅的编程范式,如果使用得当,可以写出相当简化和精简的代码。一个附加的好处是更简单的代码更不容易遇到这种“不小心在遍历列表时删掉列表元素”的bug。例如列表推导式list comprehensions)就提供了这样的范式。再者,列表推导式在避免这样的问题上特别有用,接下来这个对上面的代码的重新实现就相当完美:


1

2

3

4

5

>>> odd = lambda x : bool(x % 2)

>>> numbers = [n for n in range(10)]

>>> numbers[:] = [n for n in numbers if not odd(n)]  # 啊,这多优美

>>> numbers

[0, 2, 4, 6, 8]

常见错误6:搞不清楚在闭包(closures)中Python是怎样绑定变量的

看这个例子:


1

2

3

4

5

>>> def create_multipliers():

...     return [lambda x : i * x for i in range(5)]

>>> for multiplier in create_multipliers():

...     print multiplier(2)

...

期望得到下面的输出:


1

2

3

4

5

0

2

4

6

8

但是实际上得到的是:


1

2

3

4

5

8

8

8

8

8

意外吧!

这是由于Python的后期绑定(late binding)机制导致的,这是指在闭包中使用的变量的值,是在内层函数被调用的时候查找的。因此在上面的代码中,当任一返回函数被调用的时候,i的值是在它被调用时的周围作用域中查找(到那时,循环已经结束了,所以i已经被赋予了它最终的值4)。

解决的办法比较巧妙:


1

2

3

4

5

6

7

8

9

10

11

>>> def create_multipliers():

...     return [lambda x, i=i : i * x for i in range(5)]

...

>>> for multiplier in create_multipliers():

...     print multiplier(2)

...

0

2

4

6

8

这下对了!这里利用了默认参数去产生匿名函数以达到期望的效果。有人会说这很优美,有人会说这很微妙,也有人会觉得反感。但是如果你是一名Python程序员,重要的是能理解任何的情况。

常见错误7:循环加载模块

假设你有两个文件,a.py和b.py,在这两个文件中互相加载对方,例如:

在a.py中:


1

2

3

4

import b

def f():

    return b.x

print f()

在b.py中:


1

2

3

4

import a

x = 1

def g():

    print a.f()

首先,我们试着加载a.py:


1

2

>>> import a

1

没有问题。也许让人吃惊,毕竟有个感觉应该是问题的循环加载在这儿。

事实上在Python中仅仅是表面上的出现循环加载并不是什么问题。如果一个模块以及被加载了,Python不会傻到再去重新加载一遍。但是,当每个模块都想要互相访问定义在对方里的函数或者变量时,问题就来了。

让我们再回到之前的例子,当我们加载a.py时,它再加载b.py不会有问题,因为在加载b.py,它并不需要访问a.py的任何东西,而在b.py中唯一的引用就是调用a.f()。但是这个调用是在函数g()中完成的,并且a.py或者b.py中没有人调用g(),所以这会儿心情还是美丽的。

但是当我们试图加载b.py时(之前没有加载a.py),会发生什么呢:


1

2

3

4

5

6

7

8

9

10

>>> import b

Traceback (most recent call last):

      File "<stdin>", line 1, in <module>

      File "b.py", line 1, in <module>

    import a

      File "a.py", line 6, in <module>

    print f()

      File "a.py", line 4, in f

    return b.x

AttributeError: ‘module‘ object has no attribute ‘x‘

恭喜你,出错了。这里问题出在加载b.py的过程中,Python试图加载a.py,并且在a.py中需要调用到f(),而函数f()又要访问到b.x,但是这个时候b.x却还没有被定义。这就产生了AttributeError异常。

解决的方案可以做一点细微的改动。改一下b.py,使得它在g()里面加载a.py:


1

2

3

4

x = 1

def g():

    import a    # 只有当g()被调用的时候才加载

    print a.f()

这会儿当我们加载b.py的时候,一切安好:


1

2

3

4

>>> import b

>>> b.g()

1   # 第一次输出,因为模块a在最后调用了‘print f()’

1   # 第二次输出,这是我们调用g()

常见错误8:与Python标准库模块命名冲突

Python的一个优秀的地方在于它提供了丰富的库模块。但是这样的结果是,如果你不下意识的避免,很容易你会遇到你自己的模块的名字与某个随Python附带的标准库的名字冲突的情况(比如,你的代码中可能有一个叫做email.py的模块,它就会与标准库中同名的模块冲突)。

这会导致一些很粗糙的问题,例如当你想加载某个库,这个库需要加载Python标准库里的某个模块,结果呢,因为你有一个与标准库里的模块同名的模块,这个包错误的将你的模块加载了进去,而不是加载Python标准库里的那个模块。这样一来就会有麻烦了。

所以在给模块起名字的时候要小心了,得避免与Python标准库中的模块重名。相比起你提交一个“Python改进建议(Python Enhancement Proposal (PEP))”去向上要求改一个标准库里包的名字,并得到批准来说,你把自己的那个模块重新改个名字要简单得多。

常见错误9:不能区分Python 2和Python 3

看下面这个文件foo.py:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import sys

def bar(i):

    if i == 1:

        raise KeyError(1)

    if i == 2:

        raise ValueError(2)

def bad():

    e = None

    try:

        bar(int(sys.argv[1]))

    except KeyError as e:

        print(‘key error‘)

    except ValueError as e:

        print(‘value error‘)

    print(e)

bad()

在Python 2里,运行起来没有问题:


1

2

3

4

5

6

$ python foo.py 1

key error

1

$ python foo.py 2

value error

2

但是如果拿到Python 3上面玩玩:


1

2

3

4

5

6

7

8

$ python3 foo.py 1

key error

Traceback (most recent call last):

  File "foo.py", line 19, in <module>

    bad()

  File "foo.py", line 17, in bad

    print(e)

UnboundLocalError: local variable ‘e‘ referenced before assignment

这是怎么回事?“问题”在于,在Python 3里,在except块的作用域以外,异常对象(exception object)是不能被访问的。(原因在于,如果不这样的话,Python会在内存的堆栈里保持一个引用链直到Python的垃圾处理将这些引用从内存中清除掉。更多的技术细节可以参考这里。)

避免这样的问题可以这样做:保持在execpt块作用域以外对异常对象的引用,这样是可以访问的。下面是用这个办法对之前的例子做的改动,这样在Python 2和Python 3里面都运行都没有问题。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import sys

def bar(i):

    if i == 1:

        raise KeyError(1)

    if i == 2:

        raise ValueError(2)

def good():

    exception = None

    try:

        bar(int(sys.argv[1]))

    except KeyError as e:

        exception = e

        print(‘key error‘)

    except ValueError as e:

        exception = e

        print(‘value error‘)

    print(exception)

good()

在Py3k里面运行:


1

2

3

4

5

6

$ python3 foo.py 1

key error

1

$ python3 foo.py 2

value error

2

耶!

(顺带提一下,我们的“Python招聘指南”里讨论了从Python 2移植代码到Python 3时需要注意的其他重要的不同之处。)

常见错误10:错误的使用__del__方法

假设有一个文件mod.py中这样使用:


1

2

3

4

5

6

import foo

class Bar(object):

        ...

    def __del__(self):

        foo.cleanup(self.myhandle)

然后试图在another_mod.py里这样:


1

2

import mod

mybar = mod.Bar()

那么你会得到一个恶心的AttributeError异常。

为啥呢?这是因为(参考这里),当解释器关闭时,模块所有的全局变量会被置为空(None)。结果便如上例所示,当__del__被调用时,名字foo已经被置为空了。

使用atexit.register()可以解决这个问题。如此,当你的程序结束的时候(退出的时候),你的注册的处理程序会在解释器关闭之前处理。

这样理解的话,对上面的mod.py可以做如下的修改:


1

2

3

4

5

6

7

8

9

10

import foo

import atexit

def cleanup(handle):

    foo.cleanup(handle)

class Bar(object):

    def __init__(self):

        ...

        atexit.register(cleanup, self.myhandle)

这样的实现方式为在程序正常终止时调用清除功能提供了一种干净可靠的办法。显然,需要foo.cleanup决定怎么处理绑定在self.myhandle上的对象,但你知道怎么做的。

总结

Python 是一门非常强大且灵活的语言,它众多的机制和范式能显著的提高生产效率。不过,和任何一款软件或者语言一样,对它的理解或认识不足的话,常常是弊大于利的,并会处于一种“一知半解”的状态。

多熟悉Python的一些关键的细微的地方,比如(但不局限于)本文中提到的这些问题,可以帮你更好的使用这门语言的同时帮你避免一些常见的陷阱。

感兴趣的话可以读一读这篇“Python面试指南(Insider’s Guide to Python Interviewing”,了解一些能够区分Python程序员的面试题目。

希望您能在本文学到有用的地方,并欢迎您的反馈。

关于作者: datorhjaelten

时间: 2024-10-05 04:01:55

Python程序员的10个常见错误(转)的相关文章

Python程序员的10个常见错误

关于Python Python是一门解释性的,面向对象的,并具有动态语义的高级编程语言.它高级的内置数据结构,结合其动态类型和动态绑定的特性,使得它在快速应用程序开发(Rapid Application Development)中颇为受欢迎,同时Python还能作为脚本语言或者胶水语言讲现成的组件或者服务结合起来.Python支持模块(modules)和包(packages),所以也鼓励程序的模块化以及代码重用. 关于本文 Python简单.易学的语法可能会误导一些Python程序员(特别是那些

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

不管是在学习还是工作过程中,人都会犯错.虽然Python的语法简单.灵活,但也一样存在一些不小的坑,一不小心,初学者和资深Python程序员都有可能会栽跟头.本文是Toptal网站的程序员梳理的10大常见错误,非常有参考意义.大家在开发过程中需要格外注意.译文中如有理解错误的地方,可以在网站留言或通过微信公众号编程派回复. 常见错误1:错误地将表达式作为函数的默认参数 在Python中,我们可以为函数的某个参数设置默认值,使该参数成为可选参数.虽然这是一个很好的语言特性,但是当默认值是可变类型时

Python 程序员经常犯的 10 个错误

关于PythonPython是一种解释性.面向对象并具有动态语义的高级程序语言.它内建了高级的数据结构,结合了动态类型和动态绑定的优点,这使得... 关于Python Python是一种解释性.面向对象并具有动态语义的高级程序语言.它内建了高级的数据结构,结合了动态类型和动态绑定的优点,这使得它在快速应用开发中非常有吸引力,并且可作为脚本或胶水语言来连接现有的组件或服务.Python支持模块和包,从而鼓励了程序的模块化和代码重用. 关于这篇文章 Python简单易学的语法可能会使Python开发

月薪3万的python程序员都看了这本书

想必大家都看过吧 Python编程从入门到实践 全书共有20章,书中的简介如下: 本书旨在让你尽快学会 Python ,以便能够编写能正确运行的程序 —— 游戏.数据可视化和 Web 应用程序,同时掌握让你终身受益的基本编程知识.本书适合任何年龄的读者阅读,它不要求你有任何 Python 编程经验,甚至不要求你有编程经验.如果你想快速掌握基本的编程知识以便专注于开发感兴趣的项目,并想通过解决有意义的问题来检查你对新学概念的理解程度,那么本书就是为你编写的.本书还可供初中和高中教师用来通过开发项目

介绍Python程序员常用的IDE和其它开发工具

概述 “工欲善其事,必先利其器”,如果说编程是程序员的手艺,那么IDE就是程序员的吃饭家伙了. IDE 的全称是Integration Development Environment(集成开发环境),一般以代码编辑器为核心,包括一系列周边组件和附属功能.一个优秀的IDE,最重要的就是在普通文本编辑之外, 提供针对特定语言的各种快捷编辑功能,让程序员尽可能快捷.舒适.清晰的浏览.输入.修改代码.对于一个现代的IDE来说,语法着色.错误提示.代码折 叠.代码完成.代码块定位.重构,与调试器.版本控制

Python 程序员的 Golang 学习指南(III): 入门篇

基础语法 类型和关键字 类型 // 基础类型 布尔类型: bool 整型: int8,uint8,int16,uint16,int32,uint32,int64,uint64,int,rune,byte,complex128, complex64,其中,byte 是 int8 的别名 浮点类型: float32 . float64 复数类型: complex64 . complex128 字符串: string 字符类型: rune(int32的别名) 错误类型: error // 复合类型 指

[转] SQL Server 致程序员(容易忽略的错误)

转自:SQL Server 致程序员(容易忽略的错误) 概述 因为每天需要审核程序员发布的SQL语句,所以收集了一些程序员的一些常见问题,还有一些平时收集的其它一些问题,这也是很多人容易忽视的问题,在以后收集到的问题会补充在文章末尾,欢迎关注,由于收集的问题很多是针对于生产数据,测试且数据量比较大,这里就不把数据共享出来了,大家理解意思就行. 步骤 大小写 大写T-SQL 语言的所有关键字都使用大写,规范要求. 使用“;” 使用“;”作为 Transact-SQL 语句终止符.虽然分号不是必需的

Python程序员面试,这些问题你必须提前准备!

近些年随着Python语言越来越流行,越来越多的人选择Python语言作为自己的职业方向.如何在心仪公司的面试中获得好成绩,并最终成功获得offer是每一个Python开发者都要慎重对待的事情,如下是笔者整理的Python程序员在面试中经常被问到的问题,供您参考,未尽题目,欢迎留言补充. 这些面试问题大致可以分为四类:什么(what)?如何做(how)?说区别/谈优势(difference)以及实践操作(practice). What? 1. 什么是Python? 2. 什么是Python自省?

所有 Python 程序员必须要学会的「日志」记录。

本文字数:3840 字 阅读本文大概需要:10 分钟 写在之前 在我们的现实生活中,「日志记录」其实是一件非常重要的事情,比如银行的转账记录,汽车的行车记录仪记录行驶过程中的一切,如果出现了什么问题,我们可以通过「日志记录」来搞清楚到底发生了什么事情. 除了在生活中,在日常的系统开发以及调试等过程中,记录日志同样是一件很重要的事情.很多编程初学者并没有「记录日志」的习惯,认为记录日志是一件可有可无的事情,出现问题的时候只要使用 print 函数打印一下程序的中间结果即可,真是 too young