Пориньте у Python 3/Вирази над структурами

Наша уява напружена до країв, не як в художній літературі, щоб уявити собі речі яких немає, а щоб зрозуміти ті речі, які насправді є.
Річард Фейнман

Кожна мова програмування має щось, що могло б бути складним, але в ній воно спеціально зроблено просто. Якщо ви прийшли від іншої мови, ви можете це запросто пропустити, через те, що ваша стара мова не робила це просто (тому що вона натомість робила простим щось інше). Цей розділ розповість вам про спискові вирази, словникові вирази, і множинні вирази: три пов’язані ідеї, які зібрані навколо одної дуже потужної техніки. Але спершу, я хочу здійснити невелику екскурсію, в два модулі, які допоможуть вам переміщуватись по файловій системі.


* * *


Робота з файлами та каталогами ред.

Python 3 поширюється з модулем os, що містить купу функцій для отримання інформації і у деяких випадках для маніпуляції, локальними каталогами, файлами, процесами та змінними середовища. Python робить все що може, щоб надати уніфіковане API на всіх підтримуваних операційних системах щоб ваші програми могли працювати на будь-якому комп’ютері, з якомога меншою кількістю платформо-залежного коду.

Поточна робоча директорія ред.

Якщо ви тільки-но встановили Python, ви будете проводити багато часу в інтерактивній оболонці. Протягом цієї книжки, ви побачити приклади, які працюють за такою схемою:

  1. Імпортують один з прикладів з каталогу examples.
  2. Викликають функцію з того модуля
  3. Пояснюють результат
Хоч якийсь з каталогів завжди повинен бути поточним робочим.

Якщо ви не знаєте свою поточну робочу директорію, перший крок напевне буде невдалим, і закінчиться з ImportError. Чому? Бо Python буде шукати модуль в шляхах пошуку імпортів, але не знайде, бо там немає каталогу examples. Щоб це виправити можна зробити дві речі:

  1. Додати каталог examples в шляхи імпорту.
  2. Змінити поточну робочу директорію на examples

Поточна робоча директорія - невидима змінна яку Python ввесь час зберігає в пам’яті. Поточна робоча директорія завжди задана, незалежно чи ви в інтерактивній оболонці, запустили скрипт з командного рядка, чи виконуєте CGI скрипт на якомусь веб-сервері.

Модуль os містить дві функції для роботи з поточною робочою директорією.

>>> import os

Він поширюється разом з Python, і його можна імпортувати будь-де в будь-який момент.

>>> print(os.getcwd())
C:\Python31

Щоб визначити поточну робочу директорію використайте функцію os.getcwd(). Коли ви запустаєте графічну оболонку інтерпретатора - поточна директорія зазвичай та, де знаходиться виконуваний файл ітерпретатора. На Windows це зазвиай там, куди ви встановили Python, за замовучуванням C:\Python31. Якщо ви запускаєте інтерпретатор з командної оболонки, то поточна робоча директорія - та сама що й директорія в якій ви були, коли запустили python3.

>>> os.chdir('/Users/pilgrim/diveintopython3/examples')

Використовуйте функцію os.chdir() щоб змінити поточну робочу директорію.

>>> print(os.getcwd())
C:\Users\pilgrim\diveintopython3\examples

Коли я викликав функцію os.chdir(), я використав Лінукс-синтаксис запису шляху (прямі слеші, відсутні буквенні позначення логічних дисків) навіть незважаючи на те, що я в цей момент працюю на Windows. Це одне з місць в яких Python намагається стерти різницю між операційними системами.

Робота з іменами файлів та директорій ред.

Поки ми говоримо про директорії, я хочу вказати на те, що модуль os.path містить функції для маніпулювання іменами файлів та директорій.

>>> import os 
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py'))
/Users/pilgrim/diveintopython3/examples/humansize.py

Функція os.path.join() складає шлях з кількох часткових шляхів. В даному випадку - просто послідовно з’єднує рядки.

>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py'))
/Users/pilgrim/diveintopython3/examples\humansize.py

В цьому дещо менш тривіальному випадку, виклик os.path.join() додає додатковий слеш до імені перед його приєднанням до імені файла. Це зворотній, а не прямий слеш, тому що я пишу цей приклад під Windows. Якщо ви повторите такі ж дії в Linux чи Mac OS X, ви побачите замість цього прямий слеш. Не жартуйте зі слешами, завжди використовуйте os.path.join() і дозвольте мові Python зробити все правильно.

>>> print(os.path.expanduser('~'))
c:\Users\pilgrim

Функція os.path.expanduser() розгортає шлях який використовує ~ для позначення домашнього каталога корисувача в повний. Вона працює на будь-якій платформі де користувачі можуть мати домашню категорію, включно з Linux, Mac OS X та Windows. Шлях який повертається не містить останнього слеша, але функція os.path.join() не звертає на таке увагу.

>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py'))
c:\Users\pilgrim\diveintopython3\examples\humansize.py

Комбінуючи ці техніки, можна просто конструювати шляхи для директорій та файлів в домашньому каталозі. Функція os.path.join() приймає будь-яку кількість аргументів. Я дуже радів, коли відкрив це, тому що addSlashIfNecessary() - одна з тих дурних маленьких функцій які я завжди писав, коли створював свій інструментарій в новій мові. Не пишіть такі малі дурні функції в Python - про це вже замість вас подбали розумні люди.

os.path також містить функції для поділу повних імен файлів та каталогів на їх складові частини.

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname)
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')

Функція split() розділяє повне ім'я файла, і повертає кортеж що містить шлях та назву.

>>> (dirname, filename) = os.path.split(pathname)

Пам'ятаєте, коли я казав що можна використовувати множинне присвоювання щоб отримувати кілька значень з функції? os.path.split() - саме така функція. Ми присвоюємо результат її роботи кортежу з двох змінних. Кожна змінна приймає значення відповідного елемента кортежу що повертається.

>>> dirname
'/Users/pilgrim/diveintopython3/examples'

Перша змінна, dirname, приймає значення першого елементу результату os.path.split() - шлях до файла.

>>> filename
'humansize.py'

Друга змінна, filename приймає значення другого елементу кортежу - ім'я файла.

>>> (shortname, extension) = os.path.splitext(filename)
>>> shortname
'humansize'
>>> extension
'.py'

Модуль os.path також містить функцію os.path.splitext(), яка відділяє від імені файла його розширення, і повертає кортеж з двох елементів. Ви можете використати подібну техніку щоб присвоїти ці значення різним змінним.

Лістинги директорій ред.

Модуль glob використовує шаблони як в командному рядку

Модуль glob - інший інструмент зі стандартної бібліотеки. Це простий спосіб отримувати вміст директорій в своїх програмах, і він використовує шаблони з якими ви вже могли бути знайомими працюючи з командного рядка.

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml')
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']

Модуль glob отримує шаблон, і повертає шляхи до всіх файлів і директорій які цьому шаблону відповідають. Наприклад, шаблон "*.xml", відповідає будь-якому файла з розширенням .xml.

>>> os.chdir('examples/')

Тепер перейдемо в підкаталог examples функція os.chdir() може приймати відносні шляхи.

>>> 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']

В шаблон можна додавати кілька загальних символів. НАприклад в цьому прикладі шаблон шукає всі файли, назви яких закінчуються розширенням .py і містять слово test де-небудь.

Отримання метаданих файла ред.

Кожна сучасна файлова система зберігає певні метадані про кожен файл: час створення, час останньї зміни, розмір і т.п. Python надає єдине API для доступу до цих метаданих. Вам не потрібно відкривати файл, все що потрібно, це назва файла.

>>> import os
>>> print(os.getcwd())                
c:\Users\pilgrim\diveintopython3\examples

Поточна робоча директорія - examples.

>>> metadata = os.stat('feed.xml')

feed.xml - файл в каталозі examples. Виклик функції os.stat() повертає об'єкт, який містить кілька різних видів відомостей про файл.

>>> metadata.st_mtime                  
1247520344.9537716

st_mtime - час модифікації, але записаний в не надто зручному форматі. (Технічно це кількість секунд від початку Епохи, яка почалася першого січня 1970-того року. Серйозно.)

>>> import time

Модуль 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)

Функція time.localtime() перетворює часове значення часу з секунд-від-початку-епохи (які є властивістю файла st_mtime значення якої повернула нам функція os.stat()) в більш зручну структуру з року, місяця, дня, години, хвилини, секунд, і так далі.

>>> metadata.st_size                              
3070

Функція os.stat() також повертає розмір файла в атрибуті st_size. Файл feed.xml містить 3070 байт.

>>> import humansize
>>> humansize.approximate_size(metadata.st_size)  
'3.0 KiB'

Ми можемо передати атрибут st_size функції approximate_size().

Отримання абсолютних шляхів ред.

У попередній секції функція glob.glob() повернула список відносних шляхів. Перший приклад використовував шляхи вигляду 'examples\feed.xml', а другий - навіть коротші відносні шляхи що складались лише з імені файла. Поки ви залишаєтесь в тій самій поточній директорії, ці відносні шляхи будуть працювати для відкриття файлів та отримання їх метаданих. Але якщо вам потрібно отримати абсолютні шляхи (тобто такі що включають всі директорії аж до кореневої, або до літери що позначає диск) - тоді вам потрібна функція 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


* * *


Спискові вирази ред.

Спискові вирази можуть містити довільні вирази мови Python

Спискові вирази (англ. list comprehensions) - це компактний спосіб створення списку з іншої послідовності, застосовуючи певну функцію до кожного елемента послідовності. (Іноді українською їх ще називали генераторні списки, але цей термін невдалий, тому що генератори, і генераторні вирази - зовсім інша історія).

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list]
[2, 18, 16, 8]

Щоб зрозуміти що твориться прочитайте вираз справа наліво. a_list - список з якого ми беремо елементи. Інтерпретатор бере кожний елемент по черзі, і тимчасово присвоює його значення змінній elem. Після цього обчислює значення функції elem * 2 і додає результат до списку який повертається.

>>> a_list
[1, 9, 8, 4]

Спискові вирази створюють нові списки, старі залишаються незмінними.

>>> a_list = [elem * 2 for elem in a_list]
>>> a_list
[2, 18, 16, 8]

Крім того, можна цілком безпечно присвоювати результат спискового виразу змінній яка використовується у виразі. Python спочатку створить новий список в пам’яті, і тільки після завершення обчислення його елементів присвоїть отримані значення оригінальній змінній.

Всередині спискових виразів можна використовувати довільні вирази мови Python, в тому числі і функції модуля os.

>>> import os, glob
>>> glob.glob('*.xml')                                 
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']

Це поверне список всіх .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']

А цей списковий вираз перетворить список імен файлів на список повних шляхів до них.

Спискові вирази також можуть фільтрувати елементи, включаючи в кінцевий список не кожні з них.

>>> 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']

Щоб відфільтрувати елементи списку можна додати оператор 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')]

Цей списковий вираз знаходить всі xml-файли в поточному каталозі, отримує розмір кожного файла (викликаючи функцію os.stat()), і створює кортеж з розміру файла і абсолютного шляху до нього (викликаючи функцію os.path.realpath()).

>>> 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')]

А цей списковий вираз - покращення попереднього за допомогою виклику функції approximate_size(), для розміру кожного файла.


* * *


Словникові вирази ред.

Словникові вирази - це так само як спискові вирази, тільки в результаті ми отримуємо словник а не список.

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]

Це не словниковий вираз, це все ще списковий вираз. Він знаходить всі файли .py, що містять в імені слово test, і будує кортежі з імені файла і його метаданих (які повертаються функцією os.stat()).

>>> 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')}

А оце - словниковий вираз. Синтаксис подібний до спискового виразу, з двома основними відмінностями. По-перше, його поміщено в фігурні дужки замість квадратних. По-друге, замість єдиного виразу для кожного елемента, він містить два вирази розділені двокрапкою. Вираз перед двокрапкою (в цьому прикладі f) є ключем, а вираз після двокрапки (в даному випадку os.stat(f)) - значення.

>>> 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']

Ключами даного словника є імена файла отримані з функції glob.glob('*test*.py').

>>> metadata_dict['alphameticstest.py'].st_size
2509

Значення пов’язане з кожним ключем було обчислене функцією 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}

Цей словниковий вираз побудовано на основі попереднього. Він відбирає тільки файли розмір яких більший за 6000 байт (if meta.st_size > 6000), та використовує їх щоб створити словник, ключами якого є імена файлів без розширення (os.path.splitext(f)[0]) а значення яких - приблизний розмір кожного файла (humansize.approximate_size(meta.st_size)).

>>> list(humansize_dict.keys()) 
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']

Ми вже бачили що таких файлів шість.

>>> humansize_dict['romantest9'] 
'6.5 KiB'

Значення кожного ключа - рядок що обчислений функцією approximate_size().

Інші трюки з словниковими виразами ред.

Ось трюк з використанням словникого виразу, який можливо колись стане в нагоді: обмін місцями ключів і значень словника.

>>> 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'


* * *


Множинні вирази ред.

Ну, і не варто забувати про те, що множини теж мають синтаксис виразів які дозволяють їх утворювати. Він надзвичайно схожий на синтаксис словникових виразів, тільки містить звичайні значення замість пар ключ:значення.

>>> 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}

Множинні вирази (як і всі інші) можуть приймати на вхід множину. Тут ми будуємо множину квадратів натуральних чисел від нуля до 9.

>>> {x for x in a_set if x % 2 == 0}  
{0, 8, 2, 4, 6}

І як і всі попередні, множинні вирази теж можуть містити оператор if для фільтрації.

>>> {2**x for x in range(10)}         
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}

Ну, і множинним виразам не обов’язково використовувати як базові дані множину, підійде будь-яка послідовність.


* * *


Для подальшого читання ред.

Стандартні типи даних · Текст