Язык Python
Стандартные модули II
В этом разделе мы продолжаем разговор о модулях стандартной библиотеки python. Формат курса не позволяет обсуждать их в деталях, поэтому в большинстве случаев показаны только простые и наиболее характерные примеры использования различных инструментов.
Сериализация объектов
Сериализация — это процесс перевода объектов в формат, в котором их можно хранить и передавать. Обратный процесс называется десериализацией.
Текстовое представление
В разделе про ООП мы обсуждали метод __repr__
, который возвращает текстовое представления объекта. В комбинации с функцией literal_eval
модуля ast
его можно использовать для сериализации и десериализации объектов python:
import ast
# создаем список объектов различных стандартных типов
data = [
1, 2, 'a',
['2', 1],
(3, 2, 1),
{x: y**2 for x, y in enumerate(range(3))}
]
# сохраняем текстовое представление в файл
with open('s1.txt', 'w') as f:
f.write(repr(data))
# читаем файл и конструируем точную копию исходного объекта
with open('s1.txt', 'r') as f:
restored_data = ast.literal_eval(f.read())
type(restored_data) # <class 'list'>
type(restored_data[4]) # <class 'tuple'>
print(restored_data)
# [1, 2, 'a', ['2', 1], (3, 2, 1), {0: 0, 1: 1, 2: 4}]
Модуль pickle
Модуль pickle
содержит инструменты для сериализации стандартных объектов python в последовательность байтов:
# сохраняем бинарное представление в файл
with open('s1.dat', 'wb') as f:
# используем объект data из предыдущего примера
pickle.dump(data, f)
# читаем файл и конструируем точную копию исходного объекта
with open('s1.dat', 'rb') as f:
restored_data = pickle.load(f)
type(restored_data) # <class 'list'>
type(restored_data[4]) # <class 'tuple'>
print(restored_data)
# [1, 2, 'a', ['2', 1], (3, 2, 1), {0: 0, 1: 1, 2: 4}]
Сравним скорость выполнения этого и предыдущего примеров. Запись и чтение данных в текстовом формате занимает около 180 микросекунд, в то время как использование бинарного представления с применением модуля pickle
требует около 80 микросекунд для аналогичной операции.
Модуль json
Формат JSON (JavaScript Object Notation) очень популярен при передаче данных в сети. Этот формат переводит объекты в текстовое представление, которое не привязано к языку программирования.
Стандартные структуры данных могут быть сериализованы с модулем json следующим образом:
import json
with open('s1.txt', 'w') as f:
json.dump(data, f)
with open('s1.txt', 'r') as f:
restored_data = json.load(f)
Чтобы определить правила сериализации для нового типа данных, необходимо определить класс-наследник класса json.JSONEncoder
. Во многих случаях объекты пользовательских типов могут быть сериализованы через атрибут __dict__
. Подробное обсуждение сериализации произвольных объектов выходит за рамки этого обзора.
Стандартная библиотека python содержит и другие модули, позволяющие выполнять сериализацию данных, например, CSV и XML. Из нестандартных инструментов упомянем библиотеку pyyaml и мощную библиотеку protobuf для сериализации от компании Google.
Сжатие данных
Модуль zlib
Модуль zlib
предоставляет интерфейс к библиотеке zlib языка C и содержит инструменты для архивирования и разархивирования последовательности байтов.
Функция zlib.compress
принимает последовательность байтов и возвращает объект, содержащий сжатые данные:
import zlib
import sys
import binascii
s = b'Compress me!' * 100
cs = zlib.compress(s, -1)
print(binascii.hexlify(cs))
# b'789c73cecf2d284a2d2e56c84d55741e658fb247d9a3ec41cc06003a6ab52c'
len(s) # 1200
len(cs) # 31
sys.getsizeof(s) # 1233
sys.getsizeof(cs) # 64
Второй параметр функции zlib.compress
— целое число от -1 до 9 — определяет степень и скорость сжатия. Значение -1 соответствует степени сжатия по умолчанию 6. Чтобы сжать файл, достаточно прочитать его содержимое в бинарной моде и передать в функцию zlib.compress
.
Функция zlib.decompress
выполняет обратное преобразование:
zlib.decompress(cs).decode()[:12]
# Compress me!
Модуль zipfile
Модуль zipfile
позволяет создавать и читать zip-архивы:
import os
import zipfile
work_dir = './'
fname = os.path.join(work_dir, 'text.txt')
zipname = os.path.join(work_dir, 'text.zip')
# создаем файл, который будем сжимать
with open(fname, 'w') as f:
f.write('Compress me!'*10000)
# создаем zip-архив
with zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) as zipObj:
# ZIP_STORED — сжатие отсутствует
# ZIP_DEFLATED — обычное ZIP-сжатие, использует модуль zlib
# ZIP_BZIP2 — метод сжатия BZIP2, использует модуль bz2
# ZIP_LZMA — метод сжатия LZMA, использует модуль lzma
zipObj.write(fname)
# проверяем, что архив создан и узнаем какой у него размер
os.path.getsize(fname) # 120000
os.path.getsize(zipname) # 379
# удалаяем исходный файл
os.remove(fname)
assert not os.path.isfile(fname)
# читаем архив
with zipfile.ZipFile(zipname, 'r') as zipObj:
zipObj.extractall()
# открываем данные, полученные из архива
with open(fname, 'r') as f:
s = f.read()
print(s[:12]) # Compress me!
Модуль tarfile
Модуль tarfile
позволяет работать с tar-архивами:
import os
import tarfile
work_dir = './'
fname = os.path.join(work_dir, 'text.txt')
tarname = os.path.join(work_dir, 'text.tar')
with open(fname, 'w') as f:
f.write('Compress me!'*10000)
with tarfile.open(tarname, 'w:gz') as targz:
# 'w|' — сжатие отсутствует
# 'w:gz' — сжатие gzip
# 'w|bz2' — сжатие bzip2
# 'w|xz' — сжатие lzma
targz.add(fname)
os.path.getsize(fname) # 120000
os.path.getsize(tarname) # 445
os.remove(fname)
assert not os.path.isfile(fname)
with tarfile.open(tarname, 'r') as tar:
tar.extractall()
with open(fname, 'r') as f:
s = f.read()
print(s[:12]) # Compress me!
Для работы с разными типами архивов модуль tarfile
использует инструменты из модулей gzip
, bz2
и lzma
.
Кроме рассмотренных модулей, для работы с архивами можно использовать функции make_archive
и unpack_archive
модуля shutil.
Тестирование кода
Тестирование является важной частью разработки на python. В отсутствие проверок компилятора, тесты различных частей кода помогают обеспечить доверие к работе программы. В дополнение к тестам полезно использовать статические анализаторы кода, но мы не будем касаться этой темы.
Тесты кода можно разделить на два типа: тесты, проверяющие потребление ресурсов (времени и памяти), и тесты, проверяющие логику работы программы. Рассмотрим примеры выполнения тестов обоих типов.
Модуль timeit
Модуль timeit
содержит инструменты для измерения времени работы небольших частей кода. Этот модуль можно использовать двумя способами: через консольный интерфейс и через вызов функций внутри кода. Начнем с интерфейса командной строки и классического сравнения функции map
, генераторного выражения и спискового включения:
$ python -m timeit -r 10 -n 1000 'sum([x**2 for x in range(10000)])'
1000 loops, best of 10: 2.84 msec per loop
$ python -m timeit -r 10 -n 1000 'sum(map(lambda x: x**2, range(10000)))'
1000 loops, best of 10: 3.01 msec per loop
$ python -m timeit -r 10 -n 1000 'sum(x**2 for x in range(10000))'
1000 loops, best of 10: 2.79 msec per loop
Параметр -n
определяет количество повторений, по которым будет вычисляться среднее время выполнения, параметр -r
определяет количество повторений всей процедуры. Также можно использовать параметр -s
, который позволяет задавать окружение, например:
-s 'import math'
Покажем теперь вызов модуля timeit
в коде и добавим в сравнение отрывок кода с циклом for
:
import timeit
code1 = '''
sum=0
for i in range(10000):
sum += i**2
'''
code2 = 'sum([x**2 for x in range(10000)])'
code3 = 'sum(map(lambda x: x**2, range(10000)))'
code4 = 'sum(x**2 for x in range(10000))'
for idx, c in enumerate([code1, code2, code3, code4]):
print(f'code {idx+1}', end=': ')
print(f'{timeit.timeit(c, number=1000):.5f}')
# code 1: 2.87627
# code 2: 2.83845
# code 3: 3.04344
# code 4: 2.83078
Функция timeit.repeat
выполняет несколько тестов подряд (аналогично аргументу командной строки -r
), что позволяет оценить разброс результатов и оценить точность измерения:
for idx, c in enumerate([code1, code2, code3, code4]):
res = timeit.repeat(c, number=1000, repeat=5)
print(' '.join([f'{item:.3f}' for item in res]))
# code 1: 2.909 2.874 2.938 2.894 2.872
# code 2: 2.788 2.786 2.768 2.802 2.792
# code 3: 3.041 3.037 3.070 3.046 3.050
# code 4: 2.842 2.824 2.845 2.848 2.831
Функции модуля timeit
могут принимать и другие аргументы. Детали можно найти в документации.
Модуль unittest
Модуль unittest
предоставляет большой набор инструментов для модульного тестирования кода. Покажем базовые приемы тестирования на примере функции quad_eq
, которая должна принимать числа a
, b
и c
и возвращать действительные корни квадратного уравнения ax^2+bx+c=0
:
# файл quadeq.py
from math import sqrt
def quad_eq(a, b, c):
D = b**2 - 4*a*c
if D < 0:
return []
elif D == 0:
return [-b/2 * a]
return [
(-b - sqrt(D)) / (2*a),
(-b + sqrt(D)) / (2*a)
]
Найдем здесь ошибку с помощью тестов. Для этого определим наследника класса unittest.TestCase
. Методы этого класса, имена которых начинаются с символов test
, задают набор тестов. В методах-тестах вызываются специальные методы класса unittest.TestCase
, выполняющие проверки:
# файл test_quadeq.py
from quadeq import quad_eq
import unittest
class TestQuadEq(unittest.TestCase):
def test_integer_roots(self):
# assertEqual(a, b) проверяет равенство a и b
self.assertEqual(quad_eq(1, 3, 2), [-2, -1])
self.assertEqual(quad_eq(1, -1, -2), [-1, 2])
self.assertEqual(quad_eq(2, -2, -4), [-1, 2])
def test_single_root(self):
self.assertEqual(quad_eq(1, 2, 1), [-1])
self.assertEqual(quad_eq(2, 4, 2), [-1])
self.assertEqual(quad_eq(1, 6, 9), [-3])
def test_no_roots(self):
# assertFalse(a) проверяет, что bool(a) равно False
self.assertFalse(quad_eq(1, 1, 1))
self.assertFalse(quad_eq(1, 0, 1))
def test_linear_equation(self):
self.assertEqual(quad_eq(0, 1, 1), [-1])
self.assertEqual(quad_eq(0, 2, 2), [-1])
def test_not_an_equation(self):
self.assertFalse(quad_eq(0, 0, 1))
self.assertFalse(quad_eq(0, 0, 0))
def test_wrong_type(self):
# assertRaises(exception, callable) проверяет, что вызов
# callable приводит к исключению типа exception
self.assertRaises(TypeError, lambda: quad_eq(1, 2, '1'))
self.assertRaises(TypeError, lambda: quad_eq(1, '2', 1))
self.assertRaises(TypeError, lambda: quad_eq('1', 2, 1))
if __name__ == '__main__':
unittest.main()
Эти тесты покрывают не все возможные случаи, однако их вполне достаточно для иллюстрации (и обнаружения нашей ошибки). Запустим тестирование:
$ python test_quadeq.py
.E.FF.
======================================================================
ERROR: test_linear_equation (__main__.TestQuadEq)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_quadeq.py", line 22, in test_linear_equation
self.assertEqual(quad_eq(0, 1, 1), [-1])
File "/home/vitaly/work/CppAndPython/playground/quadeq.py", line 10, in quad_eq
(-b - sqrt(D)) / (2*a),
ZeroDivisionError: float division by zero
======================================================================
FAIL: test_not_an_equation (__main__.TestQuadEq)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_quadeq.py", line 26, in test_not_an_equation
self.assertFalse(quad_eq(0, 0, 1))
AssertionError: [0.0] is not false
======================================================================
FAIL: test_single_root (__main__.TestQuadEq)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_quadeq.py", line 13, in test_single_root
self.assertEqual(quad_eq(2, 4, 2), [-1])
AssertionError: Lists differ: [-4.0] != [-1]
First differing element 0:
-4.0
-1
- [-4.0]
+ [-1]
----------------------------------------------------------------------
Ran 6 tests in 0.006s
FAILED (failures=2, errors=1)
Модуль unittest
обнаружил класс TestQuadEq
и выполнил тесты. Первая строка
.E.FF.
показывает, что три теста завершились успешно (символы '.'
), в двух из тестов была провалена проверка в assert-методе класса unittest.TestCase
(символ 'F'
) и при выполнении еще одного теста было выброшено исключение (символ 'E'
). Отчет об ошибках, который вывел unittest
, позволяет понять, что мы забыли проверить некоторые частные случаи перед применением основной формулы и поставить скобки около 2*a
при отрицательном дискриминанте. Внесем необходимые изменения:
# файл quadeq.py
from math import sqrt
def quad_eq(a, b, c):
if a == 0:
return [-c / b] if b != 0 else []
D = b**2 - 4*a*c
if D < 0:
return []
elif D == 0:
return [-b / (2*a)]
return [
(-b - sqrt(D)) / (2*a),
(-b + sqrt(D)) / (2*a)
]
Теперь все тесты будут пройдены успешно:
python test_quadeq.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
Вызов с параметром -v
(verbose) приведет к более подробному выводу:
$ python test_quadeq.py -v
test_integer_roots (__main__.TestQuadEq) ... ok
test_linear_equation (__main__.TestQuadEq) ... ok
test_no_roots (__main__.TestQuadEq) ... ok
test_not_an_equation (__main__.TestQuadEq) ... ok
test_single_root (__main__.TestQuadEq) ... ok
test_wrong_type (__main__.TestQuadEq) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
Заметим, что с последним тестом нам повезло, поскольку в функции quad_eq
нет явной проверки правильности типов аргументов. Рассмотренный пример показывает далеко не все возможности модуля unittest
. Полное описание модуля смотрите в документации.
Модуль unittest
— не единственное средство для модульного тестирования в python. Для проверки решений заданий в этом курсе используется pytest
— другая популярная библиотека для тестирования.
Модуль doctest
Хорошей практикой при написании комментариев к коду является включение в текст примеров использования функций. Модуль doctest
позволяет исполнять такие примеры и проверять, что приведенный код действительно приводит к ожидаемому результату. Включим некоторые примеры в строку описания функции quad_eq
и проверим их работу с помощью модуля doctest
:
# файл quadeq.py
from math import sqrt
import doctest
def quad_eq(a, b, c):
""" Finds rational roots of the equation ax^2 + bx + c = 0
and returns a list of roots in ascending order.
>>> quad_eq(1, 3, 2)
[-2.0, -1.0]
>>> quad_eq(1, 2, 1)
[-1.0]
>>> quad_eq(1, 1, 1)
[]
>>> quad_eq(0, 0, 0)
[]
"""
if a == 0:
return [-c / b] if b != 0 else []
D = b**2 - 4*a*c
if D < 0:
return []
elif D == 0:
return [-b / (2*a)]
return [
(-b - sqrt(D)) / (2*a),
(-b + sqrt(D)) / (2*a)
]
if __name__ == '__main__':
doctest.testmod()
Запускаем:
python quadeq.py
Все примеры были запущены и во всех случаях было получены ожидаемые результаты. В консоль при этом ничего не было выведено. Чтобы убедиться в том, что все произошло именно так, запустим программу еще раз, включив моду verbose:
$ python quadeq.py -v
Trying:
quad_eq(1, 3, 2)
Expecting:
[-2.0, -1.0]
ok
Trying:
quad_eq(1, 2, 1)
Expecting:
[-1.0]
ok
Trying:
quad_eq(1, 1, 1)
Expecting:
[]
ok
Trying:
quad_eq(0, 0, 0)
Expecting:
[]
ok
1 items had no tests:
__main__
1 items passed all tests:
4 tests in __main__.quad_eq
4 tests in 2 items.
4 passed and 0 failed.
Test passed.
Работает.
Разработка программы может опираться на процедуру тестирования. При таком подходе сначала пишутся тесты, и только после этого — основная программа. Эта техника позволяет сразу писать код, который удовлетворяет необходимым контрактам, и быстро обнаруживать различные ошибки. Покрытие кода тестами является признаком хорошего программного продукта. Не пренебрегайте модульным тестированием своих программ.
Веб-программирование
Язык python хорошо подходит для работы с сетью: его стандартная библиотека имеет модули для работы с различными протоколами (например, HTTP, FTP, SMTP); очень популярны фреймворки для разработки веб-сайтов на python (например, Django). Подробный разговор об этом выходит далеко за пределы этого курса, однако несколько примеров работы с сетью мы всё же рассмотрим.
Модуль urllib
Модуль urllib.requests
содержит функции и классы для доступа к ресурсам через URL (uniform resource locator, унифицированный указатель ресурса). Рассмотрим несколько примеров его использования с протоколом HTTP(S).
Некоторые веб-сайты предоставляют специальные инструменты для программного доступа к данным (API — application programming interface, программный интерфейс приложения). Так, социальная сеть Вконтакте имеет хорошо проработанный API. Получим имя пользователя по его идентификатору:
import urllib.request
import urllib.parse
import json
protocol='https://'
base_url='api.vk.com/method'
method='users.get'
params = {
'user_id' : 591408, # идентификатор пользователя
'access_token': vkkey, # секрет
'v': 5.52 # версия API
}
parstr = urllib.parse.urlencode(params)
url = f'{protocol}{base_url}/{method}?{parstr}'
with urllib.request.urlopen(url) as f:
if f.getcode() == 200: # статус OK
data = json.loads(f.read())['response'][0]
else:
data = {}
for key, val in data.items():
print(f'{key:>10}: {val}')
# id: 591408
# first_name: Vitaly
# last_name: Vorobyev
API-сервис Вконтакте работает через протокол HTTPS, api.vk.com/method
— это адрес API-сервиса. Мы использовали API-метод users.get
с тремя параметрами: идентификатор пользователя (user_id
), ключ идентификации (access_token
) и версия API (v
). Параметры для вставки в URL склеили с помощью функции urllib.parse.urlencode
. Ключ идентификации является секретом и не должен оказываться в открытом доступе. Создать ключ для своего аккаунта Вконтакте можно с помощью простой процедуры.
Функция urllib.request.urlopen
выполнила метод GET протокола HTTP для указанного URL и вернула объект типа http.client.HTTPResponse
. Статус-код 200
означает, что запрос был успешно обработан. Метод HTTPResponse.read
позволяет прочитать полученные в ответ на запрос данные. В нашем случае данные получены в формате json, который мы десериализовали с помощью функции json.reads
.
Метод friends.get
API-сервиса Вконтакте позволяет получить список друзей пользователя:
method='friends.get'
# ...
with urllib.request.urlopen(url) as f:
if f.getcode() == 200:
data = json.loads(f.read())['response']
print(f'count: {data["count"]}')
# count: 279
print(f'items: {data["items"][:5]}')
# items: [4834, 12521, 13121, 16351, 20537]
Вы можете сами поэкспериментировать API-сервисом Вконтакте, изучив документацию.
Работа с API через URL-запросы, в которых задан метод с параметрами, является распространенным стандартом. Подобным образом можно работать с базой данных научных публикаций INSPIREhep. Найдем в ней десять публикаций Ричарда Фейнмана:
addr='https://inspirehep.net/api'
params = {
'q' : 'find a r feynman',
'size': 10,
'page': 1
}
parstr = urllib.parse.urlencode(params)
url = f'{addr}/literature?{parstr}'
with urllib.request.urlopen(url) as f:
data = json.loads(f.read())
for item in data['hits']['hits']:
mdata = item['metadata']
first_auth = mdata['authors'][0]['full_name']
title = mdata['titles'][0]['title']
date = mdata['preprint_date']
doc_type = mdata['document_type'][0]
iid = item['id']
print(f'{iid:->8}: {date:<11} {first_auth:>13} {doc_type}\n "{title}"')
Мы снова получили данные формате json. Результаты нашего запроса:
--894667: 1939 Feynman, R.P. thesis
"Forces and Stresses in Molecules"
-1115227: 1976-11 Field, R.D. article
"Quark Elastic Scattering as a Source of High Transverse Momentum Mesons"
-1115223: 1952 Brown, L.M. article
"Radiative corrections to Compton scattering"
---61328: 1970 Feynman, R.P. article
"Some comments on baryonic states"
---69949: 1971 Feynman, R.P. conference paper
"The quark model at low energies"
---46626: 1949-05-15 Feynman, R.P. article
"Equations of State of Elements Based on the Generalized Fermi-Thomas Theory"
---47515: 1953-09-15 Feynman, R.P. article
"Atomic Theory of Liquid Helium Near Absolute Zero"
---42560: 1948 Feynman, R.P. article
"Relativistic cutoff for quantum electrodynamics"
--942923: 1955-02-01 Feynman, R.P. article
"Slow Electrons in a Polar Crystal"
--427379: 1996 Feynman, R.P. book
"Feynman lectures on gravitation"
Первая запись имеет тип thesis. Мы обнаружили диссертацию Фейнмана. Давайте скачаем ее текст средствами urllib.requests
:
itype='literature'
iid=894667
url = f'https://inspirehep.net/api/{itype}/{iid}'
with urllib.request.urlopen(url) as f:
data = json.loads(f.read())
fulltexturl = None
for doc in data['metadata']['documents']:
if doc['fulltext']:
fulltexturl = doc['url']
print('Full text found!')
break
if fulltexturl:
with urllib.request.urlopen(fulltexturl) as f:
with open('feynman_thesis.pdf', 'wb') as of:
of.write(f.read())
У нас получилось:
import os
fname='feynman_thesis.pdf'
if os.path.isfile(fname):
print(os.path.getsize(fname))
# 2336577
Если Вас заинтересовали рассмотренные примеры, обратите внимание на более удобную для работы с HTTP запросами библиотеку requests
, которая не входит в набор модулей стандартной библиотеки.
Резюме
В этом разделе были рассмотрены некоторые модули стандартной библиотеки python, полезные для
- сериализации объектов
- сжатия данных
- тестирования кода
- веб-программирования
Были рассмотрены примеры работы с новыми инструментами, которые позволяют продолжить их самостоятельное использование и дальнейшее изучение.