摘要 :Python 是一个简洁优雅的高级编程语言,它容易上手的同时,也隐藏了一些难以理解和甚至反人类直觉的坑。本文列出一些我们线上代码实际遇到过的一些编码问题。

一、不要混用 Tab 和空格

py-tab-space.png

如上图中的代码,return n那行代码的是Tab缩进,而其他行是4 个空格,当编辑器设置Tab显示宽带为 4 个空格宽时,就会出现逻辑和直觉相悖的情况。

解决办法:

  1. 要求团队成员遵循 PEP 8 代码规范 ,统一采用 4 个空格缩进代码
  2. 项目根目录配置 .editorconfig ,Python 文件采用 4 个空格缩进
  3. 配置 IDE 编辑器,显示特殊控制字符

二、不要使用可变默认参数

class Blog(object):
    def __init__(self, blog_id, tags=[]):
        self.blog_id = blog_id
        self.tags = tags

blog1 = Blog(1)
blog2 = Blog(2)

blog1.tags.append('Python')
blog2.tags.append('Java')

print(blog1.tags, blog2.tags)  # Output: ['Python', 'Java'], ['Python', 'Java']

Python 函数的默认参数在定义时确定,如果调用函数时不显示传递默认参数,那么每次调用都相同对象。如果该对象时可变类型(比如list),则容易出现上面的问题。

解决办法:

不要使用可变变量做默认参数,可以使用None,然后代码里面判断如果是None就初始化一个新对象。

class Blog(object):
    def __init__(self, blog_id, tags=None):
        self.blog_id = blog_id
        self.tags = [] if tags is None else tags

三、注意 is 和 == 的区别

>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a, b = 257, 257
>>> a is b
True

is== 的区别:

  • is 运算符检查两个运算对象是否引用自同一对象(即,它检查两个对象的 id 是否相同)
  • == 运算符比较两个运算对象的值是否相等

256是一个已经存在的对象,而257不是。为什么?

实际编程过程中,-5 到 256 之间的数字经常会用到,Python 为了避免频繁创建和销毁内存,对 -5 到 256 之间的所有整数会预先分配一个全局整数对象数组, 当需要创建一个该范围内的整数时,Python 会直接返回现有对象的引用。其他编程语言(比如 Java)也有类似的缓存机制。

对于字面量字符串也有类似缓存优化,另外也可以手动使用 intern 将字符串缓存

四、生成器执行时间的差异

array = [1, 8, 15]
g = (x for x in array if array.count(x) > 0)
array = [2, 8, 22]

输出:

>>> print(list(g))
[8]

原因:

  • (x for x in array) 返回的是一个 生成器 (而 [x for x in array] 返回的是一个 list
  • 在一个生成器表达式里,in 的操作是在声明时求值的,而 if 是在运行期求值的
  • 所以 x 枚举的是声明时的列表 [1, 8, 15],而 if 判断的是被重新赋值列表 [2, 8, 22]
  • 对于 [1, 8, 15] 中的每个数,只有 8 在新的 array 中个数大于 0,因此生成器只生成 8

五、for 循环中修改循环变量

如下代码,在for循环体修改循环变量i的值:

for i in range(4):
    print(i)
    i = 10

输出:

0
1
2
3

原因: Python 的for循环机制是每次迭代到下一项的时候都会解包并分配一次;即range(4)生成的四个值在每次迭代的时候都会解包一次并赋值给i;所以赋值操作i = 10对迭代没有影响。

六、一个 += 的谜题

>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

到底会发生下面 4 种情况中的哪一种?

  1. t 变成 (1, 2, [30, 40, 50, 60])
  2. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
  3. 以上两个都不是。
  4. 1 和 2 都是对的。

你可能会选 2,但答案是 4:

>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

s[a] = b背后的字节码:

>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0(s)
3 LOAD_NAME 1(a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR 
8 LOAD_NAME 2(b)
11 INPLACE_ADD 
12 ROT_THREE
13 STORE_SUBSCR 
14 LOAD_CONST 0(None)
17 RETURN_VALUE
  • ➊ 将 s [a] 的值存入 TOS(Top Of Stack,栈的顶端)。
  • ➋ 计算 TOS += b,这一步能够完成,是因为 TOS 指向的是一个可变对象。
  • s [a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组。

PS,如果将t[2] += [50, 60]改成t[2] = t[2] + [50, 60]又该选哪个答案呢?

扩展资料