Главная страница

Математический анализ. 3е издание


Скачать 4.86 Mb.
Название3е издание
АнкорМатематический анализ
Дата04.02.2022
Размер4.86 Mb.
Формат файлаpdf
Имя файлаpython_01.pdf
ТипДокументы
#351981
страница43 из 98
1   ...   39   40   41   42   43   44   45   46   ...   98
i = 0
>>> while i < len(X): # Обход с помощью цикла while
... print X[i],; i += 1
s p a m
Однако управлять индексами вручную можно и в цикле for, если ис
пользовать функцию range для воспроизведения списка индексов:
>>> X
'spam'
>>> len(X) # Длина строки
4
>>> range(len(X)) # Все допустимые смещения в X
[0, 1, 2, 3]
>>>
>>> for i in range(len(X)): print X[i], # Извлечение элементов вручную
s p a m
В этом примере выполняется обход списка смещений в строке X, а не фактических элементов строки – нам пришлось внутри цикла обра
щаться к строке X, чтобы извлечь каждый элемент.

352
Глава 13. Циклы while и for
Обход части последовательности: range
Последний пример в предыдущем разделе вполне работоспособен, но он выполняется гораздо медленнее, чем мог бы. Кроме того, нам при
шлось выполнить больше работы, чем требуется для решения такой задачи. Если вы не предъявляете особых требований к индексам, все
гда лучше использовать простейшую форму цикла for – используйте цикл for вместо while везде, где только возможно, и используйте функ
цию range в циклах for, только если это действительно необходимо.
Следующее простое решение является лучшим:
>>> for item in X: print item, # Простейшая итерация
Однако прием, представленный в предшествующем примере, позволя
ет нам управлять порядком обхода последовательности, например пропускать элементы:
>>> S = 'abcdefghijk'
>>> range(0, len(S), 2)
[0, 2, 4, 6, 8, 10]
>>> for i in range(0, len(S), 2): print S[i],
a c e g i k
Здесь в цикле выбирается каждый второй элемент строки S при обходе списка значений, сгенерированных функцией range. Чтобы извлечь ка
ждый третий элемент, достаточно изменить третий аргумент функции range
, передав в нем значение 3, и т. д. Таким образом, функция range позволяет пропускать элементы, сохраняя при этом простоту цикла for.
Однако на сегодняшний день это, пожалуй, не самый лучший способ.
Если вам действительно необходимо пропустить элементы последова
тельности, можно использовать расширенную форму операции извле
чения среза с тремя пределами, представленную в главе 7, которая обеспечивает более простой путь к достижению цели. Чтобы получить каждый второй символ из строки S, можно извлечь срез с шагом 2:
>>> for x in S[::2]: print x
Изменение списков: range
Еще одно место, где можно использовать комбинацию функции range и цикла for, – это циклы, изменяющие список в процессе его обхода.
Например, предположим, что по тем или иным причинам нам необхо
димо прибавить 1 к каждому элементу списка. Можно попытаться ис
пользовать для этой цели простейшую форму цикла for, но скорее все
го это не то, что нам нужно:
>>> L = [1, 2, 3, 4, 5]

Приемы программирования циклов
353
>>> for x in L:
... x += 1
>>> L
[1, 2, 3, 4, 5]
>>> x
6
Такое решение вообще ничего не дает – здесь изменяется переменная цикла x, а не список L. Причину такого поведения трудно заметить.
Всякий раз, когда цикл выполняет очередную итерацию, переменная x
ссылается на очередное целое число, которое уже было извлечено из списка. В первой итерации, например, переменная x является целым числом 1. На следующей итерации в переменную x будет записана ссылка на другой объект – целое число 2, но это никак не повлияет на список, откуда было взято число 1.
Чтобы действительно изменить список во время его обхода, нам необ
ходимо использовать операцию присваивания по индексу и изменить значения во всех позициях, по которым осуществляется цикл. Необ
ходимые нам индексы можно воспроизвести с помощью комбинации функций range/len:
>>> L = [1, 2, 3, 4, 5]
>>> for i in range(len(L)): # Прибавить 1 к каждому элементу в списке L
... L[i] += 1 # Or L[i] = L[i] + 1
>>> L
[2, 3, 4, 5, 6]
При такой реализации список изменяется в процессе обхода. Простой цикл for x in L: такого результата дать не может, потому что в таком цикле выполняется обход фактических элементов, а не позиций в спи
ске. А возможно ли создать эквивалентный цикл while? Для этого нам потребуется приложить немного больше усилий, и такой цикл навер
няка будет работать медленнее:
>>> i = 0
>>> while i < len(L):
... L[i] += 1
... i += 1
>>> L
[3, 4, 5, 6, 7]
В данном случае решение на базе функции range может быть неидеаль
ным. Генератор списка в виде [x+1 for x in L] также даст желаемый ре
зультат, но первоначальный список при этом не изменится (мы могли бы присвоить получившийся новый список обратно переменной L, но это выражение не изменит другие ссылки на первоначальный список).
Поскольку эта концепция циклов занимает такое важное положение,

354
Глава 13. Циклы while и for мы еще раз вернемся к ней, когда будем рассматривать генераторы спи
сков ниже в этой главе.
Параллельный обход: zip и map
Как было показано выше, встроенная функция range позволяет выпол
нять обход отдельных частей последовательностей. В том же духе встроенная функция zip позволяет использовать цикл for для обхода нескольких последовательностей параллельно. Функция zip принима
ет одну или несколько последовательностей в качестве аргументов и возвращает список кортежей, составленных из соответствующих элементов этих последовательностей. Например, предположим, что мы выполняем обработку двух списков:
>>> L1 = [1,2,3,4]
>>> L2 = [5,6,7,8]
Для объединения элементов этих списков можно использовать функ
цию zip, которая создаст список кортежей из пар элементов:
>>> zip(L1,L2)
[(1, 5), (2, 6), (3, 7), (4, 8)]
Такой результат может пригодиться в самых разных ситуациях, но применительно к циклу for он обеспечивает возможность выполнения параллельных итераций:
>>> for (x, y) in zip(L1, L2):
... print x, y, ' ', x+y
1 5  6 2 6  8 3 7  10 4 8 – 12
Здесь выполняется обход результата обращения к функции zip, т. е.
пар, составленных из элементов двух списков. Обратите внимание, что в этом цикле используется операция присваивания кортежей для по
лучения элементов каждого кортежа, полученного от функции zip. На первой итерации она будет выглядеть, как если бы была выполнена инструкция (x, y) = (1, 5).
Благодаря этому мы можем сканировать оба списка L1 и L2 в одном цикле. Тот же эффект можно получить с помощью цикла while, в кото
ром доступ к элементам производится вручную, но такой цикл будет сложнее в реализации и наверняка медленнее, чем прием, основанный на использовании for/zip.
Функция zip на самом деле более универсальна, чем можно было бы представить на основе этого фрагмента. Например, она принимает по
следовательности любого типа (в действительности – любые итерируе
мые объекты, включая и файлы) и позволяет указывать более двух ар
гументов:

Приемы программирования циклов
355
>>> T1, T2, T3 = (1,2,3), (4,5,6), (7,8,9)
>>> T3
(7, 8, 9)
>>> zip(T1,T2,T3)
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
Длина списка, возвращаемого функцией zip, равна длине кратчайшей из последовательностей, если аргументы имеют разную длину:
>>> S1 = 'abc'
>>> S2 = 'xyz123'
>>>
>>> zip(S1, S2)
[('a', 'x'), ('b', 'y'), ('c', 'z')]
Родственная (и более старая) встроенная функция map объединяет эле
менты последовательностей похожим образом, но она дополняет не
достающие элементы значениями None, если аргументы имеют разную длину:
>>> map(None, S1, S2)
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None,'3')]
В этом примере фактически используется простейшая форма обраще
ния к встроенной функции map. Обычно она принимает функцию и од
ну или более последовательностей и собирает результаты вызова функ
ции с соответствующими элементами, извлеченными из последова
тельностей.
Когда в первом аргументе передается значение None (как в данном слу
чае), она просто объединяет элементы подобно функции zip. Функция map и прочие родственные ей инструменты будут рассматриваться в гла
ве 17.
Конструирование словаря с помощью функции zip
В главе 8 я говорил, что функцию zip, используемую здесь, удобно при
менять для создания словарей, когда ключи и значения вычисляются во время выполнения программы. Теперь, когда мы поближе познако
мились с этой функцией, я объясню, какое отношение она имеет к кон
струированию словарей. Как вы уже знаете, словарь всегда можно соз
дать с помощью литерала словаря или присваивая значения ключам:
>>> D1 = {'spam':1, 'eggs':3, 'toast':5}
>>> D1
{'toast': 5, 'eggs': 3, 'spam': 1}
>>> D1 = {}
>>> D1['spam'] = 1
>>> D1['eggs'] = 3
>>> D1['toast'] = 5
Но как быть, если программа получает ключи и значения для словаря в виде списков во время выполнения, уже после того, как сценарий

356
Глава 13. Циклы while и for был написан? Например, предположим, что имеются следующие спи
ски ключей и значений:
>>> keys = ['spam', 'eggs', 'toast']
>>> vals = [1, 3, 5]
Один из способов превратить их в словарь состоит в том, чтобы пере
дать списки функции zip и затем выполнить обход полученного ре
зультата в цикле for:
>>> zip(keys, vals)
[('spam', 1), ('eggs', 3), ('toast', 5)]
>>> D2 = {}
>>> for (k, v) in zip(keys, vals): D2[k] = v
>>> D2
{'toast': 5, 'eggs': 3, 'spam': 1}
Однако, начиная с версии Python 2.2, можно обойтись без цикла for и просто передать результат вызова функции zip встроенному конст
руктору dict:
>>> keys = ['spam', 'eggs', 'toast']
>>> vals = [1, 3, 5]
>>> D3 = dict(zip(keys, vals))
>>> D3
{'toast': 5, 'eggs': 3, 'spam': 1}
Встроенное имя dict в языке Python в действительности является име
нем типа (больше об именах типов и о создании подтипов вы узнаете в главе 26). Этот вызов производит преобразование списка в словарь,
но в действительности это вызов конструктора объекта. Далее в этой главе мы рассмотрим родственное, но более широкое понятие генера+
торов списков
, которые позволяют создавать списки с помощью един
ственного выражения.
Генерирование индексов и элементов: enumerate
Ранее мы рассматривали использование функции range для генерации индексов (смещений) элементов в строке вместо получения самих эле
ментов с этими индексами. Однако в некоторых программах необходи
мо получить и то, и другое: и элемент, и его индекс. При традицион
ном подходе можно было бы использовать простой цикл for, в котором вести счетчик текущего индекса:
>>> S = 'spam'
>>> offset = 0
>>> for item in S:
... print item, 'appears at offset', offset
... offset += 1
s appears at offset 0

Генераторы списков: первое знакомство
357
p appears at offset 1
a appears at offset 2
m appears at offset 3
Этот способ вполне работоспособен, но в последних версиях языка Py
thon те же самые действия можно выполнить с помощью встроенной функции с именем enumerate:
>>> S = 'spam'
>>> for (offset, item) in enumerate(S):
... print item, 'appears at offset', offset
s appears at offset 0
p appears at offset 1
a appears at offset 2
m appears at offset 3
Функция enumerate возвращает объект+генератор – разновидность объекта, который поддерживает итерационный протокол, с которым мы встретились выше в этой главе и более подробно будем осуждать в следующей части книги. Он имеет метод next, возвращающий кор
теж (index, value) для каждого элемента списка, который мы можем использовать для присваивания кортежей в цикле for (точно так же,
как и в случае с функцией zip):
>>> E = enumerate(S)
>>> E.next()
(0, 's')
>>> E.next()
(1, 'p')
Обычно мы не видим всю эту механику, потому что во всех контекстах итераций (включая генераторы списков – тема следующего раздела)
итерационный протокол выполняется автоматически:
>>> [c * i for (i, c) in enumerate(S)]
['', 'p', 'aa', 'mmm']
Генераторы списков: первое знакомство
В предыдущем разделе мы узнали о возможности использовать функ
цию range для изменения списков в ходе выполнения итераций:
>>> L = [1, 2, 3, 4, 5]
>>> for i in range(len(L)):
... L[i] += 10
>>> L
[11, 12, 13, 14, 15]
Этот способ работает, но, как я уже упоминал, он может быть далеко не самым оптимальным в языке Python. В наши дни выражения гене
раторы списков переводят многое из того, что использовалось раньше,

358
Глава 13. Циклы while и for в разряд устаревших приемов. Например, в следующем фрагменте цикл был заменен единственным выражением, которое в результате воспроизводит требуемый список:
>>> L = [x + 10 for x in L]
>>> L
[21, 22, 23, 24, 25]
Конечный результат получается тем же самым, но от нас потребова
лось меньше усилий и, скорее всего, этот вариант работает быстрее.
Выражения генераторов списков нельзя считать равнозначной заме
ной инструкции цикла for, потому что они создают новые объекты списков (что может иметь значение при наличии нескольких ссылок на первоначальный список), но это подходящая замена для большин
ства применений, к тому же распространенная и достаточно удобная,
чтобы заслужить внимательного изучения здесь.
Основы генераторов списков
Впервые с генераторами списков мы встретились в главе 4. Синтаксис генераторов списков происходит от конструкций, используемых в тео
рии множеств для описания операций над каждым элементом множе
ства, но вам совсем необязательно знать теорию множеств, чтобы ис
пользовать их. Многие считают, что генераторы списков в языке Py
thon напоминают цикл for, записанный задом наперед.
Давайте рассмотрим пример из предыдущего раздела более подробно.
Генераторы списков записываются в квадратных скобках, потому что это, в конечном счете, способ создания нового списка. Генератор спи
ска начинается с некоторого составленного нами выражения, которое использует введенную нами переменную цикла (x + 10). Далее следует то, что вы без труда опознаете как заголовок цикла for, в котором объ
является переменная цикла и итерируемый объект (for x in L).
Чтобы найти значение выражения, Python выполняет обход списка L,
присваивая переменной x каждый очередной элемент, и собирает ре
зультаты пропускания всех элементов через выражение слева. Полу
ченный в результате список является точным отражением того, что
«говорит» генератор списков, – новый список, содержащий x+10 для каждого x в L.
С технической точки зрения всегда можно обойтись без генераторов списков, потому что существует возможность создавать список резуль
татов выражения вручную, с помощью цикла for:
>>> res = []
>>> for x in L:
... res.append(x + 10)
>>> res
[21, 22, 23, 24, 25]

Генераторы списков: первое знакомство
359
Фактически это точное представление внутреннего механизма генера
тора списков.
Но генераторы списков записываются компактнее, и данный способ сборки списков получил широкое распространение в языке Python,
поэтому они оказываются очень удобными во многих ситуациях. Бо
лее того, генераторы списков могут выполняться значительно быстрее
(зачастую почти в два раза), чем инструкции циклов for, потому что итерации выполняются со скоростью языка C, а не со скоростью про
граммного кода на языке Python. Такое преимущество в скорости осо
бенно важно для больших объемов данных.
Использование генераторов списков
для работы с файлами
Рассмотрим еще один распространенный случай использования гене
раторов списков, исследуя в деталях их работу. Вспомним, что у объ
екта файла имеется метод readlines, который загружает файл целиком в список строк:
>>> f = open('script1.py')
>>> lines = f.readlines()
>>> lines
['import sys\n', 'print sys.path\n', 'x = 2\n', 'print 2 ** 33\n']
Этот фрагмент работает, но все строки в списке оканчиваются симво
лом новой строки (\n). Символ новой строки является препятствием для многих программ – приходится быть осторожным, чтобы избе
жать появления пустых строк при выводе и т. д. Было бы совсем не
плохо, если бы мы могли одним махом избавиться от этих символов новой строки.
Всякий раз, когда мы заговариваем о выполнении операций над каж
дым элементом последовательности, мы попадаем в сферу действий ге
нераторов списков. Например, предположим, что переменная lines на
ходится в том же состоянии, в каком она была оставлена в предыду
щем примере. Тогда следующий фрагмент обработает каждую строку в списке функцией rstrip, чтобы удалить завершающие пробельные символы (также можно было бы использовать выражение извлечения среза line[:1], но только если бы мы были абсолютно уверены, что все строки завершаются символом новой строки):
>>> lines = [line.rstrip() for line in lines]
>>> lines
['import sys', 'print sys.path', 'x = 2', 'print 2 ** 33']
Этот метод работает, генераторы списков – это другой итерационный контекст, но точно так же, как и в простом цикле for, нам не требуется даже открывать файл заранее. Если открыть его внутри выражения,
генератор списков автоматически будет использовать итерационный протокол, с которым мы познакомились выше в этой главе. То есть он

360
Глава 13. Циклы while и for будет читать из файла по одной строке за раз – вызовом метода next файла, пропускать строку через функцию rstrip и добавлять результат в список. И снова мы получаем именно то, что запрашиваем, – резуль
тат работы метода rstrip для каждой строки в файле:
>>>
1   ...   39   40   41   42   43   44   45   46   ...   98


написать администратору сайта