Python: 无意中修改传入函数的参数

16 投票
4 回答
6994 浏览
提问于 2025-04-17 02:02

有几次我不小心改动了一个函数的输入。因为Python没有常量引用,我在想有什么编程技巧可以帮助我减少这种错误的发生频率。

举个例子:

class Table:
  def __init__(self, fields, raw_data):
    # fields is a dictionary with field names as keys, and their types as value 
    # sometimes, we want to delete some of the elements 
    for field_name, data_type in fields.items():
      if some_condition(field_name, raw_data):
        del fields[field_name]
    # ...


# in another module

# fields is already initialized here to some dictionary
table1 = Table(fields, raw_data1) # fields is corrupted by Table's __init__
table2 = Table(fields, raw_data2)

当然,解决办法是在我改变参数之前先复制一份:

  def __init__(self, fields, raw_data):
    fields = copy.copy(fields)
    # but copy.copy is safer and more generally applicable than .copy 
    # ...

但这真的很容易忘记。

我在考虑在每个函数开始时都复制每个参数,除非这个参数可能指向一个很大的数据集,复制起来会很耗费资源,或者这个参数是打算被修改的。这样几乎可以消除这个问题,但这会导致每个函数开头都有大量没用的代码。此外,这样做基本上就违背了Python通过引用传递参数的方式,而这样做显然是有原因的。

4 个回答

1

你可以这样使用元类:

import copy, new
class MakeACopyOfConstructorArguments(type):

    def __new__(cls, name, bases, dct):
        rv = type.__new__(cls, name, bases, dct)

        old_init = dct.get("__init__")
        if old_init is not None:
            cls.__old_init = old_init
            def new_init(self, *a, **kw):
                a = copy.deepcopy(a)
                kw = copy.deepcopy(kw)
                cls.__old_init(self, *a, **kw)

        rv.__init__ = new.instancemethod(new_init, rv, cls)
        return rv

class Test(object):
    __metaclass__ = MakeACopyOfConstructorArguments

    def __init__(self, li):
        li[0]=3
        print li


li = range(3)
print li
t = Test(li)
print li
4

随便复制参数“以防万一”其实是个坏主意:这样做会导致性能变差;或者你得时刻关注参数的大小。

更好的做法是好好理解对象和名称,以及Python是怎么处理这些的。可以从这篇文章开始。

重点是

def modi_list(alist):
    alist.append(4)

some_list = [1, 2, 3]
modi_list(some_list)
print(some_list)

some_list = [1, 2, 3]
same_list = some_list
same_list.append(4)
print(some_list)

完全一样的效果,因为在函数调用时并没有进行参数的复制,也没有创建指针……实际上发生的是Python在说alist = some_list,然后执行函数modi_list()里的代码。换句话说,Python是在绑定(或赋值)另一个名称给同一个对象

最后,当你有一个函数会修改它的参数,而你又不想让这些修改在函数外部可见时,通常可以做一个浅拷贝:

def dont_modi_list(alist):
    alist = alist[:]  # make a shallow copy
    alist.append(4)

这样some_listalist就是两个不同的列表对象,它们里面的内容是一样的——所以如果你只是对列表对象进行一些操作(比如插入、删除、重新排列),那就没问题。但如果你要深入修改列表里的对象,那就需要用deepcopy()。不过这些事情还是得你自己注意,并且写出合适的代码。

14

第一条基本规则: 不要修改容器:创建新的容器。

也就是说,不要直接修改你收到的字典,而是要创建一个新的字典,只包含你需要的键。

self.fields = dict( key, value for key, value in fields.items()
                     if accept_key(key, data) )

这样做通常比逐个删除不需要的元素要更有效率。更一般来说,避免修改对象,而是创建新的对象,通常会更简单。

第二条基本规则: 在传递容器后不要修改它。

你不能假设你传递给别人的容器会自己复制一份。因此,不要尝试去修改你已经给他们的容器。所有的修改应该在你交出数据之前完成。一旦你把容器交给别人,你就不再是唯一的拥有者了。

第三条基本规则: 不要修改你没有创建的容器。

如果你接收到一个容器,你不知道还有谁在使用这个容器。所以不要去修改它。要么使用未修改的版本,要么遵循第一条规则,创建一个新的容器并进行你想要的修改。

第四条基本规则:(来自Ethan Furman)

有些函数是 专门用来修改 列表的。这是它们的工作。如果是这种情况,在函数名称中要明确表示出来(比如列表方法append和extend)。

总结一下:

一段代码只有在它是唯一可以访问那个容器的代码时,才可以修改这个容器。

撰写回答