Kodomo

Пользователь

Итераторы. Строки и списки

Напоминание

Для понимания материала ОБЯЗАТЕЛЬНО каждый пример пытаться исполнить, понять, почему он работает или не работает, и попробовать в нём что-то поменять, и посмотреть, что получится.

Программирование – это практическая дисциплина, и без успешных попыток что-то сделать руками понимания теории не появляется.

Разбор задач

Задача
Напишите программу, которая считает среднее значение числа в списке. Проверьте её работу на списках: [1, 2, 3], [4], []

Начнём с определения списка:

   1 list = [1, 2, 3]

Среднее – это сумма элементов, делённая на их количество. Значит, первым делом нам нужно посчитать сумму элементов. Это можно сделать способом, очень похожим на то, как мы в первом задании считали номер строки (только на каждом шагу мы прибавляем к счётчику не единицу, а величину следующего элемента):

   1 sum = 0
   2 for elem in list:
   3     sum = sum + elem

Теперь мы можем посчитать и среднее:

   1 print "Average:", sum / len(list)
Задача
Напишите программу, которая собирает в списке list_reversed элементы списка list в обратном порядке.

Начало нам предопределено (с точностью до наполнения списка list):

   1 list = [1, 4, 2, 5]

Теперь нам нужно в list_reversed положить элементы списка list в обратном порядке. В обратном порядке, то есть list_reversed[i] = list[-i - 1]. Казалось бы, можно было бы так и написать, но натыкаемся на такую трудность: мы не можем менять содержимое списка за рамками границ списка. То есть расширять границы списка конструкцией list_reversed[i] = ... у нас не получится. Как быть? Есть два варианта: либо добиться того, чтобы эта операция не была расширением списка (то есть он изначально должен быть нужного размера, например: list_reversed = [0] * len(list)), либо нужно воспользоваться операцией добавления элемента в список – методом append (но в этом случае нам тоже нужно, чтобы вначале в переменной list_reversed уже лежал какой-то список, притом в нём не было бы ничего лишнего – то есть не было бы ничего вообще):

Получаются варианты. Вариант 1:

   1 list_reversed = []
   2 for index in range(len(list)):
   3     list_reversed.append(list[-index - 1])

Вариант 2:

   1 list_reversed = [0] * len(list)
   2 for index in range(len(list)):
   3     list_reversed[index] = list[-index - 1]

Вариант 3:

   1 list_reversed = []
   2 for elem in list:
   3     list_reversed = [elem] + list_reversed
   4     # list_reversed.insert(0, elem)

Ну и в конце мы можем вывести наши достижения:

   1 print list, "reversed will get you", list_reversed
Задача
Напишите программу, которая проверяет, является ли данный список list палиндромом. (Программа начинается со строки list = [...], и пишет ответ вида "List [1, 2, 3, 2, 1] is a palindrome"). Проверьте её на нескольких разных списках. Может ли она проверить на палиндромность строку?

первая строка программы нам уже дана:

   1 list = [1, 2, 3, 2, 1]

дальше нам нужно вспомнить определение палиндрома: это строка, которая читается спереди так же, как и сзади. Проще всего эту задачу свести к предыдущей: палиндром – это такая строка, для которой сама строка равна развёрнутой. Если мы воспользуемся частью прошлого решения, то получится:

   1 list_reversed = []
   2 for elem in list:
   3     list_reversed.insert(0, elem)
   4 
   5 if list_reversed == list:
   6     print list, "is a palindrome"
   7 else:
   8     print list, "is not a palindrome"

elif

Вместо:

   1 if n == 1:
   2    print n, "bottle"
   3 else:
   4    if n == 0:
   5        print "No more bottles"
   6    else:
   7        print n, "bottles"

Можно (и нужно!) писать так:

   1 if n == 1:
   2    print n, "bottle"
   3 elif n == 0:
   4    print "No more bottles"
   5 else:
   6    print n, "bottles"

Итераторы

Когда мы писали песенку про 99 бутылок, нам хотелось бы идти по списку 0..99 в обратном порядке. Мы обошлись без этого, но это можно сделать: функция range() может принимать до трёх аргументов, точно так же, как и слайсы в списках:

   1 print range(10)
   2 print range(3, 10)
   3 print range(10, 0, -1)

Результат последней команды наверняка вас удивил. Но на самом деле всё очевидно: здесь действует то же правило, что и раньше – мы всегда останавливаемся непосредственно перед последним элементом. Но в этом случае приходится долго и напряжённо думать, как же именно задать условия, чтобы получить именно тот диапазон, с которым мы хотим работать. Поэтому этот способ я не рекомендую.

А более простой способ пройти от 99 до 0 – использовать итератор reversed(). Итератор – это такой объект, который ведёт себя как список, если его подсунуть в цикл for, но во всех остальных применениях совершенно несъедобен1:

   1 print reversed(range(10))
   2 for x in reversed(range(10)):
   3     print x

Если же нам нужно всё-таки превратить то, что вернул итератор, в список, то для этого нужно воспользоваться функцией list – она пытается делать список из всего, что ей дадут в качестве аргумента:

   1 print list(reversed(range(10)))

В первую очередь итераторы придумали для того, чтобы экономить время и память (но нам о таких вещах пока что думать не имеет смысла). Однако мы ещё увидим (где-нибудь в третьем модуле), что итераторы могут очень здорово упрощать жизнь – когда мы научимся делать специальные итераторы для наших задач.

Сейчас же нам легко понять пользу от итераторов вот как: в питоне есть итератор, который идёт по бесконечному списку. (Мы не можем просто взять и положить бесконечный список в память – поэтому без итераторов тут не обойтись). Такие итераторы (а ещё куча других полезных и других бесполезных) лежит в модуле itertools. Нашу задачку с вертушкой мы могли бы решить так2:

   1 import itertools
   2 stick = "/-\\|"
   3 for step in itertools.count(0):
   4     print stick[step % len(stick)]

А можете ли вы угадать, что делают:

   1 word = "hello"
   2 print list(word)
   3 for x in word:
   4     print x

Разбор строк

И о том, как мы можем себе немножко упростить вытаскивание полезной информации из строк.

Самый частый вид полезной информации – список слов в строке. Делает это метод split у строк:

   1 text = "Hello, wonderful world!"
   2 words = text.split()
   3 print words

Питон тут понимает слово как любой набор видимых символов, идущих подряд (то есть, всё кроме пробелов, табуляций, переносов строки). Как следствие, если в строке идёт подряд несколько пробелов, питон их считает за один разделитель между слов.

У функции split есть второе полезное назначение, с которым мы познакомимся ближе в следующий раз: разбивать строку по заданному разделителю:

   1 row = "Vasily,, Pupkin,42"
   2 fields = row.split(',')
   3 print "First name:", fields[0]
   4 print "Middle name:", fields[1]
   5 print "Last name:", fields[2]
   6 print "Age:", fields[3]

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

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

Для борьбы с этим делом в питоне есть метод строк strip, который отрезает все лишние пробелы по краям строки (а с ними заодно и переносы строк и табуляции – все такие символы, которые видны как просто пустое белое пространство, называются whitespace – пробельными символами):

   1 text = "     \n   Hello, world! "
   2 cleared = text.strip()
   3 print cleared

И, соответственно:

   1 row = "Vasily,, Pupkin,42"
   2 fields = row.split(',')
   3 
   4 cleared = []
   5 for field in fields:
   6     cleared.append(field.strip())
   7 
   8 print "First name:", fields[0]
   9 print "Middle name:", fields[1]
  10 print "Last name:", fields[2]
  11 print "Age:", fields[3]

У метода strip есть ещё два собрата: методы lstrip и rstrip, которые убирают лишние пробелы только слева или только справа соответственно.

Наконец, у метода split есть антипод, который собирает обратно строку из списка её кусочков, и вставляет между ними разделитель. Называется join:

   1 steps = ["wake up", "take shower", "break fast", "run fast", "work", "lunch"]
   2 print " -> ".join(steps)

В питоне есть золотое правило: B.join(A.split(B)) == A. То есть если мы разбили строку A по разделителю B, а потом собрали кусочки, вставив между ними разделитель B, то результат будет снова идентичным.

У метода join есть одна маленькая, но иногда очень противная беда: он требует на вход обязательно список только из строк. Когда мы с ней столкнёмся, нам придётся научиться превращать список чего угодно (например, чисел) в список строк. (Вдумчивый читатель может догадаться, как это сделать, уже сейчас – точно так же, как мы использовали strip для того, чтобы убрать лишние пробелы с каждого элемента списка).

Литература

  1. Я сказал "итератор reversed(), но если говорить совсем формально, то reversed() -- это функция, которая берёт на вход список, и возвращает итератор, обходящий элементы этого списка в обратном порядке. (1)

  2. И исключительно по моему недосмотру это не тот вариант, которым я предложил это делать на первом занятии (2)