欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Python:What the f*ck Python(下)

程序员文章站 2022-07-02 17:28:15
GitHub 上有一个名为《What the f ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址:https://github.com/satwikkansal/wtfpytho ......

github 上有一个名为《what the f*ck python!》的项目,这个有趣的项目意在收集 python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理!
原版地址:
最近,一位名为“暮晨”的贡献者将其翻译成了中文。
中文版地址:

上一篇 python:what the f*ck python(上)

原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些 python 中的彩蛋被我去掉了。

26. 非英文字符

>>> value = 11
>>> valuе = 32
>>> value
11

什么鬼?
将代码复制到 pycharm 里看一下就明白了。
Python:What the f*ck Python(下)

有些一些非西方字符虽然看起来和英语字母相同,但会被解释器识别为不同的字母。我们基本不会用到。

27. 空间移动

import numpy as np

def energy_send(x):
    # 初始化一个 numpy 数组
    np.array([float(x)])

def energy_receive():
    # 返回一个空的 numpy 数组
    return np.empty((), dtype=np.float).tolist()

output:

>>> energy_send(123.456)
>>> energy_receive()
123.456

说明:
energy_send 函数中创建的 numpy 数组并没有返回,因此内存空间被释放并可以被重新分配。
numpy.empty() 直接返回下一段空闲内存,而不重新初始化。而这个内存点恰好就是刚刚释放的那个(通常情况下,并不绝对)。

28. 不要混用制表符(tab)和空格(space)

tab 是8个空格,而用空格表示则一个缩进是4个空格,混用就会出错。python3 里直接不允许这种行为了,会报错:

taberror: inconsistent use of tabs and spaces in indentation

很多编辑器,例如 pycharm,可以直接设置 tab 表示 4 个空格。
Python:What the f*ck Python(下)

29. 迭代字典时的修改

x = {0: none}

for i in x:
    del x[i]
    x[i+1] = none
    print(i)

output(python 2.7- python 3.5):

0
1
2
3
4
5
6
7

说明:
python 不支持 对字典进行迭代的同时修改它,它之所以运行 8 次,是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是8, 扩容会导致散列表地址发生变化而中断循环)。
在不同的python实现中删除键的处理方式以及调整大小的时间可能会有所不同,python3.6开始,到5就会扩容。

而在 list 中,这种情况是允许的,list 和 dict 的实现方式是不一样的,list 虽然也有扩容,但 list 的扩容是整体搬迁,并且顺序不变。

list = [1]
j = 0
for i in list:
    print(i)
    list.append(i + 1)

这个代码可以一直运行下去直到 int 越界。但一般不建议在迭代的同时修改 list。

30. __del__

class someclass:
    def __del__(self):
        print("deleted!")

output:

>>> x = someclass()
>>> y = x
>>> del x  # 这里应该会输出 "deleted!"
>>> del y
deleted!

说明:
del x 并不会立刻调用x.__del__(),每当遇到del x,python 会将 x 的引用数减 1,当 x 的引用数减到 0 时就会调用x.__del__()

我们再加一点变化:

>>> x = someclass()
>>> y = x
>>> del x
>>> y  # 检查一下y是否存在
<__main__.someclass instance at 0x7f98a1a67fc8>
>>> del y # 像之前一样,这里应该会输出 "deleted!"
>>> globals() # 好吧, 并没有。让我们看一下所有的全局变量
deleted!
{'__builtins__': <module '__builtin__' (built-in)>, 'someclass': <class __main__.someclass at 0x7f98a1a5f668>, '__package__': none, '__name__': '__main__', '__doc__': none}

y.__del__()之所以未被调用,是因为前一条语句(>>> y)对同一对象创建了另一个引用,从而防止在执行del y后对象的引用数变为 0。(这其实是 python 交互解释器的特性,它会自动让_保存上一个表达式输出的值。)
调用globals()导致引用被销毁,因此我们可以看到 "deleted!" 终于被输出了。

31. 迭代列表时删除元素

在 29 中,我附加了一个迭代列表时添加元素的例子,现在来看看迭代列表时删除元素。

list_1 = [1, 2, 3, 4]
list_2 = [1, 2, 3, 4]
list_3 = [1, 2, 3, 4]
list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1):
    del item

for idx, item in enumerate(list_2):
    list_2.remove(item)

for idx, item in enumerate(list_3[:]):
    list_3.remove(item)

for idx, item in enumerate(list_4):
    list_4.pop(idx)

output:

>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]

说明:
在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本,list_3[:]就是这么做的。

del、remove、pop 的不同:

  • del var_name 只是从本地或全局命名空间中删除了 var_name(这就是为什么 list_1 没有受到影响)。
  • remove 会删除第一个匹配到的指定值,而不是特定的索引,如果找不到值则抛出 valueerror 异常。
  • pop 则会删除指定索引处的元素并返回它,如果指定了无效的索引则抛出 indexerror 异常。

为什么输出是 [2, 4]?
列表迭代是按索引进行的,所以当我们从list_2list_4中删除 1 时,列表的内容就变成了[2, 3, 4]。剩余元素会依次位移,也就是说,2的索引会变为 0,3会变为 1。由于下一次迭代将获取索引为 1 的元素(即3), 因此2将被彻底的跳过。类似的情况会交替发生在列表中的每个元素上。

32. 循环变量泄漏!

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

output:

6 : for x inside loop
6 : x in global

# 这次我们先初始化x
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

output:

6 : for x inside loop
6 : x in global

x = 1
print([x for x in range(5)])
print(x, ': x in global')

output:

[0, 1, 2, 3, 4]
(4, ': x in global')

output:

[0, 1, 2, 3, 4]
1 : x in global

说明:
在 python 中,for 循环使用所在作用域并在结束后保留定义的循环变量。如果我们曾在全局命名空间中定义过循环变量,它会重新绑定现有变量。
python 2.x 和 python 3.x 解释器在列表推导式示例中的输出差异,在文档 what’s new in python 3.0 中可以找到相关的解释:

"列表推导不再支持句法形式[... for var in item1, item2, ...]。使用[... for var in (item1, item2, ...)]代替。另外注意,列表推导具有不同的语义:它们更接近于list()构造函数中生成器表达式的语法糖,特别是循环控制变量不再泄漏到周围的作用域中。"

简单来说,就是 python2 中,列表推导式依然存在循环控制变量泄露,而 python3 中不存在。

33. 当心默认的可变参数!

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

output:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

说明:
python 中函数的默认可变参数并不是每次调用该函数时都会被初始化。相反,它们会使用最近分配的值作为默认值。当我们明确的将[]作为参数传递给some_func的时候,就不会使用default_arg的默认值, 所以函数会返回我们所期望的结果。

>>> some_func.__defaults__ # 这里会显示函数的默认参数的值
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)

避免可变参数导致的错误的常见做法是将none指定为参数的默认值,然后检查是否有值传给对应的参数。例:

def some_func(default_arg=none):
    if not default_arg:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

34. 捕获异常

这里将的是 python2

some_list = [1, 2, 3]
try:
    # 这里会抛出异常 ``indexerror``
    print(some_list[4])
except indexerror, valueerror:
    print("caught!")

try:
    # 这里会抛出异常 ``valueerror``
    some_list.remove(4)
except indexerror, valueerror:
    print("caught again!")

output:

caught!

valueerror: list.remove(x): x not in list

说明:
如果你想要同时捕获多个不同类型的异常时,你需要将它们用括号包成一个元组作为第一个参数传递。第二个参数是可选名称,如果你提供,它将与被捕获的异常实例绑定。
也就是说,代码原意是捕获indexerror, valueerror两种异常,但在 python2 中,必须写成(indexerror, valueerror),示例中的写法解析器会将valueerror理解成绑定的异常实例名。
在 python3 中,不会有这种误解,因为必须使用as关键字。

35. +=就地修改

a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]

output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]

a = [1, 2, 3, 4]
b = a
a += [5, 6, 7, 8]

output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

说明:
a += b 并不总是与 a = a + b 表现相同。
表达式 a = a + [5,6,7,8] 会生成一个新列表,并让 a 引用这个新列表,同时保持 b 不变。
表达式 a += [5, 6, 7, 8] 实际上是使用的是 "extend" 函数,就地修改列表,所以 ab 仍然指向已被修改的同一列表。

36. 外部作用域变量

a = 1
def some_func():
    return a

def another_func():
    a += 1
    return a

output:

>>> some_func()
1
>>> another_func()
unboundlocalerror: local variable 'a' referenced before assignment

说明:
当在函数中引用外部作用域的变量时,如果不对这个变量进行修改,则可以直接引用,如果要对其进行修改,则必须使用 global 关键字,否则解析器将认为这个变量是局部变量,而做修改之前并没有定义它,所以会报错。

def another_func()
    global a
    a += 1
    return a

output:

>>> another_func()
2

37. 小心链式操作

>>> (false == false) in [false] # 可以理解
false
>>> false == (false in [false]) # 可以理解
false
>>> false == false in [false] # 为毛?
true

>>> true is false == false
false
>>> false is false is false
true

>>> 1 > 0 < 1
true
>>> (1 > 0) < 1
false
>>> 1 > (0 < 1)
false

根据

形式上,如果 a, b, c, ..., y, z 是表达式,而 op1, op2, ..., opn 是比较运算符,那么 a op1 b op2 c ... y opn z 就等于 a op1 b and b op2 c and ... y opn z,除了每个表达式最多被评估一次。

  • false == false in [false]就相当于false == false and false in [false]
  • 1 > 0 < 1就相当于1 > 0 and 0 < 1

虽然上面的例子似乎很愚蠢, 但是像 a == b == c0 <= x <= 100 就很棒了。

38. 忽略类作用域的名称解析

① 生成器表达式

x = 5
class someclass:
    x = 17
    y = (x for i in range(10))

output:

>>> list(someclass.y)
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]

② 列表推导式

x = 5
class someclass:
    x = 17
    y = [x for i in range(10)]

output(python 2.x):

>>> someclass.y
[17, 17, 17, 17, 17, 17, 17, 17, 17, 17]

output(python 3.x):

>>> someclass.y
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]

说明:

  • 类定义中嵌套的作用域会忽略类内的名称绑定。
  • 生成器表达式有它自己的作用域。
  • 从 python 3 开始,列表推导式也有自己的作用域。

39. 元组

x, y = (0, 1) if true else none, none

output:

>>> x, y  # 期望的结果是 (0, 1)
((0, 1), none)

t = ('one', 'two')
for i in t:
    print(i)

t = ('one')
for i in t:
    print(i)

t = ()
print(t)

output:

one
two
o
n
e
tuple()

说明:
对于 1,正确的语句是 x, y = (0, 1) if true else (none, none)
对于 2,正确的语句是 t = ('one',) 或者 t = 'one', (缺少逗号) 否则解释器会认为 t 是一个字符串,并逐个字符对其进行迭代。
() 是一个特殊的标记,表示空元组。

40. else

① 循环末尾的 else

def does_exists_num(l, to_find):
    for num in l:
        if num == to_find:
            print("exists!")
            break
    else:
        print("does not exist")

output:

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
exists!
>>> does_exists_num(some_list, -1)
does not exist

② try 末尾的 else

try:
    pass
except:
    print("exception occurred!!!")
else:
    print("try block executed successfully...")

output:

try block executed successfully...

说明:
循环后的 else 子句只会在循环执行完成(没有触发 break、return 语句)的情况下才会执行。
try 之后的 else 子句也被称为 "完成子句",因为在 try 语句中到达 else 子句意味着 try 块实际上已成功完成。

41. 名称改写

class yo(object):
    def __init__(self):
        self.__honey = true
        self.bitch = true

output:

>>> yo().bitch
true
>>> yo().__honey
attributeerror: 'yo' object has no attribute '__honey'
>>> yo()._yo__honey
true

说明:
python 中不能像 java 那样使用 private 修饰符创建私有属性。但是,解释器会通过给类中以 _(双下划线)开头且结尾最多只有一个下划线的类成员名称加上 类名 来修饰。这能避免子类意外覆盖父类的“私有”属性。

举个例子:有人编写了一个名为 dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 dog 类的子类 beagle,如果你在毫不知情的情况下又创建了一个 mood 实例属性,那么在继承的方法中就会把 dog 类的 mood 属性覆盖掉。

为了避免这种情况,python 会将 __mood 变成 _dog__mood,而对于 beagle 类来说,会变成 _beagle__mood。这个语言特性就叫名称改写(name mangling)。

42. +=更快

>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# 用 "+=" 连接三个字符串:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281

说明:
连接两个以上的字符串时 += 比 + 更快,因为在计算过程中第一个字符串(例如, s1 += s2 + s3 中的 s1)不会被销毁。(就是 += 执行的是追加操作,少了一个销毁新建的动作。)