python: непреднамеренное изменение параметров, передаваемых в функцию - PullRequest
11 голосов
/ 11 сентября 2011

Несколько раз я случайно модифицировал вход для функции. Поскольку у 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 ]

13 голосов
/ 11 сентября 2011

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

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

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

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

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

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

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

Если вы прошли какой-то контейнер, вы не знаете, кто еще может использовать этот контейнер.Так что не изменяйте это.Либо используйте неизмененную версию, либо вызовите правило 1, создав новый контейнер с нужными изменениями.

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

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

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

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

4 голосов
/ 30 сентября 2011

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

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

1 голос
/ 30 сентября 2011

В Python есть лучшая практика для этого, которая называется модульное тестирование.

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

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

1 голос
/ 11 сентября 2011

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

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
0 голосов
/ 11 сентября 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...