在函数内部创建不可变对象

2 投票
3 回答
3572 浏览
提问于 2025-04-16 15:42

我在StackOverflow上问过一个问题,链接在这里:Python不可变类型在函数调用中的表现

这个问题让我明白,传递给函数的其实只是对不可变对象的引用,所以把一个元组传给函数并不会导致这个对象在内存中被完整复制。

不过,根据这个链接:http://www.testingreflections.com/node/view/5126

“一些对象,比如字符串、元组和数字,是不可变的。在函数或方法内部修改它们会创建一个新的实例,而函数外部的原始实例不会改变。”

我写了一些测试代码,把一个不可变对象传给一个函数。正如我预期的那样,我可以通过函数头部定义的参数名来修改这个对象,但所有的修改只在被调用的函数内部有效,函数外部的原始对象保持不变。

所以我想问的是:

只有在尝试修改传入的对象时,才会创建新的实例吗?我猜如果对象没有被改变,引用它就足够了。更重要的是,如果在尝试修改时确实会创建一个副本,Python是如何管理内存的呢?是采用零复制/写时复制的方式,还是会创建一个完整的复制对象(在内存中保留整个对象的大小),这个对象只在被调用的函数内部可见?

3 个回答

1

如果一个对象是不可变的,那就没办法去改变它。你可以把一个新的对象赋值给之前和这个对象关联的名字。要做到这一点,你首先需要创建一个新的对象。所以,是的,你需要为这个全新的对象分配空间。

2

我不是在单独说Python,而是一般来说,在不可变的数据结构中,任何需要改变状态的方法都会返回一个新的对象(这个对象的状态已经被修改)。而旧的对象则保持不变。

举个例子,Java中的可变列表可能会有:

void addItem(Object item) { ... }

而对应的不可变列表会有类似这样的一个方法:

List addItem(Object item) { ... }

所以,实际上不可变的数据结构并没有什么特别之处。在任何编程语言中,你都可以创建不可变的数据结构。不过,有些语言让创建可变的数据结构变得困难甚至不可能(通常是函数式编程语言)。

有些语言可能会提供伪不可变的数据结构。它们让某些特殊的数据结构在程序员眼里看起来是不可变的,但实际上并不是。

6

在Python中,如果你把变量想成是附在对象上的名字,而不是装值的盒子,你会更清楚地理解它们。任何对象都可以有多个名字,有些名字是局部的,只在函数内部有效,函数返回时会自动去掉这些名字。

比如你这样做:

name = "Slartibartfast"
person = name

这里有一个字符串对象,里面的内容是“Slartibartfast”,它可以用两个名字来称呼:nameperson。无论用哪个名字,你得到的都是同一个对象;你可以用id()函数来验证这一点。

那么,这个字符串的“真实”名字是name还是person呢?这是个陷阱问题。这个字符串本身并没有名字,它就是一个字符串。name并不是一个装着“Slartibartfast”的盒子,它只是一个指向这个对象的标识符。person在Python中地位是一样的;name并不因为先被赋值就“更重要”。

注意:有些对象,比如函数和类,有一个__name__属性,保存了在defclass语句中用来声明它的名字。如果可以说它有“真实名字”,那就是这个。不过,你仍然可以通过任何赋值的名字来引用它。

现在,假设你“修改”这个字符串,让它更有荷兰风味:

person = person.replace("art", "aart")

这里的“修改”加了引号,因为你不能真正修改一个字符串。由于字符串是不可变的,每次字符串操作都会创建一个新的字符串。什么时候发生呢?立刻。在这个例子中,新的字符串“Slaartibaartfast”被创建,名字person被调整为指向这个新字符串。然而,名字name仍然指向原来的字符串,因为你没有告诉它去指向其他东西。只要至少有一个名字指向它,Python就会保留那个老旧的“Slartibartfast”。

处理函数时也是一样的:

def dutchnametag(name):
    name = name.replace("art", "aart")
    print "HELLO! My Dutch name is", name

person = "Slartibartfast"
dutchnametag(person)

这里我们把字符串“Slartibartfast”赋值给全局名字person。然后我们把这个字符串传递给我们的函数,函数里得到了一个额外的局部名字name。接着通过name这个标识符调用字符串的replace()方法,创建了一个新字符串。然后,标识符name被重新指向这个新字符串。在函数外,全局标识符person仍然指向原来的字符串,因为没有任何操作改变它指向其他东西。

撰写回答