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.