Глава 3. Генераторы

Добавлено 4 апреля 2020 в 01:43

Нам приходится сильнее напрягать свое воображение не для того, чтобы, как в художественной литературе, представить себе то, чего нет на самом деле, а для того, чтобы постичь то, что действительно происходит.

Ричард Фейнман

Содержание главы

Нам приходится сильнее напрягать свое воображение не для того, чтобы, как в художественной литературе, представить себе то, чего нет на самом деле, а для того, чтобы постичь то, что действительно происходит.
Ричард Фейнман

Погружение

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

3.2 Работа с файлами и каталогами

Python 3 поставляется с модулем os, что означает «операционная система». Модуль os содержит множество функций для получения информации о локальных каталогах, файлах, процессах и переменных окружения (а в некоторых случаях, и для манипулирования ими). Python предлагает очень хороший унифицированный программный интерфейс для всех поддерживаемых операционных систем, так что ваши программы можно запускать на любом компьютере с минимальным количеством платформо-зависимого кода.

3.2.1 Текущий рабочий каталог

Всегда есть текущий рабочий каталог.

Когда ваше знакомство с Python только начинается, вы много времени проводите в интерактивной оболочке Python. На протяжении всей этой книги вы будете видеть примеры, выглядящие следующим образом:

  1. Импортирование какого-либо модуля из папки примеров
  2. Вызов функции из этого модуля
  3. Объяснение результата

Если вы ничего не знаете о текущем рабочем каталоге, то, возможно, шаг 1 окажется неудачным и будет порождено исключение типа ImportError. Почему? Потому что Python будет искать указанный модуль в пути поиска оператора import, но не найдёт его, потому что каталог examples не содержится в путях поиска. Чтобы исправить это, вы можете сделать одно из двух:

  1. либо добавить папку examples в путь поиска оператора import;
  2. либо сделать текущим рабочим каталогом папку 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. Строка 1. Модуль os поставляется вместе с Python; вы можете импортировать его когда угодно и где угодно.
  2. Строка 2. Используйте функцию os.getcwd() для получения значения текущего рабочего каталога. Когда вы работаете в графической оболочке Python, текущим рабочим каталогом является каталог из которого она была запущена. В Windows это зависит от того, куда вы установили Python; каталог по умолчанию c:\Python31. Если оболочка Python запущена из командной строки, текущим рабочим каталогом считается тот, в котором вы находились, когда запускали её.
  3. Строка 4. Используйте функцию os.chdir() чтобы сменить текущий рабочий каталог.
  4. Строка 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
  1. Строка 2. Функция os.path.join() составляет путь из одного или нескольких частичных путей. В данном случае она просто соединяет строки.
  2. Строка 4. Это уже менее тривиальный случай. Функция join добавит дополнительную косую черту (slash) к имени папки перед тем как дописать имя файла. В данном случае Python добавляет обратную косую черту (backslash) вместо обыкновенной, потому что я запустил этот пример в Windows. Если вы введёте данную команду в Linux или Mac OS X, вы увидите простую косую черту. Python может обратиться к файлу независимо от того, какой разделитель используется в пути к файлу.
  3. Строка 6. Функция os.path.expanduser() раскрывает путь, в котором используется символ ~ для обозначения домашнего каталога текущего пользователя. Функция работает на любой платформе, где у пользователя есть домашний каталог, включая Linux, Mac OS X и Windows. Функция возвращает путь без косой черты в конце, но для функции os.path.join() это не имеет значения.
  4. Строка 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'
  1. Строка 2. Функция split дробит полный путь и возвращает кортеж, содержащий отдельно путь до каталога и имя файла.
  2. Строка 4. Помните, я рассказывал про то, как присваивать несколько значений за раз и как вернуть одновременно несколько значений из функции? Функция os.path.split() действует именно так. Можно присвоить возвращаемое из функции split значение кортежу из двух переменных. Каждая из переменных примет значение соответствующего элемента результирующего кортежа.
  3. Строка 5. Первая переменная – dirname – получит значение первого элемента кортежа, возвращаемого функцией os.path.split(), а именно путь до каталога.
  4. Строка 7. Вторая переменная – filename – примет значение второго элемента кортежа, возвращаемого функцией os.path.split(), а именно имя файла.
  5. Строка 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']
  1. Строка 3. Модуль glob принимает шаблон, содержащий символы-джокеры, и возвращает пути всех файлов и каталогов, соответствующих ему. В этом примере шаблон содержит путь к каталогу и "*.xml", которому будут соответствовать все xml-файлы в каталоге examples.
  2. Строка 7. Теперь сделаем текущим рабочим каталог examples. Функция os.chdir() может принимать и относительные пути.
  3. Строка 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)
  1. Строка 2. Текущий рабочий каталог – папка с примерами.
  2. Строка 4.feed.xml – файл в папке с примерами. Вызов функции os.stat() возвращает объект, содержащий различные метаданные о файле.
  3. Строка 5.st_mtime – время изменения файла, но записано оно в ужасно неудобном формате. (Фактически это количество секунд, прошедших с начала «эры UNIX», начавшейся в первую секунду 1 января 1970 года. Серьёзно.)
  4. Строка 7. Модуль time является частью стандартной библиотеки Python. Он содержит функции для преобразований между различными форматами представления времени и часовыми поясами, для преобразования их в строки (str) и др.
  5. Строка 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'
  1. Строка 2. Функция os.stat() также возвращает размер файла в свойстве st_size. Размер файла feed.xml – 3070 байт.
  2. Строка 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]
  1. Строка 2. Чтобы понять, что здесь происходит, прочитайте генератор справа налево. a_list – отображаемый список. Python последовательно перебирает элементы списка a_list, временно присваивая значение каждого элемента переменной elem. Затем применяет функцию elem * 2 и добавляет результат в возвращаемый список.
  2. Строка 4. Генератор создаёт новый список, не изменяя исходный.
  3. Строка 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']
  1. Строка 2. Это выражение возвращает список всех .xml-файлов в текущем рабочем каталоге.
  2. Строка 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']
  1. Строка 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')]
  1. Строка 2. Этот генератор ищет все .xml-файлы в текущем рабочем каталоге, получает размер каждого файла (вызывая функцию os.stat()), и создает кортеж из размера файла и абсолютного пути каждого файла (вызывая функцию os.path.realpath()).
  2. Строка 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
  1. Строка 2. Это не генератор словаря, это генератор списка. Он находит все файлы с расширением .py, проверяет их имена, а затем создает кортеж из имени файла и метаданных файла (вызывая функцию os.stat()).
  2. Строка 3. Каждый элемент результирующего списка – кортеж.
  3. Строка 7. Это генератор словаря. Синтаксис подобен синтаксису генератора списка, но с двумя отличиями. Во-первых, он заключён в фигурные скобки, а не в квадратные. Во-вторых, вместо одного выражения для каждого элемента он содержит два, разделённые двоеточием. Выражение слева от двоеточия (в нашем примере f) является ключом словаря; выражение справа от двоеточия (в нашем примере os.stat(f)) – значением.
  4. Строка 8. Генератор словаря возвращает словарь.
  5. Строка 10. Ключи данного словаря – это просто имена файлов, полученные с помощью glob.glob('*test*.py').
  6. Строка 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'
  1. Строка 2. В этом выражении берётся список файлов в текущей директории (glob.glob('*')), для каждого файла определяются его метаданные (os.stat(f)) и строится словарь, ключами которого выступают имена файлов, а значениями – метаданные каждого файла.
  2. Строка 4. Этот генератор строится на основе предыдущего. Отфильтровываются файлы меньше 6000 байт (if meta.st_size > 6000). Отобранные элементы используются для построения словаря, ключами которого являются имена файлов без расширения (os.path.splitext(f)[0]), а значениями – приблизительный размер каждого файла (humansize.approximate_size(meta.st_size)).
  3. Строка 5. Как вам уже известно из предыдущего примера, всего имеется шесть таких файлов, следовательно, в этом словаре шесть элементов.
  4. Строка 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}
  1. Строка 4. В качестве входных данных генераторы множеств могут получать другие множества. Этот генератор рассчитывает квадраты множества чисел в диапазоне от 0 до 9.
  2. Строка 6. Подобно генераторам списков и словарей, генераторы множеств могут содержать условие if для проверки каждого элемента перед включением его в результирующее множество.
  3. Строка 8. На вход генераторы множеств могут принимать не только множества, но и любые другие последовательности.

3.6 Материалы для дальнейшего чтения

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

PythonОбучениеПрограммированиеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.