python a, b = b, реализация? Как он отличается от функции подкачки C++?

Я столкнулся с этой проблемой, когда хочу попробовать версию python: https://leetcode.com/problems/first-missing-positive/discuss/17071/My-short-C++-solution-O(1)-space и-О (п) -time

Я не уверен, почему a[0], a[a[0]] = a[a[0]], a[0] это не делает обмен?

>>> nums
[2, 1, 0]
>>> a = [2,1,0]
>>> a[0], a[a[0]] = a[a[0]], a[0]
>>> a
[2, 1, 0]
>>> a[0]
2
>>> a[0],a[2] = a[2], a[0]
>>> a
[0, 1, 2]

Я предполагаю, что реализация синтаксиса a, b = b - это что-то вроде:

tmp = a[0] (tmp = 2)
a[0]  = a[a[0]] (a[0] = a[2] = 0)
a[a[0]] = tmp (a[a[0]] = a[0] = tmp = 2)

Затем я проверил реализацию функции swap в C++. Я ничего не знаю о C++, но похоже, что идея такая же: http://www.cplusplus.com/reference/algorithm/swap/

The behavior of these function templates is equivalent to:
template <class T> void swap (T& a, T& b)
{
  T c(std::move(a)); a=std::move(b); b=std::move(c);
}
template <class T, size_t N> void swap (T (&a)[N], T (&b)[N])
{
  for (size_t i = 0; i<N; ++i) swap (a[i],b[i]);
}

мы имеем c = a, тогда a = b и b = a Итак, почему функция C++ swap не имеет этой проблемы? И как написать такую функцию подкачки в питоническом режиме?

Ответы

Ответ 1

Чтобы понять это, вам нужно войти в реализацию, используя dis.

Сначала рассмотрим простую функцию подкачки:

from dis import dis

def swap(i, j):
    i, j = j, i

dis(swap)

Выходной байт-код:

4             0 LOAD_FAST                1 (j)
              2 LOAD_FAST                0 (i)
              4 ROT_TWO
              6 STORE_FAST               0 (i)
              8 STORE_FAST               1 (j)
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

Вы можете видеть, что есть ROT_TWO, что означает, что

Своп меняет два самых больших элемента стека.

Этот ROT_TWO несет ROT_TWO ответственность за обмен.

Теперь подошел к вашему вопросу:

Давайте возьмем пример, который работает:

from dis import dis

def swap():
    a = [2, 1]
    a[0], a[1] = a[1], a[0]

dis(swap)

Выходной байт-код:

  4           0 LOAD_CONST               1 (2)
              2 LOAD_CONST               2 (1)
              4 BUILD_LIST               2
              6 STORE_FAST               0 (a)

  5           8 LOAD_FAST                0 (a)
             10 LOAD_CONST               2 (1)
             12 BINARY_SUBSCR
             14 LOAD_FAST                0 (a)
             16 LOAD_CONST               3 (0)
             18 BINARY_SUBSCR
             20 ROT_TWO
             22 LOAD_FAST                0 (a)
             24 LOAD_CONST               3 (0)
             26 STORE_SUBSCR
             28 LOAD_FAST                0 (a)
             30 LOAD_CONST               2 (1)
             32 STORE_SUBSCR
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

Выходной байтовый код похож на то, что мы имеем, когда оно является простой функцией подкачки.

Но когда код изменен:

from dis import dis

def swap():
    a = [1, 0]
    a[0], a[a[0]] = a[a[0]], a[0]
dis(swap)

swap()

Выход

  4           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (0)
              4 BUILD_LIST               2
              6 STORE_FAST               0 (a)

  5           8 LOAD_FAST                0 (a)
             10 LOAD_FAST                0 (a)
             12 LOAD_CONST               2 (0)
             14 BINARY_SUBSCR
             16 BINARY_SUBSCR
             18 LOAD_FAST                0 (a)
             20 LOAD_CONST               2 (0)
             22 BINARY_SUBSCR
             24 ROT_TWO
             26 LOAD_FAST                0 (a)
             28 LOAD_CONST               2 (0)
             30 STORE_SUBSCR
             32 LOAD_FAST                0 (a)
             34 LOAD_FAST                0 (a)
             36 LOAD_CONST               2 (0)
             38 BINARY_SUBSCR
             40 STORE_SUBSCR
             42 LOAD_CONST               0 (None)
             44 RETURN_VALUE

Вы можете видеть выходной байтовый код, что два верхних элемента одинаковы. Следовательно, он не меняет местами

Ответ 2

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

a,b=b,a

Фактически, что делает Python, сначала он "подготавливает" значения правой стороны, создавая кортеж (b,a). Затем этот кортеж распаковывается и присваивается переменным в обратном порядке.

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

Чтобы разбить пример со ссылочными типами (списками), которые вы использовали:

a = [2,1,0]    
a[0], a[a[0]] = a[a[0]], a[0]
  1. a[a[0]] принимает значение из элемента a[0] (равного 2) списка a (значение 0).
  2. a[0] равно 2 поэтому созданный набор состоит из (0,2)
  3. Tuple (0,2) распаковывается и 0 заменяет 2 в списке (0-й элемент).
  4. Теперь a[a[0]] можно прочитать как: взять 0-й элемент списка a (который в настоящее время равен 0), а затем заменить значение в списке в этом месте на 2 из распаковки кортежа (теперь 0 заменяется на 2 - которые заставляют операцию выглядеть так, как будто она ничего не делает для списка).

Как было предложено в ответе от von Oak изменение порядка помогает, потому что шаг из пункта 4. выше не заменяет значение снова.

Ответ 3

Python и C++ - это разные языки с разными правилами. Это в значительной степени причина, по которой внешне подобные конструкции ведут себя по-разному на этих языках.

Вы не можете написать общий swap в Python, который будет работать со входом, например a[0], a[a[0]]. Это не является проблемой. Вы никогда не должны пытаться менять такие вещи на любом языке, чтобы избежать путаницы и улучшить свои шансы на будущую работу в качестве программиста.

Если вам абсолютно необходимо обменивать элементы массива, индексированные элементами одного и того же массива, вы можете сделать это так на Python:

p, q, a[p], a[q] = index0, index1, a[q], a[p]

где index0 и index1 может быть любое выражение, включающее a[i], a[a[i]], a[a[a[i]]] или что - либо подобное. Например

p, q,  a[p], a[q] = a[0], a[a[0]],  a[q], a[p]

работает.

Ответ 4

Легко думать об этом также только на бумаге (например, на собеседовании), и вам не нужно отлаживать или дизассемблировать код для байт-кода для понимания.

Я также думаю, что это не имеет ничего общего с реализацией функции swap в C++. Это не связанные вещи.

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

Представьте первый случай. У нас есть:

a = [2,1,0]

a[0], a[a[0]] = a[a[0]], a[0]

Когда мы начнем выполнять этот код, правая сторона сначала оценивает, поэтому мы будем иметь

a[0], a[a[0]] = a[a[0]], a[0]    # a[a[0]] == 0, a[0] == 2, a == [2, 1, 0]

С правой стороны мы имеем кортеж (0, 2) а a все еще [2, 1, 0]

Затем мы начинаем назначать левую часть выражения слева, поэтому к a[0] мы назначаем первый элемент из кортежа, который равен 0. Теперь у нас есть

a[0], a[a[0]] = (0, 2)   # a[0] == 0, a == [0, 1, 0]

И теперь мы выполняем последнюю часть задания, которая относится к a[a[0]] присваиванию 2. Но a[0] теперь 0, поэтому после редукции мы присваиваем значение a[0] 2. Поэтому значения после последнего присваивания

a[0], a[a[0]] = (0, 2)   # a[a[0]] == 2, a == [2, 1, 0]

Который, кажется, что ничего не изменилось, и значения не менять, но, как видно из выше был a [2,1,0], затем [0,1,0] и, наконец, снова [2,1,0]. Кажется, ничего не изменилось, и обмен не работает.

А теперь второй случай, когда мы только изменяем порядок переменных в выражении:

a = [2,1,0]

a[a[0]], a[0] = a[0], a[a[0]]

Когда мы начнем выполнять этот код, правая сторона сначала оценивает, поэтому мы будем иметь

a[a[0]], a[0] = a[0], a[a[0]]    # a[0] == 2, a[a[0]] == 0, a == [2, 1, 0]

Справа мы имеем кортеж (2, 0) а a все еще [2, 1, 0]

Затем мы начинаем присваивать левой стороне выражения слева, поэтому для a[a[0]] мы назначаем первый элемент из кортежа, который равен 2. a[0] равно 2, поэтому после сокращения мы присваиваем значение a[2] 2. Теперь у нас есть

a[a[0]], a[0] = (2, 0)   # a[a[0]] == 2, a == [2, 1, 2]

И теперь мы выполняем последнюю часть задания, которая назначается 0 a[0]. Поэтому значения после последнего присваивания

a[a[0]], a[0] = (2, 0)   # a[0] == 0, a == [0, 1, 2]

Теперь это работает так, как ожидалось.

Поэтому необходимо также подумать о порядке, когда у вас есть зависимые переменные в вашем выражении подкачки. В качестве зависимых переменных я имею в виду, что в первом случае мы имеем в левой части a[0], a[a[0]] что означает a[0] изменить его значение, а a[a[0]] использовать это измененное значение, что приводит к нежелательному поведению.

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