Глава 3. Генераторы
Нам приходится сильнее напрягать свое воображение не для того, чтобы, как в художественной литературе, представить себе то, чего нет на самом деле, а для того, чтобы постичь то, что действительно происходит.
Содержание главы
Нам приходится сильнее напрягать свое воображение не для того, чтобы, как в художественной литературе, представить себе то, чего нет на самом деле, а для того, чтобы постичь то, что действительно происходит.
Ричард Фейнман
Погружение
В каждом языке программирования есть одна такая особенность, сложно устроенная, но специально упрощённая штука. Если вы раньше писали на другом языке, можете и не обратить на это внимания, поскольку ваш старый язык не так сильно упрощал эту штуку (потому что он был занят тем, что сильно упрощал что-то другое). В этой главе вы изучите генераторы списков, словарей и множеств – три взаимосвязанные концепции, сконцентрированные вокруг одной очень мощной технологии. Но сначала я хочу немного отклониться от нашего повествования, чтобы рассказать вам о двух модулях, которые помогут вам передвигаться по вашей локальной файловой системе.
3.2 Работа с файлами и каталогами
Python 3 поставляется с модулем os, что означает «операционная система». Модуль os содержит множество функций для получения информации о локальных каталогах, файлах, процессах и переменных окружения (а в некоторых случаях, и для манипулирования ими). Python предлагает очень хороший унифицированный программный интерфейс для всех поддерживаемых операционных систем, так что ваши программы можно запускать на любом компьютере с минимальным количеством платформо-зависимого кода.
3.2.1 Текущий рабочий каталог
Всегда есть текущий рабочий каталог.
Когда ваше знакомство с Python только начинается, вы много времени проводите в интерактивной оболочке Python. На протяжении всей этой книги вы будете видеть примеры, выглядящие следующим образом:
- Импортирование какого-либо модуля из папки примеров
- Вызов функции из этого модуля
- Объяснение результата
Если вы ничего не знаете о текущем рабочем каталоге, то, возможно, шаг 1 окажется неудачным и будет порождено исключение типа ImportError. Почему? Потому что Python будет искать указанный модуль в пути поиска оператора import, но не найдёт его, потому что каталог examples не содержится в путях поиска. Чтобы исправить это, вы можете сделать одно из двух:
- либо добавить папку examples в путь поиска оператора
import; - либо сделать текущим рабочим каталогом папку examples.
Текущий рабочий каталог является неявным параметром, который Python постоянно хранит в памяти. Текущий рабочий каталог есть всегда, когда вы работаете в интерактивной оболочке Python, запускаете свой сценарий из командной строки или CGI-сценарий где-то на веб-сервере.
Модуль os содержит две функции для работы с текущим рабочим каталогом.
>>> import os ①
>>> print(os.getcwd()) ②
C:\Python31
>>> os.chdir('/Users/pilgrim/diveintopython3/examples') ③
>>> print(os.getcwd()) ④
C:\Users\pilgrim\diveintopython3\examples
- Строка 1. Модуль
osпоставляется вместе с Python; вы можете импортировать его когда угодно и где угодно. - Строка 2. Используйте функцию
os.getcwd()для получения значения текущего рабочего каталога. Когда вы работаете в графической оболочке Python, текущим рабочим каталогом является каталог из которого она была запущена. В Windows это зависит от того, куда вы установили Python; каталог по умолчанию c:\Python31. Если оболочка Python запущена из командной строки, текущим рабочим каталогом считается тот, в котором вы находились, когда запускали её. - Строка 4. Используйте функцию
os.chdir()чтобы сменить текущий рабочий каталог. - Строка 5. Когда я вызывал функцию
os.chdir(), я использовал путь в стиле Linux (косая черта, нет буквы диска), даже если на самом деле работал в Windows. Это одно из тех мест, где Python пытается стирать различия между операционными системами.
3.2.2 Работа с именами файлов и каталогов
Раз зашла речь о каталогах, я хочу обратить ваше внимание на модуль os.path. Он содержит функции для работы с именами файлов и каталогов.
>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py')) ①
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py')) ②
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~')) ③
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')) ④
c:\Users\pilgrim\diveintopython3\examples\humansize.py
- Строка 2. Функция
os.path.join()составляет путь из одного или нескольких частичных путей. В данном случае она просто соединяет строки. - Строка 4. Это уже менее тривиальный случай. Функция
joinдобавит дополнительную косую черту (slash) к имени папки перед тем как дописать имя файла. В данном случае Python добавляет обратную косую черту (backslash) вместо обыкновенной, потому что я запустил этот пример в Windows. Если вы введёте данную команду в Linux или Mac OS X, вы увидите простую косую черту. Python может обратиться к файлу независимо от того, какой разделитель используется в пути к файлу. - Строка 6. Функция
os.path.expanduser()раскрывает путь, в котором используется символ ~ для обозначения домашнего каталога текущего пользователя. Функция работает на любой платформе, где у пользователя есть домашний каталог, включая Linux, Mac OS X и Windows. Функция возвращает путь без косой черты в конце, но для функцииos.path.join()это не имеет значения. - Строка 8. Комбинируя эти две функции, вы можете легко строить пути для папок и файлов в домашнем каталоге пользователя. Функция
os.path.join()принимает любое количество аргументов. Я получил огромное удовольствие, когда обнаружил это, так как в других языках при разработке инструментальных средств мне приходилось постоянно писать глупую маленькую функциюaddSlashIfNecessary(). В языке программирования Python умные люди уже позаботились об этом.
Модуль os.path также содержит функции для разбиения файловых путей, имён папок и файлов на их составные части.
>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname) ①
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')
>>> (dirname, filename) = os.path.split(pathname) ②
>>> dirname ③
'/Users/pilgrim/diveintopython3/examples'
>>> filename ④
'humansize.py'
>>> (shortname, extension) = os.path.splitext(filename) ⑤
>>> shortname
'humansize'
>>> extension
'.py'
- Строка 2. Функция
splitдробит полный путь и возвращает кортеж, содержащий отдельно путь до каталога и имя файла. - Строка 4. Помните, я рассказывал про то, как присваивать несколько значений за раз и как вернуть одновременно несколько значений из функции? Функция
os.path.split()действует именно так. Можно присвоить возвращаемое из функцииsplitзначение кортежу из двух переменных. Каждая из переменных примет значение соответствующего элемента результирующего кортежа. - Строка 5. Первая переменная –
dirname– получит значение первого элемента кортежа, возвращаемого функциейos.path.split(), а именно путь до каталога. - Строка 7. Вторая переменная –
filename– примет значение второго элемента кортежа, возвращаемого функциейos.path.split(), а именно имя файла. - Строка 9. Модуль
os.pathтакже содержит функциюos.path.splitext(), которая дробит имя файла и возвращает кортеж, содержащий отдельно имя и отдельно расширение файла. Можно использовать ту же технику, что и ранее для присваивания каждого из интересующих значений отдельным переменным.
3.2.3 Получение содержимого каталога
Модуль glob понимает символы-джокеры, использующиеся в командных оболочках.
Модуль glob – это ещё один инструмент из стандартной библиотеки Python. Это простой способ программно получить содержимое папки, а также он умеет использовать символы-джокеры, с которыми вы наверняка знакомы, если работали в командной строке.
>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml') ①
['examples\\feed-broken.xml',
'examples\\feed-ns0.xml',
'examples\\feed.xml']
>>> os.chdir('examples/') ②
>>> glob.glob('*test*.py') ③
['alphameticstest.py',
'pluraltest1.py',
'pluraltest2.py',
'pluraltest3.py',
'pluraltest4.py',
'pluraltest5.py',
'pluraltest6.py',
'romantest1.py',
'romantest10.py',
'romantest2.py',
'romantest3.py',
'romantest4.py',
'romantest5.py',
'romantest6.py',
'romantest7.py',
'romantest8.py',
'romantest9.py']
- Строка 3. Модуль
globпринимает шаблон, содержащий символы-джокеры, и возвращает пути всех файлов и каталогов, соответствующих ему. В этом примере шаблон содержит путь к каталогу и "*.xml", которому будут соответствовать все xml-файлы в каталоге examples. - Строка 7. Теперь сделаем текущим рабочим каталог examples. Функция
os.chdir()может принимать и относительные пути. - Строка 8. Вы можете использовать несколько символов-джокеров в своём шаблоне. Этот пример находит все файлы в текущем рабочем каталоге, заканчивающиеся на .py и содержащие слово test где-нибудь в имени файла.
3.2.4 Получение сведений о файле
Любая современная операционная система хранит сведения о каждом файле (метаданные): дата создания, дата последней модификации, размер файла и т.д. Python предоставляет единый программный интерфейс для доступа к этим метаданным. Вам не надо открывать файл; всё, что требуется – имя файла.
>>> import os
>>> print(os.getcwd()) ①
c:\Users\pilgrim\diveintopython3\examples
>>> metadata = os.stat('feed.xml') ②
>>> metadata.st_mtime ③
1247520344.9537716
>>> import time ④
>>> time.localtime(metadata.st_mtime) ⑤
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
- Строка 2. Текущий рабочий каталог – папка с примерами.
- Строка 4. feed.xml – файл в папке с примерами. Вызов функции
os.stat()возвращает объект, содержащий различные метаданные о файле. - Строка 5.
st_mtime– время изменения файла, но записано оно в ужасно неудобном формате. (Фактически это количество секунд, прошедших с начала «эры UNIX», начавшейся в первую секунду 1 января 1970 года. Серьёзно.) - Строка 7. Модуль
timeявляется частью стандартной библиотеки Python. Он содержит функции для преобразований между различными форматами представления времени и часовыми поясами, для преобразования их в строки (str) и др. - Строка 8. Функция
time.localtime()преобразует время из формата «секунды с начала эры» (полеst_mtime, возвращённое функциейos.stat()) в более удобную структуру, содержащую год, месяц, день, час, минуту, секунду и т. д. Этот файл в последний раз изменялся 13 июля 2009 года, примерно в 17 часов, 25 минут.
# continued from the previous example
>>> metadata.st_size ①
3070
>>> import humansize
>>> humansize.approximate_size(metadata.st_size) ②
'3.0 KiB'
- Строка 2. Функция
os.stat()также возвращает размер файла в свойствеst_size. Размер файла feed.xml – 3070 байт. - Строка 5. Вы можете передать свойство
st_sizeв функциюapproximate_size().
3.2.5 Получение абсолютных путей
В предыдущем разделе функция glob.glob() возвращала список относительных путей. В первом примере пути имели вид 'examples\feed.xml', а во втором относительные пути были даже короче, например, 'romantest1.py'. Пока вы остаётесь в текущем рабочем каталоге, по этим относительным путям можно будет открывать файлы или получать их метаданные. Но если вы захотите получить абсолютный путь – то есть тот, который включает все имена каталогов до корневого или до буквы диска, вам понадобится функция os.path.realpath().
>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml
3.3 Генераторы списков
В генераторах списков можно использовать любые выражения Python.
С помощью генераторов списков можно легко отобразить один список в другой, применив некоторую функцию к каждому элементу.
>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list] ①
[2, 18, 16, 8]
>>> a_list ②
[1, 9, 8, 4]
>>> a_list = [elem * 2 for elem in a_list] ③
>>> a_list
[2, 18, 16, 8]
- Строка 2. Чтобы понять, что здесь происходит, прочитайте генератор справа налево.
a_list– отображаемый список. Python последовательно перебирает элементы спискаa_list, временно присваивая значение каждого элемента переменнойelem. Затем применяет функциюelem * 2и добавляет результат в возвращаемый список. - Строка 4. Генератор создаёт новый список, не изменяя исходный.
- Строка 6. Можно присвоить результат работы генератора списка отображаемой переменной. Python создаст новый список в памяти и, когда результат работы генератора будет получен, присвоит его исходной переменной.
В генераторах списков можно использовать любые выражения Python, включая функции модуля os, применяемые для работы с файлами и каталогами.
>>> import os, glob
>>> glob.glob('*.xml') ①
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')] ②
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
- Строка 2. Это выражение возвращает список всех .xml-файлов в текущем рабочем каталоге.
- Строка 4. Этот генератор принимает список всех .xml-файлов и преобразует его в список полных путей.
При генерировании списков можно также фильтровать элементы, чтобы отбросить некоторые значения.
>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000] ①
['pluraltest6.py',
'romantest10.py',
'romantest6.py',
'romantest7.py',
'romantest8.py',
'romantest9.py']
- Строка 2. Чтобы профильтровать список, добавьте оператор
if в конце генератора списка. Выражение, стоящее после оператораif, будет вычислено для каждого элемента списка. Если это выражение будет истинно, данный элемент будет обработан и включён в генерируемый список. В данной строке генерируется список всех .py-файлов в текущей директории, а операторifфильтрует этот список, оставляя только файлы размером больше 6000 байт. Таких файлов только шесть, поэтому будет сгенерирован список из шести имён файлов.
Все рассмотренные примеры генераторов списков использовали простые выражения: умножение числа на константу, вызов одной функции или просто возврат элемента списка без изменений (после фильтрации). Но при генерации списков можно использовать выражения любой сложности.
>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')] ①
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
(3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
(3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')] ②
[('3.0 KiB', 'feed-broken.xml'),
('3.3 KiB', 'feed-ns0.xml'),
('3.0 KiB', 'feed.xml')]
- Строка 2. Этот генератор ищет все .xml-файлы в текущем рабочем каталоге, получает размер каждого файла (вызывая функцию
os.stat()), и создает кортеж из размера файла и абсолютного пути каждого файла (вызывая функциюos.path.realpath()). - Строка 7. Этот генератор, основанный на предыдущем, вызывает функцию
approximate_size(), передавая ей размер каждого .xml-файла.
3.4 Генераторы словарей
Генератор словаря похож на генератор списка, но вместо списка он создает словарь.
>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')] ①
>>> metadata[0] ②
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')} ③
>>> type(metadata_dict) ④
<class 'dict'>
>>> list(metadata_dict.keys()) ⑤
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size ⑥
2509
- Строка 2. Это не генератор словаря, это генератор списка. Он находит все файлы с расширением .py, проверяет их имена, а затем создает кортеж из имени файла и метаданных файла (вызывая функцию
os.stat()). - Строка 3. Каждый элемент результирующего списка – кортеж.
- Строка 7. Это генератор словаря. Синтаксис подобен синтаксису генератора списка, но с двумя отличиями. Во-первых, он заключён в фигурные скобки, а не в квадратные. Во-вторых, вместо одного выражения для каждого элемента он содержит два, разделённые двоеточием. Выражение слева от двоеточия (в нашем примере
f) является ключом словаря; выражение справа от двоеточия (в нашем примереos.stat(f)) – значением. - Строка 8. Генератор словаря возвращает словарь.
- Строка 10. Ключи данного словаря – это просто имена файлов, полученные с помощью
glob.glob('*test*.py'). - Строка 16. Значение, связанное с каждым ключом, получено с помощью функции
os.stat(). Это означает, что в этом словаре мы можем по имени файла получить его метаданные. Один из элементов метаданных (st_size) – это размер файла. Размер файла alphameticstest.py – 2509 байт.
Также, как и в генераторах списков, вы можете включать в генераторы словарей условие if, чтобы отфильтровать входную последовательность с помощью выражения-условия, вычисляющегося для каждого элемента.
>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')} ①
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \
... for f, meta in metadata_dict.items() if meta.st_size > 6000} ②
>>> list(humansize_dict.keys()) ③
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9'] ④
'6.5 KiB'
- Строка 2. В этом выражении берётся список файлов в текущей директории (
glob.glob('*')), для каждого файла определяются его метаданные (os.stat(f)) и строится словарь, ключами которого выступают имена файлов, а значениями – метаданные каждого файла. - Строка 4. Этот генератор строится на основе предыдущего. Отфильтровываются файлы меньше 6000 байт (
if meta.st_size > 6000). Отобранные элементы используются для построения словаря, ключами которого являются имена файлов без расширения (os.path.splitext(f)[0]), а значениями – приблизительный размер каждого файла (humansize.approximate_size(meta.st_size)). - Строка 5. Как вам уже известно из предыдущего примера, всего имеется шесть таких файлов, следовательно, в этом словаре шесть элементов.
- Строка 7. Значение для каждого ключа представляет из себя строку, полученную вызовом функции
approximate_size().
3.4.1 Другие интересные штуки, которые можно делать с помощью генераторов словарей
Вот трюк с генераторами словарей, который когда-нибудь может оказаться полезным: перестановка местами ключей и значений словаря.
>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}
Конечно же, это сработает, только если значения элементов словаря неизменяемы, как, например, строки или кортежи.
>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'
3.5 Генераторы множеств
Нельзя оставить за бортом и множества, они тоже могут создаваться с помощью генераторов. Единственное отличие – вместо пар ключ:значение, они строятся на основе одних значений.
>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set} ①
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0} ②
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)} ③
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
- Строка 4. В качестве входных данных генераторы множеств могут получать другие множества. Этот генератор рассчитывает квадраты множества чисел в диапазоне от 0 до 9.
- Строка 6. Подобно генераторам списков и словарей, генераторы множеств могут содержать условие
ifдля проверки каждого элемента перед включением его в результирующее множество. - Строка 8. На вход генераторы множеств могут принимать не только множества, но и любые другие последовательности.
3.6 Материалы для дальнейшего чтения
- Модуль
os os– доступ к особым возможностям операционных систем- Модуль
os.path os.path– манипуляции с именами файлов, независимые от платформы- Модуль
glob glob– сравнение имён файлов с шаблонами- Модуль
time time– Функции для манипулирования временем- Генераторы списков
- Вложенные генераторы списков
- Техника циклов
