Когда Python копирует локальные списки, добавленные в глобальный словарь? - PullRequest
1 голос
/ 28 октября 2019
g_list = {}

def add_new_row(row_key, curr_value):
    global g_list

    curr_row = [None] * 2  # a local variable. I expected it to disappear after the function returns.
    curr_row[0] = curr_value
    curr_row[1] = curr_value * 2

    if row_key not in g_list:
        g_list[row_key] = []
    g_list[row_key].append(curr_row)
    curr_row[1] = 100  # just for test and illustrate the append by reference.

def main():
    curr_v = 1
    add_new_row(1, curr_v)

    curr_v = 2
    add_new_row(2, curr_v)

    global g_list
    print(g_list)

if __name__ == "__main__":
    main()

Output:
{1: [[1, 100]], 2: [[2, 100]]}

Насколько я понимаю, операция добавления в g_list[row_key].append является копированием по ссылке, поэтому значение curr_row[1] всегда равно 100. Однако я не понимаю, как глобальная переменная g_list может содержать значение локального выделенного списка (то есть curr_row) даже после того, как функция будет возвращена.

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

g_list[row_key].append(curr_row[:])

но работает следующий код:

g_list[row_key].append(curr_row)

Может кто-нибудь дать мне какой-нибудь совет, когда произошла эта глубокая копия локального списка?

Ответы [ 3 ]

5 голосов
/ 02 ноября 2019

Вы путаете объекты с именами . Все Объекты Python живут в куче, а имена - это просто ссылки на эти объекты. Список никогда не копируется, Python не обязан.

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

Для целей этой аналогии индексы списка - это просто больше меток, а ключи в словаре действуют как меткидля соответствующих значений. Фактически глобальные переменные модуля на самом деле являются просто парами ключ-значение в словаре!

Итак, в вашем конкретном примере вы создали объект списка, который дважды ссылается на None:

[None] * 2

На данный момент к этому списку не прикреплены ярлыки, он только создан. Единственная причина, по которой он не загорается сразу, заключается в том, что стек исполнения Python ссылается на него. Следующая инструкция, которую затем выполняет Python, состоит в том, чтобы взять верх стека выполнения и назначить его локальному, потому что остальная часть этого оператора:

curr_row = ...

Таким образом, объект списка выше теперь имеет метку, прикрепленную кэто, локальный curr_now.

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

curr_row[0] = curr_value
curr_row[1] = curr_value * 2

Итак, с учетом curr_value из 1 вытеперь получим следующее:

+---------+          +---------+              +---------+  
| Globals |    |     |  Heap   |        |     | Locals  |  
+---------+    |     +---------+        |     +---------+  
               |                        |                  
               |                        |                  
 g_list ------------> {}                |                  
               |                        |                  
               |            [1, 2] <----------- curr_row   
               |                        |                  

(приведенная выше диаграмма несколько упрощает ситуацию, так как целые числа 1 и 2 тоже действительно отдельные объекты, но из-за этого приведенная выше диаграмма не читается).

Затем вы добавляете ключ к g_list с новым пустым списком: row_key is 1 здесь:

if row_key not in g_list:
    g_list[row_key] = curr_row

Это создает ссылку между словарем и новымпустой объект списка:

+---------+          +---------+              +---------+
| Globals |    |     |  Heap   |       |      | Locals  |
+---------+    |     +---------+       |      +---------+
               |                       |                 
               |                       |                 
 g_list -------------> {1: <value>}    |                 
               |             |         |                 
               |             |         |                 
               |             v         |                 
               |            []         |                 
               |                       |                 
               |                       |                 
               |                       |                 
               |          [1, 2] <------------- curr_row 
               |                       |                              

Наконец, вы изменяете этот пустой список, добавляя в него новый индекс, потому что добавляете к нему список, на который ссылается curr_row:

g_list[row_key].append(curr_row)

который меняет ситуацию на:

+---------+          +---------+              +---------+
| Globals |    |     |  Heap   |       |      | Locals  |
+---------+    |     +---------+       |      +---------+
               |                       |                 
               |                       |                 
 g_list -------------> {1: <value>}    |                 
               |             |         |                 
               |             |         |                 
               |             v         |                 
               |           [<0>]       |                 
               |             |         |                 
               |             |         |                 
               |             v         |                 
               |          [1, 2] <------------- curr_row 
               |                       |                 
               |                       |                 

Обратите внимание, как диктуетonary value ссылается на список, который в свою очередь ссылается на список с 1 и 2 в нем. Следующая строка изменяет последний список, присваивая индексу 1:

curr_row[1] = 100

, изменяя ситуацию следующим образом:

+---------+          +---------+              +---------+
| Globals |    |     |  Heap   |       |      | Locals  |
+---------+    |     +---------+       |      +---------+
               |                       |                 
               |                       |                 
 g_list -------------> {1: <value>}    |                 
               |             |         |                 
               |             |         |                 
               |             v         |                 
               |           [<0>]       |                 
               |             |         |                 
               |             |         |                 
               |             v         |                 
               |         [1, 100]<------------- curr_row 
               |                       |                 
               |                       |                 

Наконец, функция завершается, удаляя все локальные имена:

+---------+          +---------+    
| Globals |    |     |  Heap   |    
+---------+    |     +---------+    
               |                    
               |                    
 g_list -------------> {1: <value>} 
               |             |      
               |             |      
               |             v      
               |           [<0>]    
               |             |      
               |             |      
               |             v      
               |         [1, 100]   
               |                    
               |                    

Список, который вы создали внутри функции и на который ссылается как curr_row , не удаляется , поскольку на него все еще есть ссылка из индекса 0 списка, который g_listсловарь ссылается как значение.

Нигде Python не должен делать копию списка (глубокую или нет) .

В качестве примечания: Вы неВ ваших функциях нужно использовать операторы global g_list. Это утверждение необходимо только в том случае, если вы хотите изменить то, с чем связано глобальное имя (метка). Вы устанавливаете g_list только один раз , привязывая эту метку к объекту словаря. С этого момента этот ярлык остается связанным с тем же словарем. Вам нужно использовать global <name>, только если вам нужно повторно привязать такое имя из функции, и только потому, что в противном случае Python обрабатывает имена, которые вы назначаете, как локальные. Поскольку в ваших функциях не делается попыток изменить то, на что ссылается g_list, вы можете безопасно удалить операторы global g_list из своего кода, не изменяя поведения кода.

2 голосов
/ 31 октября 2019

Python не имеет выделенного объема памяти, а имеет ограниченные ссылки.

Python управляет удалением объектов, которые больше не используются системой подсчета ссылок. Для каждого объекта интерпретатор отслеживает количество ссылок на этот объект. Ссылки могут быть именами, используемыми в вашем коде, или указателями, хранящимися в структурах данных:

a = object()  # 1 reference
b = [a]       # 2 references
a = 1         # Still one reference in the list
b = None      # No more references, so the garbage collector will destroy the object

Когда число ссылок на объект достигает нуля, интерпретатор знает, что объект больше не используется, и онможно собрать мусор. (Реальная реализация немного более сложна, чтобы иметь дело с такими вещами, как циклические ссылки).

Так что нередко объекты сохраняются за пределами области, в которой они созданы. (Язык / интерпретатор Python не применяет концепции владения объектами). Приведенная ниже функция создает 3 объекта и не уничтожает и не копирует ни один из них, даже если только один явно возвращается вызывающей стороне:

def f():
    return [[], []]

Итак, чтобы ответить на ваш вопрос напрямую:

Однако я не понимаю, как глобальная переменная g_list могла бы содержать значение локального выделенного списка (то есть curr_row) даже после того, как функция была возвращена.

Pythonне имеет локально распределенных списков, только локальные ссылки на списки. Глубокое копирование не выполняется, поскольку не ожидается, что завершение области действия функции освободит всю выделенную память.

0 голосов
/ 31 октября 2019

Если вы хотите сделать глубокую копию

Возможно, вас заинтересует модуль copy. Вот так:

from copy import deepcopy
list_b = deepcopy(list_a)

И у вас будут разные ссылки. Все остальное в python - это мелкие копии.

(ну, если вы сами не осуществите глубокое копирование ... но это не произойдет случайно.)

list.extend

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

g_list[row_key].append(curr_row[:])

, но работает следующий код:

g_list[row_key].append(curr_row)

Можеткто-нибудь дал мне какой-нибудь совет, когда произошла эта глубокая копия локального списка?

метод list.append() добавляет один элемент в список. list.extend() вместо этого ожидает итерацию (например, список или срез [:], равный единице) и добавляет все элементы в этой итерации к списку.

Так что же произошло DID?

Однако я не понимаю, как глобальная переменная g_list может содержать значение локального распределенного списка (то есть curr_row) даже после того, как функциявозвращено.

  1. На него есть 2 ссылки - одна локальная для функции, а другая внутри другого (глобального) списка.
  2. Затем функция заканчивается, и ссылка в глобальном списке все еще жива.
  3. Локальная ссылка на функцию удалена
  4. Но она все еще жива в другом спискепоэтому список никогда не удаляется.

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

a = "something"                       # Binds name "a" to string "something"
identity_of_a = id(a)                 # Binds name "identity_of_a" to the result of function id(a)
b = a                                 # Binds name "b" to string "something"
a = "something completely different"  # Binds name "a" to string "something completely different"
print(id(b) == identity_of_a)         # Compares the result of id(b) to the value bound to "identity_of_a", and prints the result.

ВСЕГДА печатает True. И обратите внимание, что ни в одной точке после начального присвоения со строкой «что-то» ничего не происходило, за исключением обновления значения пересчета.

...