Pull to refresh

Comments 34

В прошлой статье вы исправили мутабельность/немутабельность на изменяемость/неизменяемость. Почему бы тогда слайс не исправить на срез? А слайсинг на срезинг
«Слайсы» на «срезы» переименовал, но «слайсинг» как обозначение процесса оставил, адекватного ему перевода, кроме как словосочетанием не вижу.
Внезапно, процесс тоже «срез»
Хорошо, «срез» так «срез» — везде поменял «слайсы» и «слайсинг» на «срезы»
Отглагольное существительное для глаголов «нарезать» (с обоими вариантами ударения) — очевидно, «нарезка».
В кулинарии, между прочим, вовсю используется :)
Вопрос в том, насколько термин является общепринятым в программировании в конкретном контексте.
Вот из-за таких неоднозначностей я лично всегда предпочитаю английский термин даже качественному его переводу на русский язык. Но судя по комментариям к этой и предыдущей статье, такой подход сообщество не разделяет, поэтому приходится искать компромиссы с русскими терминами.
Не, мне тоже нравится больше мутабельность и слайсинг. Я просто указал на нелогичность относительно прошлой статьи.
Хороший цикл статей. Читаю с удовольствием. Спасибо.
Было бы восхитительно, если была бы также приведена сложность алгоритма, где это уместно.
Как например,
l = [1, 2, 3, 4]  # List
l.pop(0)          # -> 1 | O(n)
l.pop()           # -> 4 | O(1)

d = deque(l)  # Double-linked list
d.popleft()   # -> 1 | O(1)
d.pop()       # -> 4 | O(1)

Получился бы отличный справочник.
В обсуждении первой части поднимался этот вопрос и были приведены ссылки на соответствующие материалы, которые я добавил в конец первой статьи.
Отличная статья!
К примеру с сортировкой словаря по значениям c lambda — напоминалка: при преобразовании результата работы sorted обратно в словарь сортировка теряется, поскольку словарь является коллекцией неиндексированной.
Добавил напоминание о неиндексированности словаря в начале подраздела «3.4 Особенности сортировки словаря»
Я бы ещё добавил про то, что сортировка устойчивая (и как этим пользоваться), а также как пользоваться параметром key.

Допустим, у нас есть список названий деталей и их стоимостей. Нам нужно отсортировать его сначала по названию деталей, а одинаковые детали по убыванию цены. Самая коротка реализация даст не совсем тот результат:
shop = [('каретка', 1200), ('шатун', 1000), ('седло', 300), 
        ('педаль', 100), ('седло', 1500), ('рама', 12000), 
        ('обод', 2000), ('шатун', 200), ('седло', 2700)]
shop.sort()
for det, price in shop:
    print('{:<10} цена: {:>5}р.'.format(det, price))
каретка    цена:  1200р.
обод       цена:  2000р.
педаль     цена:   100р.
рама       цена: 12000р.
седло      цена:   300р.
седло      цена:  1500р.
седло      цена:  2700р.
шатун      цена:   200р.
шатун      цена:  1000р.


Это можно исправить так:

def prepare_item(item):
    return (item[0], -item[1])

shop = [('каретка', 1200), ('шатун', 1000), ('седло', 300), 
        ('педаль', 100), ('седло', 1500), ('рама', 12000), 
        ('обод', 2000), ('шатун', 200), ('седло', 2700)]
shop.sort(key=prepare_item)
for det, price in shop:
    print('{:<10} цена: {:>5}р.'.format(det, price))
каретка    цена:  1200р.
обод       цена:  2000р.
педаль     цена:   100р.
рама       цена: 12000р.
седло      цена:  2700р.
седло      цена:  1500р.
седло      цена:   300р.
шатун      цена:  1000р.
шатун      цена:   200р.


Что здесь произошло? Перед тем, как сравнивать два элемента списка к ним применялась функция prepare_item, которая меняла знак у стоимости (функция применяется ровно по одному разу к каждому элементу. Здесь отличие от подхода с функцией-сравнивателем в python 2 или C++, которая вызывается столько раз, сколько выполняется сравнение). В результате при одинаковом первом значении сортировка по второму происходила в обратном порядке.

Ещё можно использовать лямбды или itemgetter'ы:
shop.sort(key=lambda x: (x[0], -x[1]))

my_list = [39, 12, 21, 77, 21, 51, 48, 21, 42, 76]
hacked_list = sorted(enumerate(my_list), key=lambda x:x[1])
# Или так
from operator import itemgetter
hacked_list = sorted(enumerate(my_list), key=itemgetter(1))


Ещё один способ хитрых сортировок

Допустим данные нужно отсортировать сначала по столбцу А по возрастанию, затем по столбцу Б по убыванию, и наконец по столбцу В снова по возрастанию. Если данные в столбце Б числовые, то при помощи подходящей функции в key можно поменять знак у элементов Б, что приведёт к необходимому результату. А если все данные текстовые? Тут есть такая возможность. Дело в том, что сортировка sort в Python устойчивая, то есть она не меняет порядок «одинаковых» элементов. Поэтому можно просто отсортировать три раза по разным ключам:
data.sort(key=lambda x: x['В'])
data.sort(key=lambda x: x['Б'], reverse=True)
data.sort(key=lambda x: x['А'])


PS. https://shapyto.ru/ — для совсем новичков, но с подкапотными подробностями, делал для коллег по работе;
А ещё есть божественный визуализатор от Philip Guo.
Ещё иллюстрация работы сортировки с key:
Код
class MyTuple(tuple):  # Поправим несколько стандартных методов
    def __new__(cls, *p):  # tuple неизменяемый, поэтому определяем __new__
        return super().__new__(cls, p)
    def __lt__(self, other):  # Будем делать принт при сравнениях. При сортировке используется только <=
        print('Сравниваем', self, 'c', other)
        return super().__lt__(other)  # Да, кстати, super() — это класс tuple
    def __str__(self):  # Меняем str, чтобы отличать от обычного кортежа
        return '<' + repr(self) + '>'

def inverse_price(item):
    res = MyTuple(item[0], -item[1])  # Делаем лжекортеж, чтобы отслеживать факты сравнений
    print('Функцию inverse_price вызвали с параметром', item, 'Мы вернули', res)
    return res

shop = [('каретка', 1200), ('шатун', 1000), ('седло', 300),
        ('педаль', 100), ('седло', 1500), ('рама', 12000),
        ('обод', 2000), ('шатун', 200), ('седло', 2700)]

shop.sort(key=inverse_price)


Результат
Функцию inverse_price вызвали с параметром ('каретка', 1200) Мы вернули <('каретка', -1200)>
Функцию inverse_price вызвали с параметром ('шатун', 1000) Мы вернули <('шатун', -1000)>
Функцию inverse_price вызвали с параметром ('седло', 300) Мы вернули <('седло', -300)>
Функцию inverse_price вызвали с параметром ('педаль', 100) Мы вернули <('педаль', -100)>
Функцию inverse_price вызвали с параметром ('седло', 1500) Мы вернули <('седло', -1500)>
Функцию inverse_price вызвали с параметром ('рама', 12000) Мы вернули <('рама', -12000)>
Функцию inverse_price вызвали с параметром ('обод', 2000) Мы вернули <('обод', -2000)>
Функцию inverse_price вызвали с параметром ('шатун', 200) Мы вернули <('шатун', -200)>
Функцию inverse_price вызвали с параметром ('седло', 2700) Мы вернули <('седло', -2700)>
Сравниваем <('шатун', -1000)> c <('каретка', -1200)>
Сравниваем <('седло', -300)> c <('шатун', -1000)>
Сравниваем <('седло', -300)> c <('шатун', -1000)>
Сравниваем <('седло', -300)> c <('каретка', -1200)>
Сравниваем <('педаль', -100)> c <('седло', -300)>
Сравниваем <('педаль', -100)> c <('каретка', -1200)>
Сравниваем <('седло', -1500)> c <('седло', -300)>
Сравниваем <('седло', -1500)> c <('педаль', -100)>
Сравниваем <('рама', -12000)> c <('седло', -1500)>
Сравниваем <('рама', -12000)> c <('педаль', -100)>
Сравниваем <('обод', -2000)> c <('седло', -1500)>
Сравниваем <('обод', -2000)> c <('педаль', -100)>
Сравниваем <('обод', -2000)> c <('каретка', -1200)>
Сравниваем <('шатун', -200)> c <('рама', -12000)>
Сравниваем <('шатун', -200)> c <('седло', -300)>
Сравниваем <('шатун', -200)> c <('шатун', -1000)>
Сравниваем <('седло', -2700)> c <('седло', -1500)>
Сравниваем <('седло', -2700)> c <('педаль', -100)>
Сравниваем <('седло', -2700)> c <('рама', -12000)>

Огромное спасибо за Ваши очень ценные дополнения в комментариях к моим статьям!
Добавил основную часть информации из Вашего комментария в конце статьи с указанием Вашего авторства.

Здесь отличие от подхода с функцией-сравнивателем в python 2 или C++, которая вызывается столько раз, сколько выполняется сравнение

От С++ и я и тема статьи далеки, а вот касательно Python 2, не могли бы Вы уточнить, что именно имеется в виду? Я поверял Ваш код в Python 2.7 и Python 3.5 — он дает одинаковый результат.
> Математически это можно было бы записать как [start, stop)

интересно почему так, странный подход, ИМХО логичнее [start, stop]
Наверно потому что
a[x: z] = a[x: y] + a[y: z]
Забыл написать, что статья очень хорошая, спасибо автору!
В данном случае номер элемента y является разделителем на две части: «от x до y» и «от y до z».
То есть, элемент с номером y должен войти только в одну из частей, иначе a[x: y] + a[y: z] не будет равно a[x: z].

Объединить в непрерывную последовательность можно только лучи, но не отрезки, иначе будет дублирование концов.
Поэтому выбрана такая нотация — в силу того, что последовательность номеров элементов непрерывна, и мы знаем, что после x-го элемента обязательно должен идти x+1-й.

В других случаях такой гарантии нет, и там используется нотация [start, stop], потому что мы можем не знать следующий после stop индекс.
Такая ситуация, например, в индексировании Series и Dataframe в pandas.
Ну некоторая логика в этом есть, хотя подозреваю что такое применение сильно реже приходится использовать чем запись a[x:y+1]
Жму руку, вот это я и хотел узнать
Мне кажется, что в срезах еще одного параметра не хватает.
Есть начало, конец, шаг и неплохо было бы добавить что-то типа «длины выборки (length)».
То есть задавать срез в виде [start: stop: step: length].
Это удобно для таких задач как «выбрать по n через m» («бригада 3 дня работает, два отдыхает — выбрать рабочие дни месяца»).
Для приведенной в статье последовательности 'abcdefg' срез [::: 2] даст (дал бы) 'abdeg', а срез [:: 2: 2] — 'abef'.
Слишком неочевидная логика получается, даже приведенный Вами в конце пример мне не понятен — почему должны получится именно эти последовательности? Что вообще будет значить длина выборки, если у нас start: stop: step дефолтные, то есть последовательность целиком?
Если фильтрация такая комплексная, ее можно сделать проходом в цикле со сложным набором условий — это будет понятней выглядеть, чем такой синтаксис.
Да, согласен с тем, что данное предложение надо тщательнее проработать. И наверное, правильнее все-таки рассматривать step как фиксированную длину цикла, на которую добавляемый параметр не влияет (выше я рассматривал другой вариант — там длина цикла складывалась как step+length, — наверное, это действительно менее ясно и гибко).

В текущей реализации (три параметра start: stop: step) подразумевается, что длина выборки всегда единица (как и шаг). То есть мы перебираем индекс от начала (start) до конца (stop) с шагом (step) и возвращаем всегда один элемент, на который указывает индекс. Этот элемент вставляется в возвращаемую последовательность.

Предложение в том, чтобы добавить еще один параметр, который даст возможность регулировать — что именно надо возвращать. Самый простой (дубовый) вариант — четвертым параметром просто указываем длину возвращаемой выборки на каждом шаге.
Например, запись [::7:5] означает, что при шаге (цикле) 7 надо возвращать 5 элементов — (рабочие дни недели). А
Если длина выборки больше, чем остаток — возвращаем остаток.
При таком варианте длина возвращаемой выборки может быть больше чем цикл. Для последовательности 'abcdefg' срез [::1:2] даст 'abbccddeeffgg'.

Но и данный вариант не самый гибкий. То есть он не позволяет задать выборку, например, такого типа — «выбрать числа, на которые приходятся в данном месяце понедельник, среда, пятница». То есть когда выборка сама имеет структуру.
Поэтому наверное, самым правильным будет в качестве 4-го параметра указывать другой срез (получаем срез внутри среза). Не запутал окончательно? ).

Первый срез определяет цикл выборки, а второй работает внутри данного цикла.
Тогда указанный выше выбор 1-го, 3-го и 5-го элемента недели можно записать так:
[:: 7: [:6:2] ]. То есть мы указываем шаг 7 (длина недели), и срез внутри недели — [:6:2] — от первого до 6-го элементов выбери через 1.

Данный вариант тоже много вопросов вызовет (например, может ли быть индекс внутреннего среза больше чем шаг внешнего). Но такая запись, наверное, покроет все возможные варианты срезов.
Если даже сама постановка вопроса не всегда понятна и трактуется однозначно, значит задачу стоит разделить на несколько более простых и понятных этапов. Собственно подобный принцип стремления к понятности кода лежит в основе философии Python.
 # Формируем список дней от 1 до 31 с которым будем работать
days = [d for d in range(1, 32)]   

# Делим список дней на недели
weeks = [days[i:i+7] for i in range(0, len(days), 7)]   
print(weeks)   # [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14], [15, 16, 17, 18, 19, 20, 21], [22, 23, 24, 25, 26, 27, 28], [29, 30, 31]]

# Выбираем в каждой неделе только первые 5 рабочих дней, отбрасывая остальные
work_weeks = [week[0:5] for week in weeks]   
print(work_weeks)   # [[1, 2, 3, 4, 5], [8, 9, 10, 11, 12], [15, 16, 17, 18, 19], [22, 23, 24, 25, 26], [29, 30, 31]]

# Если нужно одним списком дней - можно объединить
wdays = [item for sublist in work_weeks for item in sublist]
print(wdays)   # [1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 29, 30, 31]

Вообще, большое спасибо за поднятый вопрос — я как раз сейчас пишу статью по генерации списков (планирую опубликовать до конца месяца), и вот этот пример с рабочими днями очень показателен, обязательно его туда добавлю!
Кстати, можно убрать выходные еще более изящно, используя индексы
# Формируем список дней от 1 до 31 с которым будем работать
days = [d for d in range(1, 32)] ]

wdays6 = [wd for (i, wd) in enumerate(days, 1) if i % 7 != 0]  # Удаляем каждый 7-й день
# Удаляем каждый 6 день в оставшихся после первого удаления:
wdays5 = [wd for (i, wd) in enumerate(wdays6, 1) if i % 6 != 0]  

print(wdays5)
# [1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 29, 30, 31]

Обратите внимание, что просто объединить два условия в одном if не получится, как минимум потому, что 12-й день делится на 6, но не выпадает на последний 2 дня недели!
Неплохо, да. Хотя я бы наверное по другому сделал, но не суть.
Понятно, что задачу произвольной выборки элементов последовательности в принципе можно решить многими способами различной степени изящности.

Просто когда я просматривал статью и увидел табличку с примерами работы срезов, то вспомнил про «периодические выборки» (сталкивался с ними) и понял, что текущими срезами Питона их не получить. Вот и возникло предложение про 4-й параметр срезов.
ИМХО, срезы в принципе не задумывались для сложных комплексных выборок с условиями — для этих целей существуют генераторы выражений (list comprehensions). Вот там намного больше простора для творчества и гибких изящных решений. А там, где задача настолько сложна, что не хватает и генераторов — уже нужно писать циклы с ветвлением условий, чтобы не получился сверхкомпактный «write only» код.

В следующей моей статье из этого цикла как-раз будут рассматриваться генераторы выражений, и там в процессе написания уже встает много очень интересных применений.
Если есть еще какие интересные идеи или задачи на эту тему — пишите в личку, могу попробовать осветить их в следующей статье.
Если бы разработчики питона остановились на двух параметрах среза (start:stop), то и вопросов бы не возникло. В этом случае мы действительно имеем дело с «чистыми срезами».
Фактически такой срез является интервалом с двумя границами. Причем в общем случае границы необязательно должны быть целыми числами (для срезов произвольных интервалов).

Но третий параметр (step) выводит нас из зоны срезов в зону периодических выборок. Это еще не произвольные выборки, но уже и не срезы.
И именно поэтому имело бы смысл довести дело до конца, то есть сделать такие периодические выборки универсальными, введя 4-й параметр (характеристику выборки), который в свою очередь тоже может быть срезом. Тогда все замыкается.

Это не универсальные выборки по любому критерию, а именно периодические.
В любом случае, конечно, их можно реализовать «руками».
Разница в сложности понимания концепции — текущий step — не вызывает неоднозначностей — есть диапазон по start и stop и есть частота выбора из диапазона, плюс, наличие шага позволяет его делать отрицательным и разворачивать направление выборки.
Таким образом третий параметр шага прост и очевиден для понимания, плюс имеет кучу реальных применений в распространенных задачах, которые оправдывают его наличие.

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

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

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

К тому же тут не просто функционал, тут еще и рекурсивное замыкание функционала. Это всегда красиво, но не всегда реализуемо.

Сорри, но я вроде бы все уже сказал и больше не буду эту ветку поддерживать. Спасибо ).
И отдельное спасибо за статьи, конечно ).
Не нужны параметры периодической выборки — не задавайте, — работайте как обычно. А зачем лишать возможно полезного функционала тех, кому он мог бы пригодиться?

Ага, а потом нужно чужой код подправить, открываешь, а там трехэтажные конструкции цепочкой выстроенные со всеми возможными параметрами…
Не, хорошо читаемый язык (а это есть в философии Python) не должен позволять стрелять в ногу из зенитки — за то и люблю Python, что тут PEP 8 даже правильность отступов определяет.

PS: Спасибо за интересную дискуссию и хороший пример для следующей статьи.
Sign up to leave a comment.

Articles