Python: 无意中修改传入函数的参数
有几次我不小心改动了一个函数的输入。因为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 个回答
你可以这样使用元类:
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
随便复制参数“以防万一”其实是个坏主意:这样做会导致性能变差;或者你得时刻关注参数的大小。
更好的做法是好好理解对象和名称,以及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_list
和alist
就是两个不同的列表对象,它们里面的内容是一样的——所以如果你只是对列表对象进行一些操作(比如插入、删除、重新排列),那就没问题。但如果你要深入修改列表里的对象,那就需要用deepcopy()
。不过这些事情还是得你自己注意,并且写出合适的代码。
第一条基本规则: 不要修改容器:创建新的容器。
也就是说,不要直接修改你收到的字典,而是要创建一个新的字典,只包含你需要的键。
self.fields = dict( key, value for key, value in fields.items()
if accept_key(key, data) )
这样做通常比逐个删除不需要的元素要更有效率。更一般来说,避免修改对象,而是创建新的对象,通常会更简单。
第二条基本规则: 在传递容器后不要修改它。
你不能假设你传递给别人的容器会自己复制一份。因此,不要尝试去修改你已经给他们的容器。所有的修改应该在你交出数据之前完成。一旦你把容器交给别人,你就不再是唯一的拥有者了。
第三条基本规则: 不要修改你没有创建的容器。
如果你接收到一个容器,你不知道还有谁在使用这个容器。所以不要去修改它。要么使用未修改的版本,要么遵循第一条规则,创建一个新的容器并进行你想要的修改。
第四条基本规则:(来自Ethan Furman)
有些函数是 专门用来修改 列表的。这是它们的工作。如果是这种情况,在函数名称中要明确表示出来(比如列表方法append和extend)。
总结一下:
一段代码只有在它是唯一可以访问那个容器的代码时,才可以修改这个容器。