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 к передаче параметров по ссылке, что, по-видимому, было сделано по какой-то причине.

5 ответов


Первое правило: не изменяйте контейнеры: создавайте новые.

поэтому не изменяйте входящий словарь, создайте новый словарь с подмножеством ключей.

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

такие методы обычно немного более эффективны, чем прохождение и удаление плохих элементов в любом случае. В целом, часто проще избегать изменения объектов и вместо этого создавать новые.

Второе правило: не изменяйте контейнеры после их передачи.

обычно вы не можете предположить, что контейнеры, которым вы передали данные, сделали свои собственные копии. В результате не пытайтесь изменить контейнеры, которые вы им дали. Любые изменения должны быть сделаны до передачи данных. Как только вы передали контейнер кому-то другому, вы больше не являетесь его единственным хозяином.

Третье правило: не изменяйте контейнеры, которые вы не создавать.

Если вам передан какой-то контейнер, вы не знаете, кто еще может использовать контейнер. Поэтому не изменяйте его. Используйте немодифицированную версию или вызовите rule1, создав новый контейнер с необходимыми изменениями.

Четвертое правило: (украдено у Итана Фурмана)

некоторые функции должен изменить список. Это их работа. Если это так сделайте это очевидным в функции имя (например, методы списка append и extend).

собираем все вместе:

фрагмент кода должен изменять контейнер только тогда, когда он является единственным фрагментом кода с доступом к этому контейнеру.


создание копий параметров "на всякий случай" - плохая идея: вы в конечном итоге платите за это в паршивой производительности; или вам нужно отслеживать размеры ваших аргументов.

лучше, чтобы получить хорошее представление об объектах и именах и как Python имеет дело с ними. Хорошее начало быть в этой статье.

точка importart том, что

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(). Но это зависит от вас, чтобы отслеживать такие вещи и код соответствующим образом.


вы можете использовать метакласс следующим образом:

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 и называется модульным тестированием.

основной момент здесь заключается в том, что динамические языки позволяют быстро развиваться даже при полных модульных тестах; а модульные тесты-гораздо более жесткая сеть безопасности, чем статическая типизация.
Как пишет Мартин Фаулер:

общий аргумент для статических типов заключается в том, что он ловит ошибки, которые иначе трудно найти. Но я обнаружил, что в присутствии SelfTestingCode, большинство ошибок, которые статические типы были бы найдены просто по тестам.


Как заметил @delnan самое простое решение-всегда проходят immutables. Вы также можете обернуть свои переменные в пользовательский объект constant.

Python: любой способ объявить постоянные параметры?