Motor 0.5 (асинхронный драйвер для MongoDB) доехал до релиза.
Новая версия работает с asyncio.
Всем рекомендую
Выпустил новую версию aiopg 0.2 -- библиотеки для работы с PostgreSQL из asyncio.
aiopg использует асинхронные вызовы и в этом похож на txpostgres и momoko -- библиотеки для работы с PostgreSQL под twisted и tornado соответственно.
В новой версии aiopg появилась опциональная поддержка SQLAlchemy Core Expressions.
Проще один раз показать.
Создаем описание структуры базы данных:
import sqlalchemy as sa
metadata = sa.MetaData()
users = sa.Table('users', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String(255)),
sa.Column('birthday', sa.DateTime))
emails = sa.Table('emails', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', None, sa.ForeignKey('users.id')),
sa.Column('email', sa.String(255), nullable=False),
sa.Column('private', sa.Boolean, nullable=False))
Как видите -- две таблицы, связанные отношением один-ко-многим. Для тех, кто не знаком -- алхимия позволяет описать любую модель данных, которая только может прийти в голову. Индексы, constraints, пользовательские типы данных такие как array и hstore -- тоже.
Теперь нужно сделать engine:
from aiopg.sa import create_engine
engine = yield from create_engine(user='aiopg',
database='aiopg',
host='127.0.0.1',
password='passwd')
engine содержит внутри connection pool.
Для работы с БД нужно получить connection и что-нибудь выполнить:
with (yield from engine) as conn:
uid = yield from conn.scalar(
users.insert().values(name='Andrew', birthday=datetime(1978, 12, 9)))
Обратите внимание: диалект знает о INSERT ... RETURNING и позвращает
primary key для вставляемой записи.
Работа с транзакциями:
with (yield from engine) as conn:
tr = yield from conn.begin()
# Do something
yield from tr.commit()
Получение данных:
with (yield from engine) as conn:
res = yield from conn.execute(users.select())
for row in res:
print(res)
Сложный запрос:
with (yield from engine) as conn:
join = sa.join(emails, users, users.c.id == emails.c.user_id)
query = (sa.select([users.c.name])
.select_from(join)
.where(emails.c.private == 0)
.group_by(users.c.name)
.having(sa.func.count(emails.c.private) > 0))
print("Users with public emails:")
ret = yield from conn.execute(query)
for row in ret:
print(row.name)
Вызов SQL функций:
with (yield from engine) as conn:
query = (sa.select([sa.func.avg(sa.func.age(users.c.birthday))])
.select_from(users))
ave = (yield from conn.scalar(query))
print("Average age of population is", ave,
"or ~", int(ave.days / 365), "years")
sa.func.avg и sa.func.age выполняются на стороне SQL сервера.
asyncio не умеет работать с HTTP.
Так и было задумано.
asyncio никогда не станет веб-сервером. Он делался как именно event loop для tcp, ssl, unix sockets, pipes и subprocesses. Плюс streaming API.
Веб был сознательно выпилен и теперь то что было лежит в aiohttp. Эта часть просто не дозрела до включения в стандартную библиотеку.
Идея такая:
Что касается меня то я пытаюсь понять какой именно должен быть API для HTTP server, что там должно быть обязательно и что нужно сознательно исключить.
Сейчас делаем это aiorest
Когда поймём, что получилось хорошо в aiorest -- займемся перенесением удачных решений в aiohttp. Там HTTP server слишком уж неудобный. А нужно что-то типа tornado.web, но более симпатичное и приятное.
Наверное, уже все слышали про asyncio -- новую стандартную библиотеку для асинхронного сетевого программирования.
Естественно, asyncio не умеет работать с ZeroMQ сокетами и никогда не будет уметь.
На днях я выпустил первую версию библиотеки aiozmq, которая устраняет проблему.
aiozmq предоставляет 0MQ event loop совместимый с asyncio и высокоуровневые средства для организации вызовов удалённых процедур aka RPC.
Если интересны подробности -- читайте документацию, она довольно большая и подробная (постарался).
Поговорим об исключениях.
Всё нижеизложенное относится к 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.
Изложение получилось несколько длинным, но сократить текст без потери смысла у меня не вышло.
Последняя проблема о которой хотелось бы рассказать — это типы исключений порожденные вызовами операционной системы.
До 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, если можете. Он хороший и облегчает жизнь.
Новые плюшки в вопросе, касающемся исключений, я показал.
Если использовать новый питон не позволяют обстоятельства — пишите на чём есть, но помните как правильно это делать.
Что приходит в голову при словах «числа в Питоне?»
int и float. Если речь идет о Python 2 — еще и упразднённый long.
Наверное, вспомнится очень мало где используемый complex.
Я же хочу рассказать о Decimal и Fraction.
Просто незаменим, если нужно считать деньги. Представим, что нам нужно работать с гривной (это такая украинская валюта, с точки зрения рассматриваемого вопроса ничем не отличающаяся от рубля, евро или доллара). Сотая часть гривны называется копейкой. Естественно думать, что гривны будут представлены целой частью числа, а копейки — дробной.
Что произойдёт, если для денег мы станем использовать 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
Предназначен для работы с обыкновенными дробями
В школе все учили дроби: «одна треть плюс одна треть равно две трети».
Не все обыкновенные дроби имеют точное конечное представление,
укладывающееся в границы 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, использовал их на полную катушку — честь и хвала, я ничего нового не сказал.
Если же, столкнувшись с областью применения этих числовых типов, читатель вспомнит мои рецепты правильного их приготовления — статья свою работу выполнила.
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 будет выпущен в конце августа. Это будет по настоящему выдающаяся версия!
Третий питон с рождения замечательно поддерживает юникод. Собственно говоря, это одна из самых заметных его особенностей.
Чуть меньше бросается в глаза тот факт, что идентификаторы тоже стали юникодными. Уважаемые читатели, если вы используете третий питон и недостаточно хорошо владеете английским - пишите по русски. Это выглядит гораздо лучше, чем убогое средство под названием "транслитерация". Оцените сами:
def функция(агрумент):
коэффициент = 5
return агрумент * коэффициент
Это на самом деле здорово!
Еще один не вполне очевидный момент: имена модулей тоже могут быть в юникоде:
from . import вспомогательный_модуль
Тоже выглядит неплохо, верно? Есть только одна небольшая проблема: это не всегда работает. Вернее, на Windows возможны неприятности. И не нужно заявлять, что вопросы, касающиеся самой популярной на сегодняшний день операционной системы - никого не волнуют. Подавляющее большинство разработчиков самого Питона Windows не используют - и тем не менее Питон обязан на ней работать, и работать хорошо.
Чтобы рассказать в чем вышла загвоздка - я должен немного погрузиться в детали.
В Python 2 немалая часть Python C API принимала char * там, где
требовалась строка. Поскольку str и был последовательностью байт -
сложностей не возникало.
При переносе кода на Python 3 нужно было с этим что-то делать: str
стал юникодным типом, последовательностью символов.
Но в С нет удобного типа для unicode! Вернее, существует стандартный
тип
wchar_t,
который обременен множеством проблем. Главные из них: в разных
реализациях этот тип имеет различный размер: 16 бит для
UCS-2 и 32 бита для
UCS-4. К тому же Windows (о,
снова она) не поддерживает UCS-2 в полной мере (UCS-4 не поддерживает
совсем).
Хуже всего то, что на некоторых платформах этот wchar_t попросту не
определен.
Таким образом, использовать wchar_t в Python C API нельзя.
Сам Питон вводит тип Py_UNICODE для этих целей. Но и тут не все
гладко. Этот тип не входит в Limited API (PEP
384).
Кроме того, разработчики не хотели радикально заменить все char * на
что-то другое.
Есть еще и вопрос практического удобства: ведь очень здорово писать
ret = PyObject_GetAttrString(obj, "attribute");
Для wchar_t все гораздо сложнее, далеко не все компиляторы
поддерживают строковые юникодные константы.
В свете вышеописанных причин Python C API продолжает использовать
char *, считая, что эти строки имеют кодировку
UTF-8 если явно не указано иное.
Т.е. прототипы функций C API выглядят как:
PyObject *
PyImport_ImportModuleLevel(char *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);
Это - импорт модуля с именем name, которое передается как UTF-8
строка, аналог питоновской функции __import__.
И эта функция - лишь верхушка используемого механизма. В процессе
импорта вызываются довольно много внутренних закрытых функций - и
везде используются переменные вроде char *name в качестве имен
модулей. В кодировке UTF-8, еще раз напомню.
А ведь имя модуля транслируется в путь к файлу! А кодировака файловой
системы может отличаться от UTF-8. Счастливые пользователи Linux
давно об этом забыли - в подавляющем большинстве систем по умолчанию
как кодировка пользователя (переменная окружения LANG) так и
файловой системы установлены в UTF-8 и проблем нет совсем. Но в
общем случае это не всегда так.
Чуть-чуть о кодировках. Для определения используемых по умолчанию
кодировок в питоне существуют три функции: sys.getdefaultencoding,
sys.getfilesystemencoding и locale.getpreferredencoding.
sys.getdefaultencoding() - кодировка по умолчанию, используемая в
питоновских исходниках. Для третьего питона всегда равна
UTF-8. Это - та самая кодировка, которую можно перекрыть написав
в начале файла
# -*- encoding: utf-8 -*-
sys.getfilesystemencoding() - кодировка файловой системы. Например, для
f = open('path/to/file', 'r')
значение 'path/to/file' имеет тип str (юникод). Лежащая в
основе функция из clib имеет прототип
int open(const char *pathname, int flags, mode_t mode);
Значит, 'path/to/file' должен быть преобразован в char *
используя кодировку sys.getfilesystemencoding(). Конечно, в
Python C API есть специальные функции для этого.
locale.getpreferredencoding() - предпочтительная для пользователя
кодировка. Она устанавливается в региональных настройках и к
файловой системе прямого отношения не имеет.
Теперь снова вспомним нашу горячо любимую Windows.
locale.getpreferredencoding() возвращает 'cp1251' - Windows
настроена на русский язык. Кодировка для консоли
(sys.stdout.encoding) другая, это 'cp866' - что добавляет сумбура
в и без того запутанную проблему. Ну да ладно, не будем отвлекаться.
sys.getfilesystemencoding() возвращает 'mbcs'. И вот здесь
начинаются основные чудеса. Обратите внимание, mbcs - это не
cp1251. Равно как и не cp1252 или какая другая кодировка. mbcs -
это нечто совершенно особенное!
При преобразовании mbcs -> unicode используется кодировка из
locale.getpreferredencoding(), преобразование однозначное и проблем
не вызывает.
Для обратного преобразования unicode -> mbcs тоже используется
locale.getpreferredencoding() (cp1251 в нашем случае). Но
cp1251 не может описать любой юникодный символ. А mbcs - хитрый и
коварный. Если для символа не существует точного преобразования -
используется ближайший похожий по начертанию.
Это непросто понять без примера. Давайте возьмем французское слово comédie и попробуем преобразовать его в mbcs, имея руский язык cp1251 в настройках по умолчанию.
Возьмем Python 3.1:
>>> b = b'com\xc3\xa9die'
>>> s = b.decode('utf8')
>>> s.encode('mbcs')
b'comedie'
Посмотрите, какая прелесть! Для символа é в русской раскладке cp1251 нет подходящего аналога. Но ведь английская буква e так похожа: нужно лишь убрать умляут (англ. umlaut, французы зовут этот знак accent aigu). Так и получили преобразование comédie -> comedie без единой ошибки.
А теперь представьте, что это - имя файла. Результат будет следующим: файл на диске есть, и так как в Windows файловая система юникодная, имя файла будет записано правильно, по французски. Но преобразование unicode -> mbcs даст несколько другое имя, которого на диске нет.
В результате получается изумительная по своей красоте ситуация:
f = open('comédie', 'r')
будет говорить, что файла нет - а на самом деле вот же он, красавец!
Справедливости ради нужно упомянуть, что в Python 3.2 поведение mbcs
немного поменялось, и 'comédie'.encode('mbcs') вызовет
UnicodeEncodeError. Дело в том, что mbcs стал использовать режим
strict по умолчанию. Чтобы повторить функциональность 3.1 следует
указывать режим replace: 'comédie'.encode('mbcs', 'replace')
С mbcs мы разобрались и выяснили, что для работы с файловой системой эта кодировка в общем случае непригодна. Т.е. если я буду использовать русские имена файлов на русской Windows - всё будет хорошо. Но открыть этот файл у американца или голландца не выйдет. Что же делать?
В Windows помимо open есть еще и функция
FILE *_wfopen(const wchar_t *filename, const wchar_t *mode);
которая принимает wchar_t * и позволяет использовать оригинальное
юникодное имя файла без всяких преобразований. Существует целый набор,
начинающийся с _w - на все случаи жизни.
Значит, нужно делать следующее: для Windows использовать юникодные
версии функций работы с файлами, а для всех остальных операционных
систем применять .encode(sys.getfilesystemencoding()).
Реализация модуля io
начиная с версии 3.1 так и поступает.
Всё отлично за одним маленьким исключением - механизм импорта не
использует io! Исторически сложилось так, что имя импортируемого
модуля довольно быстро преобразовывается в
sys.getfilesystemencoding() (с возможными ошибками и потерями, о
которых я писал выше) и в таком виде пронизывает весь очень непростой
и громоздкий код, чтобы попасть в функции стандартной библиотеки C.
Добавьте к этому довольно большой объем платформозависимого кода (на Маке все работает совсем не так, как на Linux) и проблему обратной совместимости (даже после объявления части API устаревшей она должна поддерживаться как минимум в двух следующих выпусках) - и вы сможете представить сложность и объемность задачи.
Так вот, после трехлетнего труда (с небольшими перерывами, естественно - это же добровольный некоммерческий Open Source) Victor Stinner завершил требуемое нелегкое преобразование. Довольно незаметный, но очень важный шаг!
Файловые пути стали храниться в PyObject* (на самом деле это,
конечно, str - PyUnicodeObject), работающая с ними часть C API
имеет суффикс Object. Например:
PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);
Сравните с PyImport_ImportModuleLevel. Все функции из старого API
стали тонкими обертками над новыми вариантами. Так,
PyImport_ImportModuleLevel создает PyObject из name и вызывает
PyImport_ImportModuleLevelObject.
Эти старые функции оставлены для сохранения обратной совместимости, сам Питон их уже не использует.
Если быть честным, именно Windows поддержка чуть-чуть не готова - но до выхода Python 3.3 еще очень много времени. Достаточно, чтобы закончить работу и навести полный порядок.
Я написал этот довольно длинный текст преследуя несколько целей:
Пожалуй, главная из них - показать, насколько порой незначительные внешне изменения способны перевернуть внутреннюю реализацию, и как нелегко их проделать не сломав того, что уже отлично работает пятнадцать лет.
Вторая - продемонстрировать, как работают кодировки применительно к файловой системе.
Третья - напомнить, что можно использовать русские буквы в идентификаторах. Комментарии излишни.
И, наконец, очень хотелось отметить завершение отлично выполненной работы, которая делает Питон немного лучше.