本文关注的主要是python陷阱,具体而言,是指CPython,而且除非特别说明,所有代码示例都是在python2.7运行的。

  本文会持续更新!

  本文地址:http://www.cnblogs.com/xybaby/p/7183854.html

  我个人对陷阱的定义是这样的:代码看起来可以工作,但不是以你“想当然“”的方式。如果一段代码直接出错,抛出了异常,我不认为这是陷阱。比如,Python程序员应该都遇到过的“UnboundLocalError", 示例:

  >>> a=1

  >>> def func():

  ...     a+=1

  ...     print a

  ...

  >>> func()

  Traceback (most recent call last):

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

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

  UnboundLocalError: local variable 'a' referenced before assignment

   

  对于“UnboundLocalError",还有更高级的版本:

import random

def func(ok):
if ok:
a = random.random()
else:
import random
a = random.randint(1, 10)
return a func(True)# UnboundLocalError: local variable 'random' referenced before assignment

  可能对于很多python新手来说,这个Error让人摸不着头脑。但我认为这不算陷阱,因为这段代码一定会报错,而不是默默的以错误的方式运行。不怕真小人,就怕伪君子。我认为缺陷就好比伪君子。

  那么Python中哪些真正算得上陷阱呢

  第一:以mutable对象作为默认参数

  这个估计是最广为人知的了,Python和其他很多语言一样,提供了默认参数,默认参数确实是个好东西,可以让函数调用者忽略一些细节(比如GUI编程,Tkinter,QT),对于lambda表达式也非常有用。但是如果使用了可变对象作为默认参数,那么事情就不那么愉快了

  >>> def f(lst = []):
  ...     lst.append(1)
  ...     return lst
  ...
  >>> f()
  [1]
  >>> f()
  [1, 1]

  

  惊喜不惊喜?!究其原因,python中一切都是对象,函数也不列外,默认参数只是函数的一个属性。而默认参数在函数定义的时候已经求值了。

  Default parameter values are evaluated when the function definition is executed.

  stackoverflow上有一个更适当的例子来说明默认参数是在定义的时候求值,而不是调用的时候。

  >>> import time

  >>> def report(when=time.time()):

  ...     return when

  ...

  >>> report()

  1500113234.487932

  >>> report()

  1500113234.487932

  python docoment 给出了标准的解决办法:

  A way around this is to use None as the default, and explicitly test for it in the body of the function

  

  >>> def report(when=None):

  ...     if when is None:

  ...             when = time.time()

  ...     return when

  ...

  >>> report()

  1500113446.746997

  >>> report()

  1500113448.552873

  第二: x += y vs x = x + y

  一般来说,二者是等价的,至少看起来是等价的(这也是陷阱的定义 -- 看起来都OK,但不一定正确)。

  

  >>> x=1;x += 1;print x

  2

  >>> x=1;x = x+1;print x

  2

  >>> x=[1];x+=[2];print x

  [1, 2]

  >>> x=[1];x=x+[2];print x

  [1, 2]

  呃,被光速打脸了?

  >>> x=[1];print id(x);x=x+[2];print id(x)

  4357132800

  4357132728

  >>> x=[1];print id(x);x+=[2];print id(x)

  4357132800

  4357132800

  

  前者x指向一个新的对象,后者x在原来的对象是修改,当然,那种效果是正确的取决于应用场景。至少,得知道,二者有时候并不一样

  第三,神奇的小括号--()

  小括号(parenthese)在各种编程语言中都有广泛的应用,python中,小括号还能表示元组(tuple)这一数据类型, 元组是immutable的序列。

  >>> a = (1, 2)

  >>> type(a)

  <type 'tuple'>

  >>> type(())

  <type 'tuple'>

  但如果只有一个元素呢

  >>> a=(1)

  >>> type(a)

  <type 'int'>

  

  神奇不神奇,如果要表示只有一个元素的元组,正确的姿势是:

  >>> a=(1,)

  >>> type(a)

  <type 'tuple'>

  第四:生成一个元素是列表的列表

  这个有点像二维数组,当然生成一个元素是字典的列表也是可以的,更通俗的说,生成一个元素是可变对象的序列

  很简单嘛:

  >>> a= [[]] * 10

  >>> a

  [[], [], [], [], [], [], [], [], [], []]

  >>> a[0].append(10)

  >>> a[0]

  [10]

  看起来很不错,简单明了,but

  >>> a[1]

  [10]

  >>> a

  [[10], [10], [10], [10], [10], [10], [10], [10], [10], [10]]

  我猜,这应该不是你预期的结果吧,究其原因,还是因为python中list是可变对象,上述的写法大家都指向的同一个可变对象,正确的姿势

  >>> a = [[] for _ in xrange(10)]

  >>> a[0].append(10)

  >>> a

  [[10], [], [], [], [], [], [], [], [], []]

  另外一个在实际编码中遇到的问题,dict.fromkeys, 也有异曲同工之妙: 创建的dict的所有values指向同一个对象。

  fromkeys(seq[, value])

Create a new dictionary with keys from seq and values set to value. 

  第五,在访问列表的时候,修改列表

  列表(list)在python中使用非常广泛,当然经常会在访问列表的时候增加或者删除一些元素。比如,下面这个函数,试图删掉列表中为3的倍数的元素:

  

  >>> def modify_lst(lst):

  ...     for idx, elem in enumerate(lst):

  ...             if elem % 3 == 0:

  ...                     del lst[idx]

  ...

  测试一下,

  >>> lst = [1,2,3,4,5,6]

  >>> modify_lst(lst)

  >>> lst

  [1, 2, 4, 5]

  好像没什么错,不过这只是运气好

  >>> lst = [1,2,3,6,5,4]

  >>> modify_lst(lst)

  >>> lst

  [1, 2, 6, 5, 4]

  上面的例子中,6这个元素就没有被删除。如果在modify_lst函数中print idx, item就可以发现端倪:lst在变短,但idx是递增的,所以在上面出错的例子中,当3被删除之后,6变成了lst的第2个元素(从0开始)。在C++中,如果遍历容器的时候用迭代器删除元素,也会有同样的问题。

  如果逻辑比较简单,使用list comprehension是不错的注意

  第六,闭包与lambda

  这个也是老生长谈的例子,在其他语言也有类似的情况。先看一个例子:

  >>> def create_multipliers():

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

  ...

  >>> for multiplier in create_multipliers():

  ...     print multiplier(2)

  ...

  create_multipliers函数的返回值时一个列表,列表的每一个元素都是一个函数 -- 将输入参数x乘以一个倍数i的函数。预期的结果时0,2,4,6,8. 但结果是5个8,意外不意外。

  由于出现这个陷阱的时候经常使用了lambda,所以可能会认为是lambda的问题,但lambda表示不愿意背这个锅。问题的本质在与python中的属性查找规则,LEGB(local,enclousing,global,bulitin),在上面的例子中,i就是在闭包作用域(enclousing),而Python的闭包是 迟绑定 , 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

  解决办法也很简单,那就是变闭包作用域为局部作用域。

  

  >>> def create_multipliers():

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

  ...

  第七,定义__del__

  大多数计算机专业的同学可能都是先学的C、C++,构造、析构函数的概念应该都非常熟。于是,当切换到python的时候,自然也想知道有没有相应的函数。比如,在C++中非常有名的RAII,即通过构造、析构来管理资源(如内存、文件描述符)的声明周期。那在python中要达到同样的效果怎么做呢,即需要找到一个对象在销毁的时候一定会调用的函数,于是发现了__init__, __del__函数,可能简单写了两个例子发现确实也能工作。但事实上可能掉进了一个陷阱,在python documnet是有描述的:

  Circular references which are garbage are detected when the option cycle detector is enabled (it’s on by default), but can only be cleaned up if there are no Python-level __del__() methods involved.

  简单来说,如果在循环引用中的对象定义了__del__,那么python gc不能进行回收,因此,存在内存泄漏的风险

  

  第八,不同的姿势import同一个module

  示例在stackoverflow的例子上稍作修改,假设现在有一个package叫mypackage,里面包含三个python文件:mymodule.py, main.py, __init__.py。mymodule.py代码如下:

 l = []
class A(object):
pass

  main.py代码如下:

 def add(x):
from mypackage import mymodule
mymodule.l.append(x)
print "updated list",mymodule.l, id(mymodule) def get():
import mymodule
print 'module in get', id(mymodule)
return mymodule.l if __name__ == '__main__':
import sys
sys.path.append('../')
add(1) ret = get()
print "lets check", ret

  运行python main.py,结果如下:  

  updated list [1] 4406700752
  module in get 4406700920
  lets check []

  从运行结果可以看到,在add 和 get函数中import的mymodule不是同一个module,ID不同。当然,在python2.7.10中,需要main.py的第13行才能出现这样的效果。你可能会问,谁会写出第13行这样的代码呢?事实上,在很多项目中,为了import的时候方便,会往sys.path加入一堆路径。那么在项目中,大家同意一种import方式就非常有必要了

  第九,python升级

  python3.x并不向后兼容,所以如果从2.x升级到3.x的时候得小心了,下面列举两点:

  在python2.7中,range的返回值是一个列表;而在python3.x中,返回的是一个range对象。

  map()、filter()、 dict.items()在python2.7返回列表,而在3.x中返回迭代器。当然迭代器大多数都是比较好的选择,更加pythonic,但是也有缺点,就是只能遍历一次。在instagram的分享中,也提到因为这个导致的一个坑爹的bug。

  第十:++i --i

    这个陷阱主要是坑来自C、C++背景的同学。简单来说,++i是对i取两次正号,--i是对i取两次负号,运算完之后i的值不变。

  第十一: __setattr__ __getattr__ __getattribute__

  Python中有大量的magic method(形似__xx__),其中许多跟属性访问有关,比如__get__, __set__,__delete_,__getattr__, __setattr__, __delattr__, __getattribute__。前三个跟descriptor相关,详细可参见《python descriptor 详解》。坑爹的是,__getattr__与__setattr__相差很大,在《python属性查找(attribute look up)》一文中有详细介绍。简单说来,__setattr__与__getattribute__是对应的,都是修改python默认的属性修改、查找机制,而__getattr__只是默认查找机制无法找到属性的时候才会调用,__setattr__应该叫__setattribute__才恰当!

  第负一,gil

  以GIL结尾,因为gil是Python中大家公认的缺陷

  从其他语言过来的同学可能看到python用threading模块,拿过来就用,结果发现效果不对啊,然后就会喷,什么鬼

  总结:

  毫无疑问的说,python是非常容易上手,也非常强大的一门语言。python非常灵活,可定制化很强。同时,也存在一些陷阱,搞清楚这些陷阱能够更好的掌握、使用这么语言。本文列举了一些python中的一些缺陷,这是一份不完全列表,欢迎大家补充。

references:

instagram-pycon-2017

python-2-x-gotchas-and-landmines

module-reimported-if-imported-from-different-path




程序员必知的Python陷阱与缺陷列表的更多相关文章

  1. c++程序员必知的几个库

    c++程序员必知的几个库 1.C++各大有名库的介绍——C++标准库 2.C++各大有名库的介绍——准标准库Boost 3.C++各大有名库的介绍——GUI 4.C++各大有名库的介绍——网络通信 5 ...

  2. [置顶] 程序员必知(三):一分钟知道URI编码(encodeURI)

    因为浏览器会用一些特殊的字符作为特定的意义,所以在要传输的内容上如果有这些特殊的字符的话,就需要对其进行转义才能正确传输,如以下字符为发送时候的关键字,即特殊字符 ;/?:@&=+$,# 所以 ...

  3. Android程序员必知必会的网络通信传输层协议——UDP和TCP

    1.点评 互联网发展至今已经高度发达,而对于互联网应用(尤其即时通讯技术这一块)的开发者来说,网络编程是基础中的基础,只有更好地理解相关基础知识,对于应用层的开发才能做到游刃有余. 对于Android ...

  4. 迈向高阶:优秀Android程序员必知必会的网络基础

    1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...

  5. 程序员必知的8大排序(四)-------归并排序,基数排序(java实现)

    程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现) 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现) 程序员必知的8大排序(三)-------冒 ...

  6. 程序员必知的8大排序(三)-------冒泡排序,快速排序(java实现)

    程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现) 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现) 程序员必知的8大排序(三)-------冒 ...

  7. 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现)

    程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现) 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现) 程序员必知的8大排序(三)-------冒 ...

  8. 程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现)

    http://blog.csdn.net/pzhtpf/article/details/7559896 程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现) 程序员必知的8大 ...

  9. 程序员必知的LinuxShell命令

    程序员必知的LinuxShell命令 grep (Globle Regular Expression Print全局正则表达式) 命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹 配的 ...

随机推荐

  1. 解决shiro和quartz2 版本冲突问题

    修改build.gradle   compile ("org.quartz-scheduler:quartz:2.2.3") compile ("org.apache.s ...

  2. 分享一款在线less转css的神器

          大多数web开发的程序员都了解和使用过Less, LESS是一门 CSS 预处理语言,它扩充了 CSS 语言,增加了诸如变量.混合(mixin).函数等功能,让 CSS 更易维护.方便制作 ...

  3. MPP 二、Greenplum数据加载

    Loading external data into greenplum database table using different ways... Greenplum 有常规的COPY加载方法,有 ...

  4. hdu5950

    hdu5950 题意 \(给出 f_1 , f_2 ,以及递推式 f_n = 2 * f_{n-2} + f_{n-1} + n^4 ,求 f_n (mod=2147493647)\) 推导一下. \ ...

  5. Java对【JSON数据的解析】--fastjson解析法

    要求:解析下面JSON数据 String string = "{no:1,name:'Android',employees:[{name:'zhangsan',age:20},{name:' ...

  6. C++ #if #endif #define #ifdef #ifndef #if defined #if !defined详解 (转)

    (源)http://blog.csdn.net/sky1203850702/article/details/42024673 首先,让我们先从头文件开始,在很多头文件里,我们会看到这样的语句 #ifn ...

  7. vscode中使用markdown

    vscode中使用markdown vscode 是微软推出一款轻量级的文本编辑工具,类似于sublime,由于其拥有丰富的插件,安装使用也非常简单,所以深受广大程序员的喜爱. markdown 是一 ...

  8. [python标准库]XML模块

    1.什么是XML XML是可扩展标记语言(Extensible Markup Language)的缩写,其中的 标记(markup)是关键部分.您可以创建内容,然后使用限定标记标记它,从而使每个单词. ...

  9. EntityFramework Core迁移时出现数据库已存在对象问题解决方案

    前言 刚开始接触EF Core时本着探索的精神去搞,搞着搞着发现出问题了,后来就一直没解决,觉得很是不爽,借着周末好好看看这块内容. EntityFramework Core迁移出现对象在数据库中已存 ...

  10. Akka(10): 分布式运算:集群-Cluster

    Akka-Cluster可以在一部物理机或一组网络连接的服务器上搭建部署.用Akka开发同一版本的分布式程序可以在任何硬件环境中运行,这样我们就可以确定以Akka分布式程序作为标准的编程方式了. 在上 ...