Ошибка списка Python: [:: - 1] шаг на [: -1] срез

Мне показалось, что я понял основы перебора списков в python, но получал непредвиденную ошибку при использовании отрицательного шага на срезе следующим образом:

>>> a = list(range(10))
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:-1:-1]
[]

(Обратите внимание, что это выполняется в Python 3.5)

Почему не делается обратный шаг [: - 1: -1] через срез [: - 1] таким же образом, как и весь список с помощью [:: - 1]?

Я понимаю, что вы также можете использовать list.reverse(), но лучше понять базовую функциональность среза python.

Ответы

Ответ 1

Первый -1 в a[:-1:-1] не означает, что вы думаете.

В разрезе отрицательные начальные/конечные индексы не интерпретируются буквально. Вместо этого они используются для удобного обращения к концу списка (т.е. Относятся к len(a)). Это происходит независимо от направления нарезки.

Это означает, что

a[:-1:-1]

эквивалентно

a[:len(a)-1:-1]

При опускании во время реверсивного среза начальный индекс по умолчанию равен len(a)-1, что делает эквивалентным значение

a[len(a)-1:len(a)-1:-1]

Это всегда дает пустой список, так как начальный и конечный индексы являются одинаковыми, а конечный индекс является исключительным.

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

>>> a[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:None:-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> a[:-len(a)-1:-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Ответ 2

При вводе [1, 2, 3, ...][1:4:1] он совпадает с [1, 2, 3, ...][slice(1, 4, 1)]. Таким образом, 1:4:1 является сокращением для объекта slice. slice подпись slice(stop) или slice(start, stop[, step]), и вы также можете использовать None для аргументов.

:: -> slice(None, None, None)
:4 -> slice(4)
# and so on

Предположим, что мы получили [a: b: c]. Правила для индексов будут следующими:

  • Проверяется первый c. Значение по умолчанию +1, знак c указывает направление вперед или назад. Абсолютное значение c указывает размер шага.
  • Проверяется a. Когда c является положительным или None, значение по умолчанию для a равно 0. Если c отрицательный, по умолчанию для a есть -1.
  • Наконец b проверяется. Если c положительный или None, значение по умолчанию для b равно len. Если c является отрицательным значением по умолчанию для b является -(len+1).

Примечание 1. Вырожденные фрагменты в Python обрабатываются изящно:

  • слишком большой или слишком маленький индекс заменяется на len или 0.
  • верхняя граница, меньшая нижней границы, возвращает пустой список или строку или что-то еще (для положительного c).

Примечание 2. Грубо говоря, Python берет элементы, а это условие (a < b) if (c > 0) else (a > b) равно True (обновление a += c на каждом шаге). Кроме того, все отрицательные индексы заменяются на len - index.

Если вы объедините эти правила и примечания, будет понятно, почему вы получили пустой список. В вашем случае:

 In[1]: [1, 2, 3, 4, 5, 6][:-1:-1]        # `c` is negative so `a` is -1 and `b` is -1
Out[1]: [] 

# it is the same as:

 In[2]: [1, 2, 3, 4, 5, 6][-1: -1: -1]    # which will produce you an empty list 
Out[2]: [] 

Существует очень хорошая дискуссия о нотации фрагмента: Объяснить нотацию фрагмента Python!

Ответ 3

Мне обычно полезно нарезать a range -объект (это возможно только в python3 - в python2 range выдает list и xrange нельзя отрезать), если мне нужно увидеть, какой индексы используются для списка заданной длины:

>>> range(10)[::-1]  
range(9, -1, -1)

>>> range(10)[:-1]  
range(0, 9)

И в вашем последнем случае:

>>> range(10)[:-1:-1]
range(9, 9, -1)

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

Обратите внимание, что индексирование также может применяться последовательно:

>>> list(range(10))[::-1][:-1]  # first reverse then exclude last item.
[9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> list(range(10))[:-1][::-1]  # other way around
[8, 7, 6, 5, 4, 3, 2, 1, 0]

Ответ 4

slice работает аналогично range тем, что, когда вы делаете аргумент step отрицательным числом, аргументы start и stop работают в противоположном направлении.

>>> list(range(9, -1, -1)) == a[::-1]
True

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

>>> a[6:2:-2]
[6, 4]
>>> a[0:None:1] == a[::]
True
>>> a[-1:None:-1] == a[::-1]
True
>>> a[-2:None:-1] == a[:-1][::-1]
True

Ответ 5

Сначала фрагменты Python кажутся довольно простыми, но их поведение на самом деле довольно сложное (примечания 3 и 5 здесь актуальны). Если у вас есть фрагмент a[i:j:k]:

  • Если i или j являются отрицательными, они ссылаются на индекс с конца a (поэтому a[-1] ссылается на последний элемент a)
  • Если i или j не указаны или являются None, по умолчанию они заканчиваются концами a, но заканчивается, зависит от знака k:

    • Если k положителен, вы нарезаете вперед, поэтому i становится 0 и j становится len(a)
    • если k отрицательный, вы отбрасываете назад, поэтому i становится len(a), а j становится элементом до начала a.

      NB: j не может быть заменен на -1, так как это приведет к тому, что Python будет обрабатывать j как последний элемент a, а не (несуществующий) элемент до a[0]. Чтобы получить желаемое поведение, вы должны использовать -len(a)-1 (или -(len(a)+1)) вместо j, что означает, что для перехода к a[j], срез начинается с последнего элемента a, идет влево для len(a), а затем оставил еще один элемент, заканчивающийся до начала a и включающий a[0] в срез.

Следовательно, a[:-1:-1] означает "перейти от конца a, который равен a[-1] (так как i не указан и k отрицательный), последнему элементу a (так как j == -1), размер шага -1". i и j равны - вы начинаете и останавливаете нарезку в одном и том же месте - поэтому выражение оценивается как пустой список.

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

>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[-2::-1]
[8, 7, 6, 5, 4, 3, 2, 1, 0]