坑出没 --- Python 中引用型类型浅拷贝

Python 中的对象有两种类型,一种是值类型,一种是引用类型。值类型的代表有 int,而今天的主角引用类型有 list、set、dict 等。

引用类型指的是:

a = [1, 2, 3]

在对象 a 中存储的是一个指针,这个指针指向数组 [1, 2, 3] 的底层数据,类似与 c++ 中的 vector。

那么什么叫浅拷贝呢?以下代码

shallow_cpy = a

shallow_cpy 就是对 a 这个引用类型进行一次浅拷贝(也叫做别名),虽然 a 与 shallow_cpy 是两个不同的变量,但是他们实际都是指向同一片内存空间。这就像一个人的中文名和英文名,虽然听着是不同的东西,但是其实都是对应一个人一样。任何对 a 的修改,最终也都会体现在 shallow_cpy 上(反之亦然)。

那么这会导致什么问题呢?不妨看一个实际场景:

我有一个简易爬虫,它的输入是一个 url set,因为这个爬虫运行时间很长,我们希望程序中可以加入一个功能,支持这个程序重新启动以后可以跳过之前处理过的 url。因为在单机运行,我决定使用 Python 自带的 pickle 模块 + 文件系统来做持久化存储,于是有下面的非常 naive 代码:

cache = {}

def load():
    """Load pickle from filesystem"""
    pass


def dump():
    """Dump cache dict as a pickle file"""
    pass


def set_url_set_if_not_exist(main_page, url_set):
    if main_page in cache:
        return
    cache[main_page] = url_set


def remove_url(main_page, url):
    assert main_page in cache
    cache[main_page].remove(url)


def craw(main_page, url_set):
    set_url_set_if_not_exist(main_page, url_set)
    for url in url set:
        # blabla
            
        # done with url
        print url
        remove_url(main_page, url)
    

craw('http://www.baidu.com', ['url1', 'url2', 'url3', 'url4'])

现在回首看这段代码,着实让我汗颜。这个功能的实现简直不知所谓。各位看官,不妨先问一下自己这个代码的问题在那?print 会输出那些 url ?

答案揭晓:

url1
url3

这是怎么回事, url_set 明明有4个元素,怎么 for 循环只输出了 2 个呢?这口锅,只能让咱们的浅拷贝来背了(或者让我这个菜鸡自己背也可以)。

问题的始发地在 set_url_set_if_not_exist(main_page, url_set) 上,这个函数中,我们将 cache[main_page] 赋值为 url_set,但是这个仅仅是对 url_set 的一个浅拷贝,此时 cache[main_page] 和 url_set 都指向同一个数组(['url1', 'url2', 'url3', 'url4'])。而

def craw(main_page, url_set):
    set_url_set_if_not_exist(main_page, url_set)
    for url in url set:
        # blabla
            
        # done with url
        print url
        # 其实是在 for 遍历 url_set 的期间对集合进行了修改
        remove_url(main_page, url)

for 循环中,对 remove_url(main_page, url) 的调用,其实就是在对被循环对象 url_set 的修改。这个其实就违背了一个使用迭代器的基本原则:使用迭代器期间要保证迭代器的有效性。

这里有必要先解释一下,在 Python 中,for 循环的机制如下面代码所示:

for i in iterable_obj:
    pass

# 等价于

it = iter(iterable_obj)
while True:
    try:
        i = it.next()
        # inside loop
    except StopIteration:
        break

对于一个 iterable object(iterable 指的是一种接口契约,所有符合这种契约的对象都可以成为 iterable object),Python 会先用 iter() 方法获取其迭代器,一直调用迭代器的 next 方法直到抛出 StopIteration 的异常,则结束循环。

而迭代器思想,指的就是通过提供迭代器去遍历一个数据集合的思想。要想保证 for 循环的正常运行,这个迭代器在 for 循环期间必须是有效的。

如果我们修改了数据集合,那么之前的任何迭代器,就很可能会失效了。这时候,it.next() 的行为是未定义的。不同的数据集合实现会导致这种情况下不同的输出(set 或者 tuple 的结果大家可以自己去尝试以下)。

可以看到,因为我们在 cache[main_page] 中储存的一份浅拷贝,所以对这个浅拷贝的增删修改,其实就是在修改 url_set,这就导致了 for 循环的未定义行为了。

其实不光是循环,如果对一份数据,存在多个浅拷贝(别名),那么使用其中任意一个别名对数据进行修改,其他的别名都会受到影响。这种行为,有时候可能是我们需要的(比如一些 input&output 参数),有些又往往会让我们大跌眼睛,完全没有头绪。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值