python:无意中修改传递给函数的参数

python:无意中修改传递给函数的参数,python,coding-style,Python,Coding Style,有几次我不小心修改了一个函数的输入。由于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 element

有几次我不小心修改了一个函数的输入。由于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通过引用传递参数的方法,这大概是有原因的

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

第一条一般规则:不要修改容器:创建新容器

所以,不要修改传入的字典,而是创建一个包含键子集的新字典

self.fields = dict( key, value for key, value in fields.items()
                     if accept_key(key, data) )
这样的方法通常比遍历和删除坏元素稍微有效一些。一般来说,避免修改对象而创建新对象通常更容易

第二条一般规则:不要在容器通过后修改它们

一般来说,您不能假设您向其传递数据的容器已经制作了自己的副本。因此,不要试图修改您提供给它们的容器。在移交数据之前,应进行任何修改。一旦你把容器交给别人,你就不再是它的唯一主人了

第三条一般规则:不要修改您没有创建的容器

如果您通过某种容器,您不知道还有谁在使用该容器。所以不要修改它。使用未修改的版本或调用rule1,创建具有所需更改的新容器

第四条一般规则:(从Ethan Furman那里偷来)

有些函数应该修改列表。那是他们的工作。如果是这种情况,请在函数名(例如列表方法append和extend)中显示这一点

将所有内容放在一起:


只有当容器是唯一可以访问该容器的代码时,一段代码才应该修改该容器

复制“以防万一”的参数是一个坏主意:你最终付出的代价是糟糕的性能;或者,您必须跟踪参数的大小

更好地理解对象和名称以及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
是两个不同的列表对象,恰好包含相同的对象——因此,如果您只是搞乱了列表对象(插入、删除、重新排列),那么您就没事了,buf如果您想更深入地修改列表中的对象,则需要执行
deepcopy()
。但是,由您来跟踪这些事情并适当地编写代码。

在Python中,有一种最佳实践,称为单元测试

这里的要点是,动态语言允许快速开发,即使是完整的单元测试;单元测试是比静态类型更为严格的安全网。
如文所述:

静态类型的一般论点是,它捕获的bug是 否则很难找到。但我发现,在 在SelfTestingCode中,发现了大多数静态类型的bug 同样容易通过测试


除了总是使用不可变类型之外?如果我需要一本字典,我怀疑使用一个冻结的dict(我必须自己实现)是否值得?谢谢。虽然在传递对象之后不修改它们,但这有点太极端了,不是吗?由于我必须允许任何函数修改它们,这意味着在我将对象
x
传递给一个函数后,我不能将它传递给任何其他函数?@max,添加了第三条一般规则:函数不应该修改
x
。一般的想法是,只有当只有一段代码在使用容器时,才可以真正安全地修改它。@max您可能对函数式编程感兴趣-它们根本不需要修改现有对象(大多数情况下)就可以完成。这种编程风格有一些非常有用的优点。第四条一般规则:如果您的函数确实修改了传入的容器,请在函数名中显示它(例如
列表
方法
附加
扩展
)。这听起来像是唯一现实的结果,因为我非常喜欢单元测试,我看不出它在这里有多大帮助。也许吧。我怀疑单元测试可能会漏掉这个bug,因为它只在将同一个容器传递到多个位置时发生。您必须进行集成测试。对于如何避免动态类型语言中的bug这个问题,这是一个很好的答案。这在这里似乎不合适,因为它没有引用OP询问的bug类型。OP知道他经常犯这个错误,因此在函数完成后让单元测试检查容器可能会发现bug——但是,如果测试中没有发生更改,它将