Skip to content

Python 的可变性

我自己 fork 了一份 iOSFakeRun 项目,并放到了我的私有仓库里。同时,我添加了随机生成路径的代码。可是,我发现代码最后生成的随机路径中,不乏有重复的路径。有一定经验的人,很容易看出这就是 Python 里的 mutability 问题。Python 的可变性其实是一个挺恶心的一点:int 等式不变的,但是 list 等又是可变的。与 C/C++ 等对可变性控制非常严格的语言不同,新手很容易陷入 mutability 中。

我的问题:

coords *= repeat_time

首先,上面的命令是一个浅拷贝。如果 coords 里面的 elements 是 immutable 的,那么,依靠上面的浅拷贝,就会导致 elements 重复。比如:

>>> a=[{"a":1,"b":2},{"a":5,"b":-3}] 
>>> a*=2
>>> print(list(map(id,a)))
[2103007505344, 2103006655552, 2103007505344, 2103006655552]

不难看出,由于 dict 是可变对象,我们采用浅拷贝之后,就会有重复的元素(对于同类型元素,id 一样,元素就是“同一个”)。

如果采用深拷贝:

>>> from copy import deepcopy
>>> a=[{"a":1,"b":2},{"a":5,"b":-3}] 
>>> new_a=[]
>>> for _ in range(2):
...     new_a+=deepcopy(a)
...
>>> a=new_a
>>> print(list(map(id,a)))
[2103009995264, 2103010101760, 2103010105280, 2103010109888]

可以看到,就都不一样了。

对于原问题,我们可以采用同样的方法:

from copy import deepcopy
# ...
new_coords = []
for _ in range(repeat_time):
    new_coords += coords
coords = new_coords

原理

本质上,deepcopy 就是 recursive shallow_copy。举例:

>>> a=[{"a":1,"b":2},{"a":5,"b":-3}] 
>>> new_a=[]
>>> for _ in range(2):
...     for i in a:
...         new_a.append(i.copy())
...
>>> a=new_a
>>> print(list(map(id,a)))
[1588961447424, 1588961447168, 1588961454336, 1588961447360]

我们这里由于已经提前知道了要处理的数据类型,因此不需要递归实现,而只需要实现一个 domain-specific algorithm 即可。

至于完整版本,deepcopy 的具体实现为:

from copy import copy

def deepcopy(obj):
    if isinstance(obj, dict):
        new_obj = {}
        for key, value in obj.items():
            new_obj[key] = deepcopy(value)
        return new_obj
    elif isinstance(obj, list):
        new_obj = []
        for item in obj:
            new_obj.append(deepcopy(item))
        return new_obj
    elif isinstance(obj, set):
        new_obj = set()
        for item in obj:
            new_obj.add(deepcopy(item))
        return new_obj
    elif isinstance(obj, tuple):
        new_obj = ()
        for item in obj:
            new_obj += (deepcopy(item),)
        return new_obj
    else:
        return copy(obj)

copy 的实现为:

def copy(obj):
    if isinstance(obj, dict):
        new_obj = {}
        for key, value in obj.items():
            new_obj[key] = value
        return new_obj
    elif isinstance(obj, list):
        new_obj = []
        for item in obj:
            new_obj.append(item)
        return new_obj
    elif isinstance(obj, set):
        new_obj = set()
        for item in obj:
            new_obj.add(item)
        return new_obj
    elif isinstance(obj, tuple):
        new_obj = ()
        for item in obj:
            new_obj += (item,)
        return new_obj
    else:
        return obj