Python: советы, уловки, хаки (часть 1)

http://www.siafoo.net/article/52
  • Перевод
Предлагаю читателям «Хабрахабра» перевод статьи «Python Tips, Tricks, and Hacks». Статья будет полезна на начальном и среднем этапах изучения Python.

Хотите писать более лаконичный и читаемый код? Вы хотите уместить как можно больше смысла в одно выражение? Считаете, что прочитать о нескольких уловках лучше, чем провести остаток жизни за чтением документации? Вы обратились по адресу. Мы начнем с маленьких уловок, которые вы уже могли встретить, если немного работали с Python. Но я обещаю, что ближе к концу статьи вас ожидает больше безумных вещей.

Содержание

1. Маленькие уловки. Четыре типа кавычек. Правдивость различных объектов. Проверка на вхождение подстроки. Красивый вывод списка. Целочисленное деление и деление с плавающей точкой. Лямбда-функции.
2. Списки. Генераторы списков и выражения-генераторы.

Я старался сделать, чтобы все фрагменты кода запускались без дополнительных изменений. Если хотите, можете скопировать их в оболочку Python и посмотреть, что получится. Обратите внимание, что многие примеры содержат «неправильные» фрагменты, которые закомментированы. Ничто вам не мешает раскомментировать строку и посмотреть, что произойдет.

Небольшое разграничение между true и True в этой статье: когда я говорю, что объект true, это значит, что будучи приведенным к типу boolean, он становится True. Аналогично с false и False.

1   Маленькие уловки


1.1   Четыре типа кавычек

Начнем с того, что вы, возможно, уже знаете. В некоторых языках программирования одинарные и двойные кавычки предназначены для разных вещей. Python позволяет использовать оба варианта (но строка должна начинаться и заканчиваться одним и тем же типом кавычек). В Python также есть еще два типа кавычек: ''' (тройные одинарные) и """ (тройные двойные). Таким образом, можно использовать несколько уровней кавычек, прежде чем придется заботиться об их экранировании. Например, этот код правильный:
print """Я бы не хотел никогда услышать, как он говорит: '''Она сказала: "Он сказал: 'Дай мне двести рублей'"'''"""

1.2   Правдивость различных объектов

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

Легко предсказать, что 0 — тоже false, а остальные числа — true.

Например, следующие выражения эквивалентны. В данном случае my_object — строка, но здесь мог оказаться другой тип (с соответствующими изменениями условий блока if).
my_object = 'Test' # True example
# my_object = '' # False example

if len(my_object) > 0:
    print 'my_object не пуст'

if len(my_object):  # 0 преобразовывается к False
    print 'my_object не пуст'

if my_object != '':
    print 'my_object не пуст'

if my_object: # пустая строка преобразовывается к False
    print 'my_object не пуст'

Итак, нет необходимости проверять длину объекта, если вас интересует только, пуст он или нет.

1.3   Проверка на вхождение подстроки

Это маленькая, довольно очевидная подсказка, но я узнал о ней лишь через год изучения Python. Должно быть, вы знаете, что можно проверить, содержится ли нужный элемент в кортеже, списке, словаре, с помощью конструкции 'item in list' или 'item not in list'. Я не мог представить, что это сработает для строк. Я всегда писал что-то вроде этого:
string = 'Hi there' # True example
# string = 'Good bye' # False example
if string.find('Hi') != -1:
    print 'Success!'

Этот код довольно неуклюжий. Совершенно так же работает 'if substring in string':
string = 'Hi there' # True example
# string = 'Good bye' # False example
if 'Hi' in string:
    print 'Success!'

Проще и понятней. Может быть, очевидно для 99% людей, но мне хотелось бы узнать об этом раньше, чем я узнал.

1.4   Красивый вывод списка

Обычный формат вывода списка с помощью print не очень удобен. Конечно, становится понятно, что из себя представляет список, но чаще всего пользователь не хочет видеть кавычки вокруг каждого элемента. Есть простое решение, использующее метод join строки:
recent_presidents = ['Борис Ельцин', 'Владимир Путин', 'Дмитрий Медведев']
print 'Последними президентами были %s.' % ', '.join(recent_presidents)
# печатает "Последними президентами были Борис Ельцин, Владимир Путин, Дмитрий Медведев."

Метод join преобразовывает список в строку, рассматривая каждый элемент как строку. Разделителем является та строка, для которой был вызван join. Он достаточно умен, чтобы не вставлять разделитель после последнего элемента.

Дополнительный бонус: join работает линейное время. Никогда не создавайте строку складыванием элементов списка в цикле for: это не просто некрасиво, это занимает квадратичное время!

1.5   Целочисленное деление и деление с плавающей точкой

Если вы делите целое число на целое, по умолчанию результат обрезается до целого. Например, 5/2 вернет 2.

Есть два способа это исправить. Первый и самый простой способ заключается в том, чтобы преобразовать одно из чисел к типу float. Для констант достаточно добавить ".0" к одному из чисел: 5.0/2 вернет 2.5. Также вы можете использовать конструкцию float(5)/2.

Второй способ дает более чистый код, но вы должны убедиться, что ваша программа не сломается от этого существенного изменения. После вызова 'from __future__ import division' Python всегда будет возвращать в качестве результата деления float. Если вам понадобится целочисленное деление, используйте оператор //: 5//2 всегда возвращает 2.
5/2        # Возвращает 2
5.0/2      # Возвращает 2.5
float(5)/2 # Возвращает 2.5
5//2       # Возвращает 2

from __future__ import division
5/2        # Возвращает 2.5
5.0/2      # Возвращает 2.5
float(5)/2 # Возвращает 2.5
5//2       # Возвращает 2

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

1.6   Лямбда-функции

Иногда нужно передать функцию в качестве аргумента или сделать короткую, но сложную операцию несколько раз. Можно определить функцию обычным способом, а можно использовать лямбда-функцию — маленькую функцию, возвращающую результат одного выражения. Следующие два определения полностью идентичны:
def add(a,b): return a+b

add2 = lambda a,b: a+b

Преимущество лямбда-функции в том, что она является выражением и может быть использована внутри другого выражения. Ниже приведен пример, использующий функцию map, которая вызывает функцию для каждого элемента списка и возвращает список результатов. (В следующем пункте я покажу, что map практически бесполезен. Но он дает нам возможность привести хороший пример в одну строку.)
squares = map(lambda a: a*a, [1,2,3,4,5])
# теперь squares = [1,4,9,16,25]

Без лямбда-функций нам пришлось бы определить функцию отдельно. Мы просто сэкономили одну строку кода и одно имя переменной.
Синтаксис лямбда-функции: lambda переменные: выражение
переменные — список аргументов, разделенных запятой. Нельзя использовать ключевые слова. Аргументы не надо заключать в скобки.
выражение — инлайновое выражение Python. Область видимости включает локальные переменные и аргументы. Функция возвращает результат этого выражения.


2   Списки


2.1   Генераторы списков

Если вы использовали Python достаточно долго, вы должны были хотя бы слышать о понятии «list comprehensions». Это способ уместить цикл for, блок if и присваивание в одну строку.
Другими словами, вы можете отображать (map) и фильтровать списки одним выражением.

2.1.1   Отображение списка

Начнем с простейшего примера. Допустим, нам надо возвести в квадрат все элементы списка. Свежеиспеченный программист на Python может написать код вроде этого:
numbers = [1,2,3,4,5]
squares = []
for number in numbers:
    squares.append(number*number)
# теперь squares = [1,4,9,16,25]

Мы «отобразили» один список на другой. Это также можно сделать с помощью функции map:
numbers = [1,2,3,4,5]
squares = map(lambda x: x*x, numbers)
# теперь squares = [1,4,9,16,25]

Этот код определенно короче (одна строка вместо трех), но всё еще некрасив. С первого взгляда сложно сказать, что делает функция map (она принимает в качестве аргументов функцию и список и применяет функцию к каждому элементу списка). К тому же мы вынуждены определять функцию, это выглядит довольно беспорядочно. Если бы только существовал более красивый путь… например, генератор списка:
numbers = [1,2,3,4,5]
squares = [number*number for number in numbers]
# и снова squares = [1,4,9,16,25]

Этот код делает абсолютно то же самое, но он короче, чем первый пример, и понятней, чем второй. Человек без проблем определит, что делает код, для этого даже не обязательно знать Python.

2.1.2   Фильтрация списка

А что, если нас интересует фильтрация списка? Например, требуется удалить элементы, большие или равные 4. (Да, примеры не очень реалистичны. Как бы то ни было...)

Новичок напишет так:
numbers = [1,2,3,4,5]
numbers_under_4 = []
for number in numbers:
    if number < 4:
        numbers_under_4.append(number)
# numbers_under_4 = [1,4,9]

Очень просто, не так ли? Но код занимает 4 строки, содержит два уровня отступов и при этом делает тривиальную вещь. Можно уменьшить размер кода с помощью функции filter:
numbers = [1,2,3,4,5]
numbers_under_4 = filter(lambda x: x < 4, numbers)
# numbers_under_4 = [1,2,3]

Аналогично функции map, о которой мы говорили выше, filter сокращает код, но выглядит довольно уродливо. Что, черт возьми, происходит? Как и map, filter получает функцию и список. Если функция от элемента возвращает true, элемент включается в результирующий список. Разумеется, мы можем сделать это через генератор списка:
numbers = [1,2,3,4,5]
numbers_under_4 = [number for number in numbers if number < 4]
# numbers_under_4 = [1,2,3]

Снова мы получили более короткий, ясный и понятный код.

2.1.3   Одновременное использование map и filter

Теперь мы можем использовать всю силу генератора списков. Если я вас еще не убедил, что map и filter тратят слишком много вашего времени, надеюсь, теперь вы со мной согласитесь.

Пусть требуется отобразить и отфильтровать список одновременно. Другими словами, я хочу увидеть квадраты элементов списка, меньших 4. Еще раз, неофит напишет так:
numbers = [1,2,3,4,5]
squares = []
for number in numbers:
    if number < 4:
        squares.append(number*number)
# squares = [1,4,9]

Увы, код начал растягиваться вправо. Может, получится упростить его? Попробуем использовать map и filter, но у меня плохое предчувствие…
numbers = [1,2,3,4,5]
squares = map(lambda x: x*x, filter(lambda x: x < 4, numbers))
# squares is = [1,4,9]

Раньше map и filter было трудно читать, теперь — невозможно. Очевидно, это не лучшая идея. И снова генератор списков спасает ситуацию:
numbers = [1,2,3,4,5]
squares = [number*number for number in numbers if number < 4]
# square = [1,4,9]

Получилось немного длиннее, чем предыдущие примеры с генератором списков, но, по моему мнению, вполне читабельно. Определенно лучше, чем цикл for или использование map и filter.

Как вы видите, генератор списков сначала фильтрует, а затем отображает. Если вам обязательно нужно наоборот, получится сложнее. Придется использовать либо вложенные генерации, либо map и filter, либо обычный цикл for, в зависимости от того, что проще. Но это уже выходит за рамки статьи.
Синтаксис генератора списков: [ element for variable(s) in list if condition ]
list — любой итерируемый элемент
variable(s) — переменная или переменные, которые приравниваются к текущему элементу списка, аналогично циклу for
condition — инлайновое выражение: если оно равно true, элемент добавляется в результат
element — инлайновое выражение, результат которого используется как элемент списка-результата

2.1.4   Выражения-генераторы

Существует обратная сторона генератора списков: весь список должен находиться в памяти. Это не проблема для маленьких списков, как в предыдущих примерах, и даже на несколько порядков больше. Но в конце концов это становится неэффективным.

Выражения-генераторы (Generator Expressions) появились в Python 2.4. Из всех фишек Python им уделяется, наверно, меньше всего внимания. Отличие их от генераторов списков состоит в том, что они не загружают в память список целиком, а создают 'generator object', и в каждый момент загружен только один элемент списка.

Конечно, если вы хотите использовать список для чего-нибудь, это не особо поможет. Но если вы просто передаете его куда-нибудь, где нужен любой итерируемый объект (цикл for, например), стоит использовать функцию генератора.

Выражения-генераторы имеют такой же синтаксис, как генераторы списков, но вместо квадратных скобок используются круглые:
numbers = (1,2,3,4,5) # мы стремимся к эффективной работе, поэтому используем кортеж вместо списка ;)
squares_under_10 = (number*number for number in numbers if number*number < 10)
# squares_under_10 - generator object, из которого можно получить следующее значение, вызвав метод .next()

for square in squares_under_10:
    print square,
#выводит '1 4 9'

Это более эффективно, чем использование генератора списков.

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

В качестве финального штриха хочу заметить, что выражения-генераторы достаточно заключить в одни круглые скобки. Например, в случае, если вы вызываете функцию с одним аргументом, можно писать так: some_function(item for item in list).

2.1.5   Заключение

Мне не хочется этого говорить, но мы только прикоснулись к тому, что можно делать с помощью выражений-генераторов и генераторов списков. Здесь можно использовать всю силу for и if, а также оперировать с чем угодно, лишь бы оно было итерируемым объектом.

Статья целиком в PDF
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 67
  • +2
    «Считаете, что прочитать о нескольких уловках лучше, чем провести остаток жизни за чтением документации?» — в принципе в документации написано про это…
    • +2
      Да, но документация длинная, и прочитать ее целиком довольно сложно.
      • +6
        Осилить документацию не сложно, сложно найти и запомнить такие полезные плюшечки.
    • +4
      D целом при использовании данный советов код становится более человеко-читаемым. Это есть хорошо, я считаю.
      • –3
        Спасибо! Очень познавательно и понятно объяснено.
        • +2
          Очень понравилось, что в довольно сжатой форме показаны конструкции которые привычны для Python'а но не привычны для других языков. У многих новичков с ними проблемы.
          • +1
            Спасибо. Уже видел где-то генераторы списков, но когда приходило время использовать — никак не мог вспомнить синтаксис.
            • +9
              Опа, а про «if substring in string» не знал. Спасибо!
              • НЛО прилетело и опубликовало эту надпись здесь
                • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                Странно, что использование map или filter вызовает вопросы у «человека, незнакомого с Python». Функции стандартные, практически азы.
                • +3
                  Функции стандартные, но лучше бы они были методами списка и кортежа. Чтоб писать что-то типа
                  ints=[1,2,3,4,5]
                  ints.filter(lambda x:x
                  • +1
                    то есть
                    ints=[1,2,3,4,5]
                    ints.filter(lambda x:x<3).map(lambda x:x+3)
                  • +2
                    Нельзя. Потому что аргументами map и filter может быть любая итерируемая последовательность (sequence), а не только список и кортеж. А таких последовательностей ой много. Строка, к слову, тоже последовательность (букв). И словарь — последовательность (ключей).
                    • 0
                      А в чем проблема? Метод map может существовать у всех итерируемых объектов (если не ошибаюсь, так и есть в Ruby).
                      • 0
                        А проблема в экспоненциальном взрыве.

                        В любом случае, если язык позволяет делать map(… any_sequence), и если язык позволяет делать any_sequence.map(...), где any_sequence — это действительно любая последовательность, с которой функция map заранее незнакома (например, реализующая метод __iter__, как в Python-е, или унаследованная от какого-то абстрактного класса ISequence, как даже не знаю где — а как, кстати, в Ruby? емнип, там новые подобные функции можно сразу привязать ко всему встроенному классу sequence-а), то с функциональной точки зрения любые подобные языки эквивалентны. И выбор дальше — уже дело вкуса.
                        • 0
                          Относительно ruby: читайте коммент ниже + достаточно реализации методов each и << (если ничего не путаю) у постороннего класса.
                        • 0
                          Ошибаетесь. В Ruby есть модуль Enumerable, который подмешивается ко всем «итерируемым» классам и map определен в нем, а уже он подмешивается в другие классы. Например в 1.8 он подмешивается к String, а в 1.9 уже нет. Таким образом всегда можно написать свой класс, и подмешав в него Enumerable сделать доступным для него кучу фишек, таких как map, sort…
                        • 0
                          дак ну и свои итераторы делаются за пару секунд…
                    • +1
                      В первом выражении используется X
                      squares = map(lambda x: x*x, numbers)
                      А во втором number. Лучше будет в едином стиле ( с иксом, имхо, еще красивее )
                      squares = [x*x for x in [1,2,3,4,5]]
                      • 0
                        Спасибо за статью. Незнавшим может помочь.
                        • 0
                          Четыре типа кавычек.

                          Вообще говоря, их пять. Ещё есть обратные кавычки:
                          from decimal import Decimal
                          print `Decimal(1) / 3`
                          
                          • +2
                            Но лучше бы их не было, конечно. И в Python 3.x их уже нет.
                            • 0
                              А чем они плохи? Поясните начинающему, пожалуйста.
                              • +1
                                Ну, функция repr() же есть. И она именно функция, сущность обычного и уже имеющегося типа. Совершенно новую сущность — «строки с обратными скобками, которые, на самом деле, даже и не строки совсем, а просто применение функции repr() к аргументу» (да-да, это настолько не строки, что `a\nb` они даже не поймут) вводить совершенно не обязательно. И только собьёт с толку, заставив думать, что обратные кавычки в Python имеют хоть какое-то отношение к обратным кавычкам в Perl/Shell (которые подразумевают, что их аргумент запускается в шелле, и возвращают вывод запустившейся программы).
                            • 0
                              Не очень понял как это используется? Типа inline-eval?
                          • –19
                            Кто ещё думает, что язык укурошный? И кому он за это нравится?
                            • 0
                              Прежде чем говорить об укурошности Питона, посмотрите на язык Ruby и почитайте книжку Why's (poignant) Guide to Ruby.
                              Chunky beacon, chunky beacon!
                              • 0
                                Замечательная книжка, замечательный язык. «Укурошный» ровно настолько же, насколько и Питон.
                                • 0
                                  Книжка волшебна, язык прекрасен, я был в него просто влюблён в своё время.
                            • +2
                              Дополнительный бонус: join работает линейное время. Никогда не создавайте строку складыванием элементов списка в цикле for: это не просто некрасиво, это занимает квадратичное время!


                              Кстати, про «квадратичное время» — чушь. Уже достаточно давно складывание элементов не вызывает переаллокаций памяти, и, соответственно, не занимает квадратичное время.

                              Впрочем, это нисколько не мешает складыванию элементов в тех случаях, когда можно использовать join, выглядеть уродски.
                              • +1
                                Мне это тоже показалось подозрительным. Но спорить с автором статьи — не моя задача.
                              • 0
                                Лямбда-функции


                                • 0
                                  Лямбда-функции

                                  Преимущество лямбда-функции в том, что она является выражением и может быть использована внутри другого выражения.


                                  А преимущество Python-а в том, что в нём огромное количество функций уже определено. Например, там, где можно использовать lambda a, b: a + b, там же можно использовать и operator.add.

                                  • +4
                                    а по-моему так наоборот, все эти

                                    from operator import itemgetter
                                    mylist.sort(key = itemgetter(1))

                                    — то еще это уродство)

                                    mylist.sort(key = lambda x: x[1]) — проще, понятнее, гибче, ничего импортировать и запоминать не нужно
                                    • 0
                                      itemgetter — ага, уродство. Но, говорят, чуточку более производительное. Только это его и оправдывает.
                                      А готовые операторофункции типа add/sub уж точно понятнее лямбд.
                                      • +1
                                        «Говорят»? А что Вам мешает проверить?
                                  • +7
                                    Генератор списков


                                    На самом деле, синтаксис list comprehension/generator expression чуточку богаче и позволяет, например, в одном генераторе использовать сразу несколько циклов и проверок, в т.ч. проверки между переменными цикла. Например,
                                    [(x, y, x * y) for x in xrange(10) for y in xrange(10) if x < y]

                                    • 0
                                      Эти примеры в оригинале статьи тоже есть.
                                      Просто переведена пока только первая часть.
                                      www.siafoo.net/article/52
                                    • –8
                                      ничего нового.

                                      это все будешь знать прочитав всего дайв инту питон — отправная точка любого питониста

                                      лучше бы написали как получить все методы класа или типа того, а это имхо каждый школьнег знает
                                      • +6
                                        >> как получить все методы класcа
                                        И об этом Пилгрим пишет в dive into python.
                                      • 0
                                        Вопрос к знающим.
                                        Если весь список должен находиться в памяти при использовании генераторов списка, то как обстоят дела с генераторами словарей?
                                        • 0
                                          > Если весь список должен находиться в памяти при использовании генераторов списка

                                          О каком списке идет речь? Если хотите получить список, то, понятное дело, он нужен в памяти, как его еще получить-то. Получить список (объект типа list) — это и значит «сформировать его в памяти», по определению.

                                          Если хотите пройтись по каким-то объектам, то их список целиком в памяти держать совсем не обязательно. Для этого генераторы и придумали. Например, можно на генераторах сделать бесконечный «список», который в память бы не влез чисто теоретически.

                                          Причем тут вообще словари — непонятно.

                                          Думаю, когда сформулируете вопрос по-нормальному, то и ответ сразу сами найдете.
                                        • +1
                                          пункт 1.2:
                                          в примере ошибка. у всех if'ов кроме первого в условии что-то делает object вместо my_object
                                          • +1
                                            2.1.2:
                                            numbers = [1,2,3,4,5]
                                            numbers_under_4 = filter(lambda x: x < 4, squares)
                                            # numbers_under_4 = [1,2,3]

                                            s/squares/numbers/
                                            • 0
                                              Спасибо, исправил. В оригинале эти ошибки тоже есть, и я их проглядел.
                                              • 0
                                                стоит сообщить о них автору, думаю
                                                • 0
                                                  Да, я написал ему на почту.
                                          • –14
                                            #:"'("___[До_чего_же_все_-_таки_уродлив_этот_"'"__Питон__"'"!_!_!]___")'"();
                                            • –1
                                              в этапах освоения пайтона я, признаться, не разбираюсь, но в книжке по его азам — Learning Python by Mark Lutz обо всем этом сказано, к тому же используются правильные названия переменных: spam, eggs etc. слова хаки и уловки в заголовке излишни, imho.
                                              • 0
                                                «Генераторы списков» — это я полагаю то, что другие называют списковыми включениями?
                                                • 0
                                                  Это то, что в документации называют list comprehensions.
                                                • +6
                                                  >> [number*number for number in numbers if number < 4]

                                                  Ехал number через number, видит number в реке number, number, number, number, number…

                                                  Простите, не сдержался
                                                  • 0
                                                    Ехал = [ (number, через(number), видит (number)* в реке(number) ) for number in numbers if number*number ]
                                                  • –3
                                                    Раздел 1.5 безнадёжно устарел, он верен только для Питона 2.x.
                                                    • +2
                                                      Честно говоря, ни разу в продакшене не встречал Python 3k
                                                      • +1
                                                        2.x вполне себе продолжает развиваться, несмотря на существование 3.х.
                                                        • 0
                                                          Грядущая 2.7 положит конец развитию. Дальше только багфиксы.
                                                      • 0
                                                        хочется более подробной статьи про лямбда функции на русском, с доходчивым объяснением (например reduce)
                                                        • +1
                                                          Разве еще что то можно сказать о них? Кажется все и так уже ясно…
                                                        • 0
                                                          Спасибо, познавательно.
                                                          • 0
                                                            Было интересно прочитать статью со стороны знатока Ruby.
                                                            Мое удивление вызывает только функция join. Мы же join-им массив, а не строку? Может логичнее все таки массив.join(строка)?
                                                            join переводиться как «соединить», таким образом: массив.соеденить(строкой) мне кажется логичней, чем строку.соединить(массивом)
                                                            • 0
                                                              Наверное поздновато отвечать (через три года) но для потомков пригодится.
                                                              Дело в том, что соединять через join можно не только списки, но и любые другие итераторы, как встроенные (кортежи, генераторы), так и пользовательские. Отсюда получается, что удобнее иметь единственный метод в строке, чем реализовывать в каждом новом итераторе.

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