понедельник, 24 декабря 2012 г.

Отчет по спринту

Всё получилось!

Собралось нас семеро: Вова Гоцик, Лена Кривонос, Витя Ершов, Олег Чубин, Антон Касьянов, Паша Коломиец и я.

Починили несколько багов, работа над другими только начата и будет закончена немного позже.

Главное, все приобщились к делу и поняли как идет процесс работы над кодом.

Съели пиццу и разошлись довольные друг другом, оставив меня разгребать результаты.

Пара фоток с мероприятия:



Спасибо всем кто пришёл!

воскресенье, 23 декабря 2012 г.

Ищем докладчиков на Kyiv.py #9

Всем привет.

Как-то за конференциями, спринтами и подготовкой к будущим событиям мы с Вовой Гоциком забыли про

Kyiv.py #9

Который состоится в 11.00 19 января в офисе Циклума, что на Амосова 12.

Есть всё кроме докладчиков. 
Вернее, один доклад уже есть — это Олег Чубин с интересным рассказом об итераторах-генераторах.
Он подавал эту тему как заявку на UA PyCon 2012, но к сожалению к тому моменту у нас закончились свободные слоты в расписании.

Если у вас есть интересная тема — пишите мне на andrew.svetlov@gmail.com
Очень надеюсь что наберем достаточно желающих — иначе мне придется закрывать дыру своей тушкой :)

четверг, 20 декабря 2012 г.

понедельник, 17 декабря 2012 г.

Исключения в Питоне

Поговорим об исключениях.

Всё нижеизложенное относится к Python 3.3, хотя отчасти справедливо и для более ранних версий.

Для чего они применяются, наверное, все и так прекрасно знают: для передачи сообщений об ошибках внутри программы.

Рассмотрим простейший пример: открытие файла. Если всё нормально — open(filename, 'r') возвращает объект этого самого файла, с которым можно делать всякие полезные вещи: читать из него данные и т.д.

Если файл не может быть открыт — выбрасывается исключение:

try:
    f = open(filename, 'r')
    try:
        print(f.read())
    finally:
        f.close()
except OSError as ex:
    print("Cannot process file", filename, ": Error is", ex)

Открываем файл и печатаем его содержимое.

Обратите внимание: файл нужно не только открыть но и закрыть после использования. Исключение может выбросить open (например, если файла нет на диске или нет прав на его чтение).

Если файл открыт — читаем его через f.read(). Этот вызов тоже может выбросить исключение, но файл закрывать всё равно нужно. Поэтому необходим блок finally: f.close() должен быть вызван даже если f.read() сломался. В этом месте удобней было бы воспользоваться конструкцией with но мы же сейчас говорим об исключениях а не о контекстных менеджерах, верно?

Исключения из обоих мест попадут в except OSError, где можно будет что-то сделать с ошибкой.

Питон делает явный выбор в пользу исключений перед возвратом кода ошибки в своём ядре и стандартной библиотеке. Прикладному программисту стоит придерживаться этих же правил.

Введение закончено. Теперь сконцентрируемся на том что происходит в except.

Типы исключений

Все исключения составляют иерархию с простым наследованием. Вот простой небольшой кусочек от довольно обширного набора исключений ядра Питона:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- AssertionError
      +-- AttributeError
      +-- BufferError

Самый базовый класс — BaseException. Он и его простые потомки (SystemExit, KeyboardInterrupt, GeneratorExit) не предназначены для перехвата обыкновенным программистом — только Питон и редкие библиотеки должны работать с этими типами. Нарушение правила ведет, например, к тому что программу невозможно корректно завершить — что совсем не хорошо.

Также не нужно перехватывать все исключения:

try:
    ...
except:
    ...

работает как

try:
    ...
except BaseException:
    ...

Всё, что может быть нужно программисту — это Exception и унаследованные от него классы.

Вообще-то лучше ловить как можно более конкретные классы исключений. Например, в случае с файлом это OSError или даже может быть FileNotFoundError. Таким образом мы не перехватим AttributeError или ValueError, которые в этом примере означали бы ошибку или опечатку программиста.

Кстати, обратите внимание: StopIteration порожден от Exception а GeneratorExit от BaseException. Подробности, почему сделано именно так, можно найти в PEP 342.

Цепочки исключений

Прочитав предыдущую главку все прониклись необходимостью указывать правильный класс исключений и пообещали никогда не использовать BaseException.

Идем дальше. Следующий пример:

try:
    user = get_user_from_db(login)
except DBError as ex:
    raise UserNotFoundError(login) from ex

Получаем пользователя из базы данных чтобы что-то потом с ним сделать. get_user_from_db может выбросить ошибку базы данных. Для нас это скорее всего означает что такого пользователя нет. Но для логики приложения полезней наш собственный тип UserNotFoundError с указанием логина проблемного пользователя, а не обезличенная ошибка БД — что мы и выбрасываем в обработчике исключения.

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

Для таких целей служит конструкция raise ... from ....

По PEP 3134 у объекта исключения имеется несколько обязательных атрибутов.

В первую очередь это __traceback__, содержащий кусочек стека от места возникновения исключения до места его обработки.

Затем — __context__. Если исключение было создано в ходе обработки другого исключения (выброшено из except блока) — __context__ будет содержать то самое породившее исключение. Которое, в свою очередь тоже может иметь установленный __context__. Этот атрибут равен None если наше исключение — самое первое и не имеет предшественников.

__context__ устанавливается автоматически.

В отличие от контекста __cause__ устанавливается только если исключение было выброшено конструкцией raise ... from ... и равно значению from.

Если исключение выбрасывалось простым raise ... то __cause__ будет равно None в то время как __context__ всегда будет содержать породившее исключение если оно существует.

Для вывода исключения со всей информацией служит набор функций из модуля traceback, например traceback.print_exc().

И тут тоже есть проблема: печатается либо явная цепочка если есть установленный __cause__ или неявная, тогда используется __context__.

Иногда программисту может быть нужно отбросить породившие исключения как не имеющие смысла при выводе traceback. Для этого появилась форма записи

raise exc from None

PEP 409 и PEP 415 рассказывают как это работает:

У исключения всегда есть атрибут __supress_context__. По умолчанию он равен False.

Конструкция raise ... from ... записывает from в __cause__ и устанавливает __supress_context__ в True.

Тогда семейство функций traceback.print_exc() печатают цепочку если явно указан (не равен None) __cause__ или есть __context__ и при этом __supress_context__ равен False.

Изложение получилось несколько длинным, но сократить текст без потери смысла у меня не вышло.

Семейство OSError

Последняя проблема о которой хотелось бы рассказать — это типы исключений порожденные вызовами операционной системы.

До Python 3.3 существовало много разных типов таких исключений: os.error, socket.error, IOError, WindowsError, select.error и т.д.

Это приводило к тому, что приходилось указывать несколько типов обрабатываемых исключений одновременно:

try:
    do_something()
except (os.error, IOError) as ex:
    pass

Ситуация на самом деле была еще хуже: очень легко забыть указать еще одно нужное исключение, которое может внезапно прилететь. Дело в том что исключения операционной системы часто никак не проявляют себя при разработке. На машине программиста всё работает отлично и он не подозревает о возможных проблемах. Как только программа выходит в production пользователь вдруг ловит что-то неожиданное и программа аварийно завершается. Все опечалены.

Проблема решена в PEP 3151: весь этот зоопарк теперь является псевдонимами для OSError. Т.е. пишите OSError и не ошибетесь (прочие имена оставлены для обратной совместимости и облегчения портирования кода на новую версию).

Давайте рассмотрим ещё один аспект исключений, порожденных операционной системой.

У OSError есть атрибут errno, который содержит код ошибки (список всех возможных символьных констант для ошибок можно посмотреть в модуле errno).

Открываем файл, получаем OSError в ответ. Раньше мы должны были анализировать ex.errno чтобы понять, отчего произошла ошибка: может файла нет на диске, а может нет прав на запись — это разные коды ошибок (ENOENT если файла нет и EACCES или EPERM если нет прав).

Приходилось строить конструкцию вроде следующей:

try:
    f = open(filename)
except OSError as ex:
    if ex.errno == errno.ENOENT:
       handle_file_not_found(filename)
    elif ex.errno in (errno.EACCES, errno.EPERM):
       handle_no_perm(filename)
    else:
       raise  # обязательно выбрасывать не обработанные коды ошибки

Теперь иерархия расширилась. Привожу полный список наследников OSError:

OSError
 +-- BlockingIOError
 +-- ChildProcessError
 +-- ConnectionError
 |    +-- BrokenPipeError
 |    +-- ConnectionAbortedError
 |    +-- ConnectionRefusedError
 |    +-- ConnectionResetError
 +-- FileExistsError
 +-- FileNotFoundError
 +-- InterruptedError
 +-- IsADirectoryError
 +-- NotADirectoryError
 +-- PermissionError
 +-- ProcessLookupError
 +-- TimeoutError

Наш пример можем переписать как:

try:
    f = open(filename)
except FileNotFound as ex:
    handle_file_not_found(filename)
except PermissionError as ex:
    handle_no_perm(filename)

Гораздо проще и понятней, правда? И меньше мест, где программист может ошибиться.

Заключение

Переходите на Python 3.3, если можете. Он хороший и облегчает жизнь.

Новые плюшки в вопросе, касающемся исключений, я показал.

Если использовать новый питон не позволяют обстоятельства — пишите на чём есть, но помните как правильно это делать.

пятница, 7 декабря 2012 г.

Python Sprint again

Месяц назад уже был спринт по Питону. Как говориться: всё прошло хорошо, но мало.
Поэтому мы проведем следующие спринт 23 декабря, в воскресенье. Начало в 11.00, место проведения — компания Инсолло (Куреневский пер. 12А, офис 701).

Регистрация
Приглашаются все желающие, но — с регистрацией.
Если вы еще не зарегистрировались и хотите принять участие — заполните гуглоформу.

Инструкция
Чтобы спринт был удачным, к нему нужно подготовиться.
Приходим со своими ноутбуками (думаю, это не проблема).

На ноуте может быть любая система: Linux, Windows, Mac OS X.

Нужно установить компилятор С
 - gcc на Линуксе идет в комплекте
 - на Маке не знаю (там вроде бы можно еще и clang использовать, Питон должен его понимать)
 - для Windows нужно установить Visual Studio, Visual Studio Express доступна для бесплатного скачивания и использования. Для работы нужны обе версии 2008 и 2010.

Также требуется Mercurial HG: http://mercurial.selenic.com/

Работа ведется над CPython repo, установленный из коробки Питон не подходит.

Подробные инструкции на английском языке здесь: http://docs.python.org/devguide/

Делаем:

$ hg clone http://hg.python.org/cpython
$ cd cpython
$ ./configure --with-pydebug
$ make -j4
make install делать не нужно

Далее. Чтобы не тратить время зря советую зарегистрироваться на bugs.python.org и просмотреть заранее список issues. 
Выбирайте помеченные как easy — они как правило действительно простые.

Если у вас уже есть темы, над которыми хотите поработать — добро пожаловать.
Если не сможете определиться — тоже не беда, я подскажу.

Работа выглядит так: 
 - создается issue или берется готовая
 - готовятся исправления
 - делается патч: $ hg diff > issueXXX.diff 
 - он заливается через форму на bugs.python.org Там же добавляете меня (asvetlov) в nosy list
 - я закачиваю патч и применяю его: $ curl http://path-to-patch|patch -p1
 - если всё нормально — делаю push в repo

У Питона сейчас открыты для патчей четыре ветки: 2.7, 3.2, 3.3, default (3.4). 
Новые фичи принимаются в default, баги и правки по документации могут попасть и в остальные три.
Если кто не знает как работать с ветками — я покажу на месте.

Юниттесты запускаются так:
$ make test
или 
$ ./python Lib/test/regrtest.py

Отдельный тест можно запустить
$ ./python Lib/test/test_xxx.py

Перед отсылкой патча настоятельно рекомендую запускать чекер
$ make patchcheck
Эта процедура как минимум убирает лишние пробелы — постоянная головная боль при приеме патчей.

Заключение.
Я буду на месте всё время, помогу вам советом, правкой кода и т.д. Все затруднения быстро порешаем.
Уверен, закроем достаточно ожидающих своего дыр.
Если что непонятно — спрашивайте.

пятница, 23 ноября 2012 г.

среда, 31 октября 2012 г.

Python Committer

В марте на US PyCon 2012 sprints я стал Python Core Developer.

Или, на наши деньги, был включен в команду разработчиков CPython.

С тех пор прошло почти восемь месяцев.

И сегодня я сделал свой двухсотый commit в http://hg.python.org/cpython

Не так уж и мало!

С другой стороны, можно и лучше, да и аппетит приходит именно во время еды. Чем больше вкладываю своих сил в развитие любимого языка программирования, тем больше этот процесс нравится.

Отличная команда, интересный проект, благожелательная атмосфера.
Плюс, работая над развитием языка и стандартной библиотеки, лучше узнаешь как оно устроено, куда развивается и почему именно в этом направлении, заметно повышаешь свой профессиональный уровень.

В общем, мне это занятие очень нравится, буду стараться работать дальше не снижая темпа.

Python Sprints

У Организаторов UA PyCon есть идея провести как-нибудь спринты по Питону.

Что это такое?

Добровольцы собираются вместе и работают над тем, чтобы Питон стал лучше: правят-дописывают документацию, добавляют тесты, чинят баги.

Я могу быть Sprint Lead, показать процесс работы, помочь на месте в случае затруднений, провести review кода и сделать commit.

Детали пока еще не определены. Сначала нужно понять, насколько это вообще интересно и на какое количество людей нужно ориентироваться.

Поэтому, если есть желание принять участие в спринте, убедительно прошу заполнить опрос на гуглоформах.

Если есть вопросы — задавайте.

понедельник, 29 октября 2012 г.

Learning Python

Ребята и девчата.

Закончился UA PyCon 2012. Надеюсь, вам понравилось.

Игорь Давыденко на lighting talks сделал презентацию своей идеи по организации курсов изучения Питона.

Я согласился вести advanced курс в рамках этой программы.

Игорь будет вести курс по веб-программированию с использованием популярных инструментов Django и Flask.

Я же хочу дать углубленные знания по тому, как работает веб на примере создания собственного простого сервера, показать приемы замера производительности и оптимизации кода, интеграции питона с С кодом и т.д.
Аудитория — люди, уже имеющие опыт разработки на Питоне и желающие понять что происходит «под капотом».

Курсы состоят из 12 занятий по два часа в неделю каждое.
Час на подачу теоретического материала и час на разбор домашних заданий. Выполнение домашних заданий обязательное.
Стоимость определится после набора группы, ориентировочно 150 грн за занятие.

Вопрос первый: это вообще интересно?
И второй вопрос: о чем бы вам хотелось узнать?

Регистрируйтесь на http://learnpython.in.ua. После набора 10-15 человек начнем обучение.

четверг, 25 октября 2012 г.

Питон: еще раз форматирование

Хочу напомнить об одной занятной штуке, которую сам периодически забываю.

Как переводят дату-время в строку? Обычно так:

>>> from datetime import datetime
>>> d = datetime.now()
>>> d.strftime("%d.%m.%Y")
'25.10.2012'

А на самом деле можно и так:

>>> "{:%d.%m.%Y}".format(d)
'25.10.2012'

Как это работает?

.format разбирает строку формата и определяет параметры форматирования для аргументов. Для первого аргумента, очевидно, это %d.%m.%Y.

Это значение попадает в метод .__format__ в качестве аргумента, а результат вызова подставляется в шаблон:

>>> d.__format__("%d.%m.%Y")
'25.10.2012'

Что это дает? Можно коротко и элегантно записывать сложные шаблоны:

>>> "{} — {:%d.%m.%Y}".format("Today", d)
'Today — 25.10.2012'

То же самое работает и для других классов в datetime и т.д.:

>>> from datetime import date
>>> "{:%A}".format(date.today())
'Thursday'

Более того, никто не мешает определить .__format__ для своего класса, возвращающий строку построенную по заданному шаблону.

Только ради этого стоит переключиться со старого стиля "%s" % "abc" на новый, симпатичный и удобный.

суббота, 6 октября 2012 г.

четверг, 4 октября 2012 г.

Билеты на UA PyCon 2012


Понемногу заканчиваются последние билеты на UA PyCon.
Знаю, что, как и год назад, найдутся особо несознательные граждане, которые будут просить билетик за день до события, соглашаясь на тройную цену стоя и без обеда.
Тем не менее настоятельно рекомендую тем, кто имеет желание посетить конференцию и все еще не приобрел билет — сделать это поскорее.

вторник, 7 августа 2012 г.

Числа в Python 3

Что приходит в голову при словах «числа в Питоне?»

int и float. Если речь идет о Python 2 — еще и упразднённый long. Наверное, вспомнится очень мало где используемый complex.

Я же хочу рассказать о Decimal и Fraction.

Decimal

Просто незаменим, если нужно считать деньги. Представим, что нам нужно работать с гривной (это такая украинская валюта, с точки зрения рассматриваемого вопроса ничем не отличающаяся от рубля, евро или доллара). Сотая часть гривны называется копейкой. Естественно думать, что гривны будут представлены целой частью числа, а копейки — дробной.

Что произойдёт, если для денег мы станем использовать float?

Как я писал в статье: 4 грн 31 коп будут на самом деле иметь внутреннюю запись 4.3099999999999996. Да, при печати всё показывается нормально если у вас Python 2.7+ — но внутри это всё же чуть-чуть иное число!

И если работать с такими числами (складывать, вычитать, делить и умножать) — ошибка будет нарастать и рано или поздно превысит копейку, а потом счет пойдет и на гривны. Чем больше операций — тем выше ошибка.

В результате дебет перестаёт сходиться с кредитом, бухгалтерия встаёт на уши и разработчик получает большую проблему на свою голову (случай из жизни моего друга).

Чтобы этого избежать, нужно использовать decimal — который никогда ничего не теряет.

Внутри decimal представлен как знак, набор цифр и положение десятичной точки — т.е. нет никакого округления.

Использование очень простое:

>>> from decimal import Decimal
>>> Decimal("4.31")
Decimal('4.31')
>>> Decimal("4.31") + Decimal("1.10")
Decimal('5.41')

Все стандартные операции над decimal работают так же хорошо, как и с просто числами.

К слову, базы данных как правило имеют встроенную поддержку этого типа, а драйвера DBAPI и ORM вроде Django и SQLAlchemy тоже умеют работать с decimal.

К сожалению, этого базиса недостаточно, а документация, исчерпывающе полная, — не содержит простых ясных инструкций для ленивого программиста.

Пример:

>>> Decimal("1.10") / 3
Decimal('0.3666666666666666666666666667')

Ой! Зачем так много цифр, ведь у нас гривна с копейками?!!

Дело в том, что помимо Decimal есть еще и Context. По умолчанию у него точность в 28 чисел в дробной части, что явно многовато для валюты. Настроим на 2 знака:

>>> from decimal import getcontext
>>> getcontext().prec = 2
>>> Decimal('1.10') / 3
Decimal('0.37')

Уже лучше.

Правила округления тоже задаются контекстом. По умолчанию это ROUND_HALF_UP — округлять вверх, если цифра пять и больше. Как в школе учили. Можно настроить и другой способ — читайте документацию. Еще можно указать, чтобы при разных ситуациях (потеря точности или бесконечность в результате, например) генерировалось исключение а не происходило округление. Кому надо — пусть изучает эту самую документацию, ключевое слово trap.

Вернемся к наиболее распространенным задачам.

Что делать, если часть вычислений нужно проводить с точностью «до копеек», а некоторые (например, то же сведение баланса и подсчет налогов) — до сотых долей копеек?

Наиболее практичный способ — создание своего контекста и применение его в with statement:

>>> from decimal import Context, localcontext
>>> with localcontext(Context(4)):
...     print(repr(Decimal("1.10") / 3))
Decimal('0.3667')

Округление:

>>> Decimal('1.12').quantize(Decimal('0.1'))
Decimal('1.1')
>>> Decimal('1.16').quantize(Decimal('0.1'))
Decimal('1.2')

Внимание! Округлять можно только до той максимальной точности, которая позволена текущим контекстом. Сейчас у нас глобальный контекст имеет точность 2.

>>> getcontext().prec = 2
>>> Decimal('1.10').quantize(Decimal('0.000001'))
Traceback (most recent call last):
...
decimal.InvalidOperation: quantize result has too many digits for current context

Вот и всё.

За рамками статьи осталось многое. Моя цель — дать необходимый минимум знаний, требуемый для работы с денежными единицами. Остальное, повторюсь ещё раз, найдете в документации — она большая и исчерпывающая, содержит описание многих дюжин доступных функций.

И, тем не менее, изложенного достаточно для практически любой повседневной работы.

Важное дополнение. Изначально decimal был написан на чистом питоне. Т.е. корректно считал, но делал это довольно медленно. Не настолько плохо, чтобы отказываться от него (тем более что альтернатив нет) — но часто скорость важна и хотелось бы побыстрее.

В Python 3.3 вошёл ускоритель (подключается автоматически). Автор — Stefan Krah. Большое спасибо этому человеку. Благодаря его труду производительность decimal повысилась настолько, что скорость вычислений стала сопоставима с int и float.

Всем читающим — намёк: переходите на Python 3.3

Fraction

Предназначен для работы с обыкновенными дробями

В школе все учили дроби: «одна треть плюс одна треть равно две трети».

Не все обыкновенные дроби имеют точное конечное представление, укладывающееся в границы float.

Пример:

>>> 7/71*71 == 7
False

А теперь с fraction:

>>> from fractions import Fraction
>>> Fraction(7, 71) * 71 == 7
True

Практическое применение:

С конца мая 2011 я работаю в проекте Апокалипсис. Это онлайновая игра с пошаговой боёвкой. Каждый игрок имеет определённое количество очков на ход. Передвижение на одну клетку — одно очко. Выстрел — скажем, три очка. Ходят все одновременно.

У меня 7 очков движения, у противника 8. Значит, я совершаю свои действия со скоростью 1/7, а противник — 1/8.

Я сдвинулся на одну клетку и выстрелил.

И тут очень важно правильно посчитать, когда произошел выстрел: чуть раньше супостата, чуть позже или точно одновременно. Буквально каждое мгновение — вопрос жизни или смерти в игровом бою.

float верить нельзя: если событие должно наступить одновременно — ошибка округления наверняка соврёт в ту или другую сторону. А пользователи игры забросают нас, разработчиков, виртуальными гнилыми помидорами.

Fraction считает честно, что к тому же значительно облегчает игровую математику: 1/3 всегда равна «одной трети».

Заключение

Если читатель знал о существовании Fraction и Decimal, использовал их на полную катушку — честь и хвала, я ничего нового не сказал.

Если же, столкнувшись с областью применения этих числовых типов, читатель вспомнит мои рецепты правильного их приготовления — статья свою работу выполнила.

четверг, 19 июля 2012 г.

Питон: времена, даты и временные зоны

В статье Питон: времена, даты и временные зоны я рассказывал, что такое абсолютное и относительное время в терминах Питона.

И упоминал, что сравнение относительного и абсолютного времени выбросит исключение TypeError. В Python 3.3 ситуация изменилась.

Относительное и абсолютное времена всё ещё нельзя сравнивать на упорядоченность (больше или меньше). Сравнение на эквивалентность никогда не срабатывает, при этом ошибки нет.

Пример:

>>> from datetime import datetime, timezone
>>> naive = datetime.now()
>>> aware = datetime.now(timezone.utc)
>>> naive < aware
Traceback (most recent call last):
  ...
TypeError: can't compare offset-naive and offset-aware datetimes
>>> naive == aware
False

Ещё раз подчеркиваю: относительное время никогда не равно абсолютному вне зависимости от того, на какое время суток они указывают.

Подробности можно прочитать в багтрекере.

четверг, 14 июня 2012 г.

О производительности

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

Тестовые системы ничего не показывали.
Неделю перетряхивали код, нашли и исправили несколько узких мест. Я, например, ускорил математику раз в 6-7.
Накатываем новое обновление — и опять тормоза, буквально никаких улучшений не заметно.

Проблема, как оказалось, была в следующем — обновление по ошибке включало вывод всех логов на консоль. А развертываемая система не перенаправляла stdout в /dev/null — да и не должна. Если всё правильно настроено — никто в консоль ничего не пишет. Логам место в файлах.

Досадный вывод отключили — и всё взлетело со скоростью необычайной. Такие дела...

вторник, 5 июня 2012 г.

DevConf 2012

Буду в субботу на DevConf.

Москвичи, если есть желание — можем в пятницу вечером встретиться поболтать.

воскресенье, 15 апреля 2012 г.

Реализация импорта в Питоне переписана на чистый Питон

Python 3.3 продолжает радовать. Сегодня в него вошли изменения от Bratt Cannon. Теперь import реализован почти весь на питоне, в библиотеке importlib. Она и раньше присутствовала — но теперь __import__ ведёт напрямую в importlib.

Казалось бы, какая разница? На самом деле всё просто:

  • Проще поддерживать и развивать. Существующий код в import.c нелегко читать и ещё сложнее понимать во всех деталях (включая в том числе и кроссплатформенность). Теперь ситуация значительно улучшится.

  • Легче изучать. Повторюсь, понимать реализацию по чтению Python/import.c (а именно там расположен почти весь код, относящийся к импорту модулей и пакетов) — нелегко. Отчасти потому, что написана на C, в то время как большинства питонщиков этот язык, к сожалению, практически не знаком. Главная же причина в том, что этот механизм эволюционировал потихоньку, счастливо избегая больших переделок — но уверенно вбирая в себя сотни мелких и средних улучшений. Проще и понятней от этого не становилось.

  • Унификация. Сложилась на самом деле парадоксальная ситуация: в то время как PEP 302, исчерпывающе описывает то, как должен работать механизм импорта — Питон этим не пользуется. То есть да, PEP 302 полностью реализован и поддерживается — но стандартный импорт из файловой системы идёт в обход этих правил. Не подумайте плохого — ничего не ломается. Но одновременное существование двух дублирующих систем — одной для импорта из файловой системы, а второй для поддержки PEP 302, — уже не очень хорошая вещь.

  • Снова вспоминаем про поддержку, развитие и изучение. Почему, например, по умолчанию sys.meta_path пустой? Отчего в sys.path_hooks есть только zipimporter? Где вообще import hooks для загрузки модулей из файловой системы? Почему Python не прописывает в модулях __loader__, хотя PEP 302 однозначно предписывает это делать?

  • Как, в конце концов, всё работает? Читатель, вы уверены что понимаете механизм во всех деталях?

Документация хороша, но она описывает далеко не все нюансы. Когда я попытался разобраться — мне пришлось потратить немало времени. Потом это вылилось в серию статей, посвящённых импорту: часть 1, часть 2, часть 3, импорт и юникод. На этом терпение закончилось и заключительные статьи серии так и не были опубликованы.

Изучение было крайне увлекательным, но меня не оставляло чувство, что процесс должен быть намного проще, ведь Питон старается избегать чрезмерно усложнённых решений.

Так и произошло. Сейчас импорт уже стал проще и понятней, это просто замечательно. В дальнейших планах — подчистка и сокращение кода на С, сведение его объёма к минимуму.

Немного о скорости. Питоновская реализация примерно на 5% медленней — что никого не беспокоит. Быть может, к версии 3.4 маятник качнётся назад, и наиболее критические части кода будут реализованы на С (как это произошло, например, с модулем io). Сейчас гораздо важнее корректность, — а скорость, и без того вполне приемлемая, вполне может подождать своего часа.

Запасайтесь поп-корном, Python 3.3 будет выпущен в конце августа. Это будет по настоящему выдающаяся версия!

понедельник, 27 февраля 2012 г.

Измерение интервалов времени

Речь пойдет о функциях для измерения интервалов времени, расположенных в модуле time.

time() и clock()

Очень долго их было ровно две: time.time() и time.clock().

Использование предельно простое:

t1 = time.time()
# do tested stuff
print(time.time() - t1)

Получаем время выполнения кода в секундах.

Две функции нужны для того, чтобы усложнить жизнь программиста. Дело в том, что на Windows точнее работает функция time.clock, а на прочих Unix/Linux — time.time.

Дальнейшее касается Linux и (отчасти) других Posix систем. Все нововведения появятся в грядущем Python 3.3

clock_gettime(clk_id)

Вот уже много лет time.time использует системный вызов gettimeofday(2) на тех ОС, где он доступен (Linux, например). gettimeofday(2) позволяет измерять время с точностью до микросекунд в отличие от посекундного time(2).

Прогресс не стоит на месте, и появились функции c наносекундным разрешением: time.clock_gettime и time.clock_getres являются тонкими обёртками над одноимёнными Posix вызовами.

Зачем вообще нужны эти наносекунды и почему не хватает микросекундных интервалов?

Вот вполне жизненный пример: мы используем Redis для кеширования. Он работает на той же машине, где и основное приложение. Используются unix sockets и pipelining. В результате время обработки запросов сопоставимо с 1 мс. Если мы хотим знать, какую часть времени приложение тратит на общение с redis — нужно измерять интервалы с более высокой точностью.

clock_gettime возвращает время, а clock_getres показывает, с какой точностью оперирует clock_gettime.

Не вполне ясно, какой тип таймера выбирать. Вот список таймеров, имеющихся в ядре Linux 3:

  • CLOCK_REALTIME — абсолютное системное время.

  • CLOCK_MONOTONIC — относительное время.

  • CLOCK_MONOTONIC_RAW — относительное время без коррекции NTP.

  • CLOCK_PROCESS_CPUTIME_ID — высокоточный таймер процесса.

  • CLOCK_THREAD_CPUTIME_ID — высокоточный таймер потока.

Во первых, возвращаемое время может быть абсолютным и относительным. Если по абсолютному времени можно узнать «который час», то относительное годится лишь для измерения интервалов. Относительное время отсчитывается от какой-то точки (для простоты пусть это будет момент включения компьютера), абсолютное традиционно начинается 1 января 1970 года (UNIX Epoch).

Поскольку Питон возвращает результат clock_gettime как float, то наносекундная точность возможна лишь в течении 194 дней от UNIX Epoch timestamp, микросекудная точность «заканчивается» за 272 года. 100 наносекндная точность исчерпывается за 17 лет. Вывод простой: для измерения интервалов с точностью выше микросекунды пригодно только относительное время.

Абсолютное время можно менять путём ручной или автоматизированной (службой NTP) подстройки компьютерных часов. Относительное при этом не изменяется.

На относительное время может оказывать влияние автоматическая коррекция времени (NTP), которая за сутки сдвигает системное время на доли секунды относительно аппаратных компьютерных часов. Когда работаем с наносекундными интервалами — это имеет значение. CLOCK_MONOTONIC_RAW лишён такого недостатка.

И, наконец, есть таймеры, берущие значение из счётчиков процессора. Эти счётчики работают с очень высоким разрешением, да вот беда — в симметричных многопроцессорных системах (SMP) счётчики могут быть не связаны друг с другом. Процесс исполняется то на одном процессоре, то на другом, счётчики выдают несвязанные данные по которым нельзя ничего измерить. Широко распространённые однопроцессорные многоядерные системы этим недостатком не страдают. Простой совет: никогда не используете эти таймеры счётчиков процесса, не поняв до конца всех деталей. Для практических целей обычно достаточно менее экзотичных вариантов.

Перечисленные выше типы таймеров не очень полезны «обычному программисту»: совершенно непонятно, какой таймер выбрать прямо здесь и сейчас.

time.monotonic() и time.wallclock()

Обычно желание простое: измерить интервал времени с максимально возможной точностью, причём способ не должен зависеть от установленной операционной системы и используемого «железа».

Вот тут и приходят на помощь функции time.monotonic() и time.wallclock(). Они не требуют параметров, работают везде и используют максимально точный из доступных таймеров. Для Windows это clock(), на Linux последовательно перебираются CLOCK_MONOTONIC_RAW, CLOCK_MONOTONIC и CLOCK_REALTIME. Кто первый отзовётся — тот и лучший.

Разница между функциями незначительная: time.wallclock() попытается перейти к таймеру абсолютного времени если не найдет лучший вариант (и никогда не закончится исключением), time.monotonic() выбросит OSError.

Итог

На самом деле, думаю, ни на одной современной системе до крайности не дойдет и можно никогда не ощутить разницу на себе. И всё же time.wallclock() чуть-чуть больше похожа на то «стандартное поведение», которое требуется от функции для измерения временных интервалов. Если вам нужно оценить время выполнения чего-либо — используйте её.