Python

индекс
250,37

Python Tips, Tricks, and Hacks (часть 2)

Содержание

Списки. Свёртка списка (reduce). Прохождение по списку (range, xrange и enumerate). Проверка всех элементов списка на выполнение условия (all и any). Группировка элементов нескольких списков (zip). Еще несколько операторов для работы со списками. Продвинутые логические операции с типом set.
Словари. Создание словаря с помощью именованных аргументов. Преобразование словаря в список и обратно. «Dictionary Comprehensions».

2   Списки


2.2   Свёртка списка

К сожалению, нельзя написать программу только с помощью генераторов списков. (Я шучу… конечно, можно.) Они могут отображать и фильтровать, но нет простого способа для свертки списка. Под этим понятием я подразумеваю применение функции к первым двум элементам списка, а затем к получившемуся результату и следующему элементу, и так до конца списка. Можно реализовать это через цикл for:
Copy Source | Copy HTML
  1. numbers = [1,2,3,4,5]
  2. result = 1
  3. for number in numbers:
  4.     result *= number
  5. # result = 120 

А можно воспользоваться встроенной функцией reduce, принимающей в качестве аргументов функцию от двух параметров и список:
Copy Source | Copy HTML
  1. numbers = [1,2,3,4,5]
  2. result = reduce(lambda a,b: a*b, numbers)
  3. # result = 120 

Не так красиво, как генераторы списков, но короче обычного цикла. Стоит запомнить этот способ.

2.3   Прохождение по списку: range, xrange и enumerate

Помните, как в языке C для цикла for вы использовали переменную-счетчик вместо элементов списка? Возможно, вы уже знаете, как имитировать это поведение в Python с помощью range и xrange. Передавая число value функции range, мы получим список, содержащий элементы от 0 до value-1 включительно. Другими словами, range возвращает индексы списка указанной длины. xrange действует похоже, но более эффективно, не загружая весь список в память целиком.
Copy Source | Copy HTML
  1. strings = ['a', 'b', 'c', 'd', 'e']
  2. for index in xrange(len(strings)):
  3.     print index,
  4. # печатает "0 1 2 3 4" 

Проблема в том, что обычно вам всё равно нужны элементы списка. Что толку от индексов без них? В Python есть потрясающая встроенная функция enumerate, которая воозвращает итератор для пар индекс → значение:
Copy Source | Copy HTML
  1. strings = ['a', 'b', 'c', 'd', 'e']
  2. for index, string in enumerate(strings):
  3.     print index, string,
  4. # печатает "0 a 1 b 2 c 3 d 4 e" 

Еще один плюс состоит в том, что enumerate выглядит более читаемо, чем xrange(len()). Поэтому range и xrange полезны, наверно, только для создания списка с нуля, а не на основе других данных.

2.4   Проверка всех элементов списка на выполнение условия

Допустим, нам надо проверить, выполняется ли условие хотя бы для одного элемента. До Python 2.5 можно было писать так:
Copy Source | Copy HTML
  1. numbers = [1,10,100,1000,10000]
  2. if [number for number in numbers if number < 10]:
  3.     print 'At least one element is over 10'
  4. # Результат: "At least one element is over 10" 

Если ни один из элементов не удовлетворяет условию, генератор создаст пустой список, который считается false. В противном случае будет создан непустой список, который приводится к true. Строго говоря, не нужно проверять все элементы, достаточно остановиться после первого элемента, удовлетворающего условию. Поэтому предыдущий пример менее эффективен и может быть выбран только в том случае, если вы не можете использовать Python 2.5, но хотите уместить всю логику в одно выражение.

С помощью встроенной функции any, введенной в Python 2.5, вы можете решить эту задачу более красиво и эффективно. Функция any прервется и вернет True, как только найдет элемент, удовлетворяющий условию. Ниже я использую выражение-генератор, которое возвращает True или False для каждого элемента, и передаю его функции any. Генератор вычисляет только необходимые в данный момент значения, а any принимает их по очереди.
Copy Source | Copy HTML
  1. numbers = [1,10,100,1000,10000]
  2. if any(number < 10 for number in numbers):
  3.     print 'Success'
  4. # Результат: "Success!" 

Аналогично, может возникнуть задача проверки, что все элементы удовлетворяют условию. Без Python 2.5 придется писать так:
Copy Source | Copy HTML
  1. numbers = [1,2,3,4,5,6,7,8,9]
  2. if len(numbers) == len([number for number in numbers if number < 10]):
  3.     print 'Success!'
  4. # Результат: "Success!" 

Здесь мы фильтруем список и проверяем, уменьшилась ли его длина. Если нет, то все его элементы удовлетворяют условию. Опять же, без Python 2.5 это единственный способ уместить всю логику в одно выражение.

В Python 2.5 есть более простой путь — встроенная функция all. Легко догадаться, что она прекращает проверку после первого элемента, не удовлетворяющего условию. Эта функция работает абсолютно аналогично предыдущей.
Copy Source | Copy HTML
  1. numbers = [1,2,3,4,5,6,7,8,9]
  2. if all(number < 10 for number in numbers):
  3.     print 'Success!'
  4. # Результат: "Success!" 

2.5   Группировка элементов нескольких списков

Встроенная функция zip используется для сжимания нескольких списков в один. Она возвращает массив кортежей, причем n-й кортеж содержит n-е элементы всех массивов, переданных в качестве аргументов. Это тот случай, когда пример — лучшее объяснение:
Copy Source | Copy HTML
  1. letters = ['a', 'b', 'c']
  2. numbers = [1, 2, 3]
  3. squares = [1, 4, 9]
  4.  
  5. zipped_list = zip(letters, numbers, squares)
  6. # zipped_list = [('a', 1, 1), ('b', 2, 4), ('c', 3, 9)] 

Эта вещь часто используется как итератор для цикла for, извлекающая три значения за одну итерацию («for letter, number, squares in zipped_list»).

2.6   Еще несколько операторов для работы со списками

Ниже перечислены встроенные функции, в качестве аргумента принимающие любой итерируемый объект.
max и min возвращают наибольший и наименьший элемент соответственно.
sum возвращает сумму всех элементов списка. Опциональный второй параметр задает начальную сумму (по умолчанию 0).

2.7   Продвинутые логические операции с типом set.

Я понимаю, что в разделе, посвященном спискам, не положено говорить о сетах (sets). Но пока я не использовал их, мне не хватало некоторых логических операций в списках. Сет отличается от списка тем, что его элементы уникальны и неупорядоченны. Над сетами также можно выполнять множество логических операций.

Довольно распространенная задача — убедиться, что элементы списка уникальны. Это просто, достаточно преобразовать его в сет и проверить, изменилась ли длина:
Copy Source | Copy HTML
  1. numbers = [1,2,3,3,4,1]
  2. set(numbers)
  3. # возвращает set([1,2,3,4])
  4.  
  5. if len(numbers) == len(set(numbers)):
  6.     print 'List is unique!'
  7. # не выводит ничего 

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

3   Словари


3.1   Создание словаря с помощью именованных аргументов

Когда я начал изучать Python, я полностью пропустил альтернативный способ создания словаря. Если передать конструктору dict именованные аргументы, они будут добавлены в возращаемый словарь. Конечно, на его ключи накладываются те же ограничения, что и на имена переменных. Вот пример:
Copy Source | Copy HTML
  1. dict(a=1, b=2, c=3)
  2. # возвращает {'a': 1, 'b': 2, 'c': 3} 

Этот способ немного очищает код, избавляя вас от летающих вокруг кавычек. Я часто использую его.

3.2   Преобразование словаря в список

Чтобы получить список ключей, достаточно привести словарь к типу list. Но лучше использовать .keys() для получения списка ключей или .iterkeys() для получения итератора. Если вам нужны значения, используйте .values() и .itervalues(). Но помните, что словари не упорядочены, поэтому полученные значения могут быть перемешаны любым мыслимым образом.

Чтобы получить и ключи, и значения в виде списка кортежей, можно использовать .items() или .iteritems(). Возможно, вы часто пользовались этим захватывающим методом:
Copy Source | Copy HTML
  1. dictionary = {'a': 1, 'b': 2, 'c': 3}
  2. dict_as_list = dictionary.items()
  3. #dict_as_list = [('a', 1), ('b', 2), ('c', 3)] 


3.3   Преобразование списка в словарь

Обратная операция — превращение списка, содержащего пары ключ-значение, в словарь — делается так же просто:
Copy Source | Copy HTML
  1. dict_as_list = [['a', 1], ['b', 2], ['c', 3]]
  2. dictionary = dict(dict_as_list)
  3. # dictionary = {'a': 1, 'b': 2, 'c': 3} 

Вы можете комбинировать методы, добавив именованные аргументы:
Copy Source | Copy HTML
  1. dict_as_list = [['a', 1], ['b', 2], ['c', 3]]
  2. dictionary = dict(dict_as_list, d=4, e=5)
  3. # dictionary = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} 

Превращать списка и словари друг в друга довольно удобно. Но следующий совет просто потрясающий.

3.3   «Dictionary Comprehensions»

Хотя в Python нет встроенного генератора словарей, можно сделать нечто похожее ценой небольшого беспорядка в коде. Используем .iteritems() для превращения словаря в список, передадим его выражению-генератору или генератору списков, а затем преобразуем список обратно в словарь.

Допустим, у нас есть словарь пар name:email, а мы хотим получить словарь пар name:is_email_at_a_dot_com (проверить каждый адрес на вхождение подстроки .com):
Copy Source | Copy HTML
  1. emails = {'Dick': 'bob@example.com', 'Jane': 'jane@example.com', 'Stou': 'stou@example.net'}
  2.  
  3. email_at_dotcom = dict( [name, '.com' in email] for name, email in emails.iteritems() )
  4.  
  5. # email_at_dotcom = {'Dick': True, 'Jane': True, 'Stou': False} 

Конечно, необязательно начинать и заканчивать словарем, можно в некоторых местах использовать и списки.

Это чуть менее читабельно, чем строгие генераторы списков, но я считаю, что это лучше, чем большой цикл for.

Статья целиком в PDF
+63
25 февраля 2010, 12:51
176

комментарии (28)

–18
FuN_ViT #
такое ощущение, что все эти трюки сделали для того, что бы не путаться с отступами :-)
+2
ilvar #
В 3.2 для значений не повтор .items(), а .values()
0
Riateche #
Исправил, спасибо.
+3
iOrange #
Спасибо, занес в избранное. Еще пару статей — распечатаю и подошью :)
+6
Dair_Targ #
Спасибо за any, all & enumerate — как-то пропустил.
0
evilkost #
reduce убрали в 3 питоне из глобального пространства имен. Имхо правильно: лучше лишнию строку написать, чем при чтении задумываться.
+1
mcrey #
Cапасибо за Ваши статьи.
Как раз начал изучать Python…
+2
voiza #
Set по-русски это множество. Математически не совсем точно, но все же.
0
retif #
«Это чуть менее читабельно, чем строгие генераторы списков, но я считаю, что это лучше, чем большой цикл for.»

лучше чем?
+7
Ferroman #
Чем большой цикл for.
Так то!
–2
sir_Jack #
Большое спасибо, изучать тонкости питона по Вашим статьям намного интереснее, чем по книгам))
+1
anthonio #
Продолжение следует.

Вот это радует, да! :)
И спасибо Вам.
+1
Volshebnyi #
Не отказался бы почитать про lambda-функции поподробнее, ну и про «накоротко замкнутые» выражения.
Иначе сам напишу =)
+2
Imposeren #
а в 3 есть нормальные Dictionary Comprehensions {i: chr(65+i) for i in range(4)}
reduce() ушел в модуль functools
xrange() стал просто range()
dict.keys(), dict.items(), dict.values() теперь возвращают не список, а (???)вид (view).

И вообще, когда мир решит, что стоит писать «гайды» уже для python 3.*?
0
joymax #
наверно когда портируют большую часть фреймворков и основных библиотек, типа джанго и пайлонс. Судя по pylonshq.com/project/pylonshq/ticket/425 по большей части коммюнити пока срать на 3
0
Davidov #
Если верить истории коммитов, разработка ведётся довольно активно.
0
Davidov #
Хотя, похоже, меня ввели в заблуждение коммиты из djagno-trunk.
0
helions8 #
С PyGTK тоже все не весело — https://bugzilla.gnome.org/show_bug.cgi?id=566641, хотя Qt4 есть для 3.1
0
iDemy #
Спасибо за «3.1 Создание словаря с помощью именованных аргументов»
0
kronos #
А вот reduce для пустого элемента вернет ошибку, что не очень удобно. Если бы он мог принимать помимо лямбды и собственно массива еще «значение» по умолчанию, такой проблемы бы не было. Жду следующей статьи.
Пока для себя уяснил, что ruby хоть и медленней, но такие вещи как any, all, проход по массиву, reduce сделаны гораздо красивее.
0
kmike #
Гвидо ФП не очень жалует, хотел из py3k чуть ли не map выкинуть, т.к. считает, что практически все эти трюки скорее снижают понятность кода, а не повышают ее. Из-за этого же лямбды такие урезанные, чтобы разработчики писали не «крутой» код, а понятный код.

Пописал месяц на js, где ФП-составляющая не урезанная, захотелось после этого попробовать ruby. Но вместе с тем осознаю, что в питоне из-за всех этих ограничений код и правда проще/понятнее получается, в них, определенно, есть смысл.
+1
Yngvie #
а я вот зашел на docs.python.org и прочитал про reduce:
reduce(function, iterable[, initializer])
...If the optional initializer is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty…

Что означает: Если не обязательный параметр initializer присутствует, то он подставляется в вычислениях перед элементами iterable, а также служит значением по умолчанию, если iterable пуст
0
DkZ #
Ну собственно трюки довольно знакомые среднестатистическому питонщику, однако спасибо, что выложили всё это в одном месте.
+1
xni #
А у меня завтра собеседование по поводу python-разработки! Прочитал, немного порадовался с той стороны, что всё это знаю, а с другой огорчился, что не знаю как сделать более глубокое погружение в язык.
+1
Yngvie #
А Вы уже смотрели эту статью?
0
Yngvie #
Спасибо за статью, я как то all и any упустил.
Я бы добавил, что в enumerate появился параметр start, так что первый индекс может отличаться от нуля.
И, думаю, раз уже начали рассказывать разные вкусности по работе со списками, то следует затронуть модуль itertools
0
vak #
может кого-то спасёт:

использование iteritems/itervalues/iterkeys (вместо их неитерационных аналогов) могут значительно ускорить работу в CPython.

использование items/values/keys порождает новые структуры и (часто) увеличивает ref counts на элементы материнского словарика. В результате может образоваться плохо воспроизводимое замедление, у меня бывало замедление более 10раз.
0
shvechikov #
Имитируя dictionary comprehensions более эффективно использовать tuple вместо list для определения пары (key, value).

Т.е. вместо:

dict([name, '.com' in email] for name, email in emails.iteritems())

надо:

dict((name, '.com' in email) for name, email in emails.iteritems())

А ещё нагляднее:

dict((name, '.com' in email) for (name, email) in emails.iteritems())

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