LAST UPDATE: August 24th 2018, 11:54:05 PM

wtfpython 是github上一个有趣的项目,记录了Python一些小技巧或让人感觉稀奇古怪的行为或鲜为人知的功能,之前了解到这个项目的时候还打算一次性全部看完,但是并没有做到,这次打算从头看起并做一下记录,日常来上一个两个,提提神醒醒脑,还是很有意思的,至于这次能坚持看到哪,那就看造化了……

Section: Strain your brain!

Strings can be tricky sometimes

>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string") # Notice that both the ids are same.
140420665652016

>>> a = "wtf"
>>> b = "wtf"
>>> a is b
True

>>> a = "wtf!"
>>> b = "wtf!"
>>> a is b
False

>>> a, b = "wtf!", "wtf!"
>>> a is b
True

>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False

这一节的内容是字符串的intern机制(在某些情况下使用已经存在的不可变对象,而不是重新创建),可以节省内存,上面的例子是隐式intern造成的,显式则需要使用函数sys.intern()。隐式intern的规则是

  • 空字符串
  • 只由"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"组成且在编译阶段被声明(如:对字符串字面量使用加号连接(这个是由于解释器的窥孔优化),或直接写出字符串),运行时产生的不算(如:字符串join函数)
  • 窥孔优化还规定通过乘法运算得到的字符串长度小于20才可以被intern
  • 如果不符合上述要求的字符串在同一行被创建,则是intern

Time for some hash brownies!

>>> some_dict = {}
>>> some_dict[5.5] = "Ruby"
>>> some_dict[5.0] = "JavaScript"
>>> some_dict[5] = "Python"

>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"

这个看上去奇怪的行为是因为在Python中5.0 == 5,不可变对象作为dict的键时,如果值相同,则键相同,相反,如果不可变对象值相同,则哈希值应该相同。

Return return everywhere!

>>> def some_func():
...     try:
...         return 'from_try'
...     finally:
...         return 'from_finally'
...
>>> some_func()
'from_finally'

粗看有问题,细看没毛病,finally最后总是会执行,例子中出现的情况还适用于breakcontinue

Deep down, we’re all the same.

>>> class WTF:
...     pass
...
>>> WTF() == WTF() # two different instances can't be equal
False
>>> WTF() is WTF() # identities are also different
False
>>> hash(WTF()) == hash(WTF()) # hashes _should_ be different as well
True
>>> id(WTF()) == id(WTF())
True


>>> class WTF(object):
...     def __init__(self): print("I ")
...     def __del__(self): print("D ")
...
>>> WTF() is WTF()
I I D D
>>> id(WTF()) == id(WTF())
I D I D

idhash调用时创建对象后拿到需要的东西然后把对象销毁,第二次创建时在同一个位置,所以会有相同的地址,而==is使用时对象依旧存活,不可能分配在相同的内存

For what?

>>> some_string = "wtf"
>>> some_dict = {}
>>> for i, some_dict[i] in enumerate(some_string):
...     pass
...
>>> some_dict # An indexed dict is created.
{0: 'w', 1: 't', 2: 'f'}


>>> for i in range(4):
...     print(i)
...     i = 10
...
0
1
2
3

这个是有关Python中for循环的基础知识,那种感觉很少有人会讲的基础知识
语法for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]exprlist其实是赋值对象,每次都会执行赋值操作,所以字典那个例子中实际上每次都执行了some_dict[i] = some_string[i],同理,下面的例子每次都对i进行了赋值,所赋的值由迭代器range(4)当前的状态决定,所以i = 10并没有对循环造成影响

Evaluation time discrepancy

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

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


>>> array_1 = [1,2,3,4]
>>> g1 = (x for x in array_1)
>>> array_1 = [1,2,3,4,5]

>>> array_2 = [1,2,3,4]
>>> g2 = (x for x in array_2)
>>> array_2[:] = [1,2,3,4,5]

>>> print(list(g1))
[1,2,3,4]

>>> print(list(g2))
[1,2,3,4,5]
  • 生成器表达式中in子句的绑定是在定义时执行,条件子句是在运行时执行
  • 下面的例子中array_2更改的是原来的对象,而不是新建一个对象,所以对in子句有影响

is is not what it is!

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

这个已经众所周知了

  • is比较地址,==比较值
  • int常量池: [-5, 256] ref
  • 不在常量池的如果在同一行被创建: a, b = 257, 257

A tic-tac-toe where X wins in the first attempt!

# Let's initialize a row
row = [""]*3 #row i['', '', '']
# Let's make a board
board = [row]*3

>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

这个已经是烂大街级别了,列表repeat的是引用而不是值,解决办法就是不要在列表中有可变对象的时候repeat

The sticky output function

>>> funcs = []
>>> results = []
>>> for x in range(7):
...     def some_func():
...         return x
...     funcs.append(some_func)
...     results.append(some_func())
...
>>> funcs_results = [func() for func in funcs]
>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

# OR

>>> powers_of_x = [lambda x: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

闭包绑定的是变量不是值,这里也一样,那么solution就是谨慎使用闭包变量咯:

>>> funcs = []
>>> for x in range(7):
...     def some_func(x=x):
...         return x
...     funcs.append(some_func)

is not … is not is (not …)

>>> 'something' is not None
True
>>> 'something' is (not None)
False

cpython/Python/peephole.c ref 里有这么一段:

not a is b -->  a is not b
not a in b -->  a not in b
not a is not b -->  a is b
not a not in b -->  a in b

好了破案了

The surprising comma

>>> def f(x, y,):
...     print(x, y)
...
>>> def g(x=4, y=5,):
...     print(x, y)
...
>>> def h(x, **kwargs,):
  File "<stdin>", line 1
    def h(x, **kwargs,):
                     ^
SyntaxError: invalid syntax
>>> def h(*args,):
  File "<stdin>", line 1
    def h(*args,):
                ^
SyntaxError: invalid syntax

已经在Pytho3.6修复
老版本中函数定义时如果中间有星号,并不允许再有最后面的逗号
Python3.6之后基本允许所有情况下最后有一个逗号:

parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [
        '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
      | '**' tfpdef [',']]]
  | '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
  | '**' tfpdef [','])
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [
        '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
      | '**' vfpdef [',']]]
  | '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
  | '**' vfpdef [',']
)
vfpdef: NAME

顺便mark一下大佬总结的末尾逗号用法 ref,第二节就是这里讨论的函数参数列表的,但有错误,试一下就知道了

Backslashes at the end of string

>>> print("\\ C:\\")
\ C:\
>>> print(r"\ C:")
\ C:
>>> print(r"\ C:\")

    File "<stdin>", line 1
      print(r"\ C:\")
                     ^
SyntaxError: EOL while scanning string literal

这个是依然存在的问题(或许不是个问题?),解释器对于raw string里面的反斜杠,只是不跟下一个字符合在一起解释,但是还是跟下一个字符一起传递

not knot!

>>> x = True
>>> y = False

>>> not x == y
True
>>> x == not y
  File "<input>", line 1
    x == not y
           ^
SyntaxError: invalid syntax
  • not的优先级低于== ref
  • 所以not x == y其实是not (x == y)
  • 所以x == not y其实是(x == not) y,所以就报错了

Half triple-quoted strings

>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # The following statements raise `SyntaxError`
>>> # print('''wtfpython')
>>> # print("""wtfpython")
  1. Python支持隐式字符串连接,即print('test1' 'test2')这种格式,所以print('wtfpython''')被看作两个单引号字符串
  2. 三引号在字符串开头是作为界定符,于是一直等到下一个对应的三引号结束这个字符串,所以后两个并不行

Midnight time doesn’t exist?

from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()

if midnight_time:
    print("Time at midnight is", midnight_time)

if noon_time:
    print("Time at noon is", noon_time)

已在Python3.5修复
Python 3.5 之前这里的midnight_time作为bool判断是False,原因就是datetime.time对象在午夜这个时间看作False,大概是类似于数字里0是False的设定吧

What’s wrong with booleans?

>>> # A simple example to count the number of boolean and
>>> # integers in an iterable of mixed data types.
>>> mixed_list = [False, 1.0, "some_string", 3, True, [], False]
>>> integers_found_so_far = 0
>>> booleans_found_so_far = 0
>>>
>>> for item in mixed_list:
...     if isinstance(item, int):
...         integers_found_so_far += 1
...     elif isinstance(item, bool):
...         booleans_found_so_far += 1
...
>>> booleans_found_so_far
0
>>> integers_found_so_far
4


>>> another_dict = {}
>>> another_dict[True] = "JavaScript"
>>> another_dict[1] = "Ruby"
>>> another_dict[1.0] = "Python"

>>> another_dict[True]
"Python"


>>> some_bool = True
>>> "wtf"*some_bool
'wtf'
>>> some_bool = False
>>> "wtf"*some_bool
''
  • TrueFalsebool对象,boolint的子类
  • True作为数值是1,False作为数值是0

Class attributes and instance attributes

>>> class A:
...     x = 1
...
>>> class B(A):
...     pass
...
>>> class C(A):
...     pass
...
>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)


>>> class SomeClass:
...     some_var = 15
...     some_list = [5]
...     another_list = [5]
...     def __init__(self, x):
...         self.some_var = x + 1
...         self.some_list = self.some_list + [x]
...         self.another_list += [x]
...
>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True

这一段主要有两个点:

  • 属性找不到会按着MRO去找
  • 可变对象和不可变对象

破案

yielding None

>>> ome_iterable = ('a', 'b')
>>> def some_func(val):
...     return "something"
...
>>> [x for x in some_iterable]
['a', 'b']
>>> [(yield x) for x in some_iterable]
<generator object <listcomp> at 0x7f70b0a4ad58>
>>> list([(yield x) for x in some_iterable])
['a', 'b']
>>> list((yield x) for x in some_iterable)
['a', None, 'b', None]
>>> list(some_func((yield x)) for x in some_iterable)
['a', 'something', 'b', 'something']
  1. 这是个非常新鲜的bug,在Python3.8修复,Python3.7还会给出warning的新鲜bug
  2. bug就bug在yield只能用在定义的函数里 ref,虽然Python3中列表/集合/字典/生成器推导式被编译成函数对象,但这些,虽然都是一个新的嵌套作用域,但是,但是,这是伪装的函数
  3. 在知道这个是bug的情况下来看为什么在列表推导式和生成器推导式中用yield会有不同的行为,列表推导式中每次用YIELD_VALUE把数到栈顶然后用LIST_APPEND把栈顶放进去,生成器就不一样了,生成器每次取值用的就是YIELD_VALUE然后POP_TOP,在这种情况下,每次取的是yield i,除了数字之外还会有yield i的返回值,也就是None,因为没人send回来
    >>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
    1           0 BUILD_LIST               0
               2 LOAD_FAST                0 (.0)
         >>    4 FOR_ITER                10 (to 16)
               6 STORE_FAST               1 (i)
               8 LOAD_FAST                1 (i)
              10 YIELD_VALUE
              12 LIST_APPEND              2
              14 JUMP_ABSOLUTE            4
         >>   16 RETURN_VALUE
    >>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
    1           0 LOAD_FAST                0 (.0)
         >>    2 FOR_ITER                12 (to 16)
               4 STORE_FAST               1 (i)
               6 LOAD_FAST                1 (i)
               8 YIELD_VALUE
              10 YIELD_VALUE
              12 POP_TOP
              14 JUMP_ABSOLUTE            2
         >>   16 LOAD_CONST               0 (None)
              18 RETURN_VALUE
    
  4. 列表也有返回值啊为什么没有出现返回值None,试一下会发现这个语句居然成了生成器,所以返回值被放到StopIteration对象的value属性了 ref
    >>> from itertools import islice
    >>> listgen = [(yield i) for i in range(3)]
    >>> list(islice(listgen, 3))  # avoid exhausting the generator
    [0, 1, 2]
    >>> try:
    ...     next(listgen)
    ... except StopIteration as si:
    ...     print(si.value)
    ...
    [None, None, None]
    
  5. 在集合/字典推导式中也存在这个情况

again, 这是个bug

Mutating the immutable!

>>> some_tuple = ("A", "tuple", "with", "values")
>>> another_tuple = ([1, 2], [3, 4], [5, 6])

>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) #This throws no error
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])
  • 还是可变不可变的问题,不可变对象里可能有可变对象,只要可变对象的引用不变
  • +=in-place add,所以不改变引用,但是还是会有个异常

The disappearing variable from outer scope

>>> e = 7
>>> try:
>>>     raise Exception()
>>> except Exception as e:
>>>     pass

# Output (Python 2.x):
>>> print(e)
# prints nothing

# Output (Python 3.x):
>>> print(e)
NameError: name 'e' is not defined
  • try...except...finally...没有隔开形成单独的作用域
  • 异常处理中
    try:
      stmt
    except E as N:
      process
    
    异常E会被赋给N,另外在Python3中上面代码中except子句会被转换为
    try:
      stmt
    except E as N:
      try:
          process
      finally:
          del N
    
    所以异常处理后,在Python2中因为e是个异常,所以输出的时候是空的,但是在Python3中e已经被删了,于是报错了
  • 所以异常变量别用已经存在的那些

When True is actually False

>>> True = False
>>> if True == False:
>>>     print("I've lost faith in truth!")
...
I've lost faith in truth!

这是个过期知识了,本来Python是没有bool的,只用0表示假,用非零表示真,后来Python2.2的时候加了两个内建变量TrueFalse,然后分别简单地设置为1和0,Python2.3中加入了bool类型,但是Python2中为了兼容,不能把TrueFalse做成关键字,Python3没有兼容问题,于是TrueFalse变成了关键字,也就不能赋值了

From filled to None in one instruction…

>>> some_list = [1, 2, 3]
>>> some_dict = {
...   "key_1": 1,
...   "key_2": 2,
...   "key_3": 3
... }
>>> some_list = some_list.append(4)
>>> some_dict = some_dict.update({"key_4": 4})

>>> print(some_list)
None
>>> print(some_dict)
None

大多数对sequence/mapping对象里的元素进行修改的函数都是in-place并返回None的,如list.appendlist.sortdict.update等,原因就是in-place操作能避免产生额外的副本,从而提升性能

Subclass relationships

>>> from collections import Hashable
>>> issubclass(list, object)
True
>>> issubclass(object, Hashable)
True
>>> issubclass(list, Hashable)
False
  • Python中子类没有传递性,可以自定义元类的__subclasscheck__
  • Hashable只检查有没有实现__hash__方法
def _check_methods(C, *methods):
    mro = C.__mro__
    for method in methods:
        for B in mro:
            if method in B.__dict__:
                if B.__dict__[method] is None:
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

class Hashable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __hash__(self):
        return 0

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

The mysterious key type conversion

>>> class SomeClass(str):
...     pass
...
>>> some_dict = {'s':42}

>>> type(list(some_dict.keys())[0])
str
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # expected: Two different keys-value pairs
{'s': 40}
>>> type(list(some_dict.keys())[0])
str
  • SomeClass继承自str且没有做任何改变,所以键是一样的
>>> class sss(str):
...     def __eq__(self, o):
...         return False
...     __hash__ = str.__hash__
...
>>> d = dict()
>>> 's' == sss('s')
False
>>> hash('s') == hash(sss('s'))
True
>>> d['s'] = 43
>>> d[sss('s')] = 42
>>> d
{'s': 43, 's': 42}
  • 这一个虽然__hash__一样,但是__eq__不一样
    • 自定义类时,如果重写了__eq____hash__自动停止继承,hashable规定这两个必须都有,__hash__保证在生命周期内不变,__eq__用于保证 “Hashable objects which compare equal must have the same hash value”
    • hash值相同但不相等,看作不同的键 ref

Let’s see if you can guess this?

>>> a, b = a[b] = {}, 5
>>> a
{5: ({...}, 5)}

Python规定赋值表达式格式 ref(target_list "=")+ (expression_list | yield_expression),且

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.

也就是说最后一个等号后面的是(expression_list | yield_expression),每个等号前为一组target_list,然后从左到右赋给target_list,但是因为a是可变对象,最开始赋值是a, b = {}, 5,然后到a[b]的时候,a{}变成了{5: },也就是后面那个字典的值已经变了,但这个字典的值始终是a的值,所以可以等价为a[b] = a, b,由此生成了一个无限重复的对象

链式赋值还有另一种情况

>>> s, i = [1, 2, 3, 4, 5, 6], -1
>>> i = s[i] = 0

>>> s[i] # 预期结果应该是[1, 2, 3, 4, 5, 0]
[0, 2, 3, 4, 5, 6]

对应的字节码为

>>> dis.dis(compile('''
... s, i = [1, 2, 3, 4, 5, 6], -1
... i = s[i] = 0''', '', 'exec'))
  2           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 LOAD_CONST               3 (4)
              8 LOAD_CONST               4 (5)
             10 LOAD_CONST               5 (6)
             12 BUILD_LIST               6
             14 LOAD_CONST               8 (-1)
             16 ROT_TWO
             18 STORE_NAME               0 (s)
             20 STORE_NAME               1 (i)

  3          22 LOAD_CONST               6 (0)
             24 DUP_TOP
             26 STORE_NAME               1 (i)
             28 LOAD_NAME                0 (s)
             30 LOAD_NAME                1 (i)
             32 STORE_SUBSCR
             34 LOAD_CONST               7 (None)
             36 RETURN_VALUE

这样就obvious了,原因就是Python在赋值过程一直是在栈操作的 ref,即i已经被改成了0

所以在链式赋值中要谨慎,注意可变对象已被改变以及前面的赋值会影响后面的情况

Section: Appearances are deceptive!

Skipping lines?

>>> value = 11
>>> valuе = 32
>>> value
11
  • eе 看上去一样,但一个是拉丁字母,一个是西里尔字母,并不一样
  • python 2.x 会报错

 Teleportation

>>> import numpy as np
>>>
>>> def energy_send(x):
...     # Initializing a numpy array
...     np.array([float(x)])
...
>>> def energy_receive():
...     # Return an empty numpy array
...     return np.empty((), dtype=np.float).tolist()
...
>>> energy_send(123.456)
>>> energy_receive()
123.456
  • energy_send 函数创建了 np.array 对象,但没有返回,所以这块内存是可分配的
  • energy_receive 函数返回空闲的内存槽,并且不进行初始化,很大概率出现上面的情况

Well, something is fishy…

>>> def square(x):
...     """
...     A simple function to calculate the square of a number by addition.
...     """
...     sum_so_far = 0
...     for counter in range(x):
...         sum_so_far = sum_so_far + x
...     return sum_so_far # 这个用 tab 缩进,其他用空格
...
>>> square(10) # Python 2.x
10
  • 只出现在 Python 2.x
  • 混用 tab 和 space 导致的
  • Python 把 tab 替换成 8 个空格,导致 return sum_so_farfor 循环里,执行了一次运算后就返回了
  • Python 3.x 输出 TabError: inconsistent use of tabs and spaces in indentation

Section: Watch out for the landmines!

Modifying a dictionary while iterating over it

>>> x = {0: None}
>>> for i in x:
...     del x[i]
...     x[i+1] = None
...     print(i)
# Python 2.7- Python 3.5
0
1
2
3
4
5
6
7
# > Python 3.5
0
1
2
3
4

对于 Python 3.6 及以上的版本

  1. dict 初始大小为 8 ref,大小为8且活跃条目不多于5能满足大部分需求

    8 allows dicts with no more than 5 active entries; experiments suggested this suffices for the majority of dicts

  2. 当空间用了大于 2/3 的时候会触发 resize ref,这个比率比较好地平衡了空间和哈希碰撞

    Increasing this ratio makes dictionaries more dense resulting in more collisions. Decreasing it improves sparseness at the expense of spreading indices over more cache lines and at the cost of total memory consumed.

  3. 删了的键会标为 DUMMY,只能通过插入激活,最后一次删除插入时触发 resize,插入在已经迭代过的地方,所以不会继续迭代下去

Stubborn del operator

>>> class SomeClass:
...     def __del__(self):
...         print("Deleted!")
...
>>> x = SomeClass()
>>> y = x
>>> del x
>>> del y
Deleted!
>>> x = SomeClass()
>>> y = x
>>> del x
>>> y
<__main__.SomeClass object at 0x7fa56c0b3b70>
>>> del y
>>> globals()
Deleted!
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'SomeClass': <class '__main__.SomeClass'>}
  • __del__ 在对象引用计数为 0 进行 GC 的时候才会调用
  • REPL 环境中 >>> y 这一句把对象引用计数加了 1 但没有马上减掉导致没有马上进行 GC
  • globals() 导致前面创建的引用被摧毁,计数变为 0

Deleting a list item while iterating

>>> 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)
...
>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]