четверг, 10 июля 2014 г.

Строки символов Unicode в Python 2 и Python 3

Винсент: Знаешь, что самое забавное в Европе?
Джулс: Что?
Винсент: Такие маленькие отличия. Там вроде все то же самое, что и здесь, но чуть-чуть отличается.

"Криминальное чтиво"

Как известно, ключевым отличием Python 3.x от Python 2.x является переориентация языка и стандартных библиотек со строк байтов на строки символов Unicode. Когда я решил повнимательнее посмотреть на это различие, пришлось копнуть вглубь и вширь, а результаты моих раскопок я оформил в статью, которую предлагаю вашему вниманию.

В Python 2В Python 3
строковый литерал получает тип str строковый литерал получает тип str
тип str представляет собой строку байтов тип str представляет собой строку символов Unicode
для представления строки символов Unicode используется тип unicode для представления строки байтов используется тип bytes
для представления строки unicode в нужной кодировке используется метод unicode.encode(кодировка), возвращающий строку байтов str для представления строки str в нужной кодировке используется метод str.encode(кодировка), возвращающий строку байтов bytes
для преобразования строки байтов str в строку unicode используется метод str.decode(кодировка), возвращающий строку unicode для преобразования строки байтов bytes в строку str используется метод bytes.decode(кодировка), возвращающий строку str
системная кодировка по умолчанию, она же кодировка исходных файлов по умолчанию, ASCII системная кодировка по умолчанию, она же кодировка исходных файлов по умолчанию, UTF-8
при записи в файл строки по умолчанию преобразуются в системную кодировку по умолчанию при записи в файл строки по умолчанию преобразуются в кодировку, определяемую пользовательскими настройками
идентификаторы в программе содержат только символы ASCII, не могут использовать никаких других букв, кроме латинских идентификаторы в программе содержат символы Unicode, могут использовать буквы других алфавитов, кроме латинского

Мои эксперименты я ставлю под ОС Windows 7 в стандартной консоли.

Для разминки, несколько манипуляций со строковыми литералами и переменными в Python 2:


>>> s = 'Hello'
>>> s
'Hello'
>>> type(s)
<type 'str'>
>>> u = s.decode('ascii')
>>> u
u'Hello'
>>> type(u)
<type 'unicode'>

И в Python 3:


>>> s = 'Hello'
>>> s
'Hello'
>>> type(s)
<class 'str'>
>>> b = s.encode('ascii')
>>> b
b'Hello'
>>> type(b)
<class 'bytes'>

Теперь посмотрим на кодировки, используемые в Python 2 и Python 3 по умолчанию:

  • Системная кодировка по умолчанию (system default encoding), возвращается функцией sys.getdefaultencoding(). Это кодировка исходных текстов Python по умолчанию, она же используется по умолчанию для кодирования и декодирования строк unicode.
  • Кодировки стандартных потоков ввода, вывода и ошибок - файловых объектов, атрибуты sys.stdin.encoding, sys.stdout.encoding и sys.stderr.encoding, соответственно. Строки символов Unicode, посылаемые в канал вывода, преобразуются в соответствующую кодировку (или в системную кодировку по умолчанию, если атрибут encoding is None).
  • Кодировка имен файлов в файловой системе, возвращается функцией sys.getfilesystemencoding(). Имена файлов, представленные в программе на Python как строки символов Unicode, преобразуются в эту кодировку для взаимодействия с файловой системой (или в системную кодировку по умолчанию, если sys.getfilesystemencoding() возвращает None).
  • Кодировка для текстовых данных, заданная пользовательскими настройками, возвращается функцией locale.getpreferredencoding(False).

Кодировки в Python 2:


>>> import sys, locale
>>> sys.getdefaultencoding()
'ascii'
>>> sys.stdin.encoding
'cp866'
>>> sys.stdout.encoding
'cp866'
>>> sys.stderr.encoding
'cp866'
>>> sys.getfilesystemencoding()
'mbcs'
>>> locale.getpreferredencoding(False)
'cp1251'

Кодировки в Python 3:


>>> import sys, locale
>>> sys.getdefaultencoding()
'utf-8'
>>> sys.stdin.encoding
'cp866'
>>> sys.stdout.encoding
'cp866'
>>> sys.stderr.encoding
'cp866'
>>> sys.getfilesystemencoding()
'mbcs'
>>> locale.getpreferredencoding(False)
'cp1251'

Как видим, системной кодировкой по умолчанию для Python 2 является ascii, а для Python 3 - utf-8. И это единственное обнаруженное различие.

Нам, русским, очень "повезло" с обилием кириллических кодировок в Windows. Работая в консоли Windows, по умолчанию мы имеем дело с кириллической кодировкой cp866. Работая с текстовым файлом в Блокноте, по умолчанию мы работаем в кириллической кодировке cp1251. Имена файлов, использующие русские символы, в файловой системе Windows представлены в кодировке mbcs (multi-byte character set), - это двухбайтовая кодировка, которая позволяет представить подмножество символов Unicode (UTF-16?).

Вооружившись знанием об используемых по умолчанию кодировках, попробуем в интерактивном режиме Pyhton вводить и выводить строки, включающие нелатинские символы.

Фрагмент интерактивного сеанса Python 2:


>>> u = u'Привет world'
>>> type(u)
<type 'unicode'>
>>> u
u'\u041f\u0440\u0438\u0432\u0435\u0442 world'
>>> print u, u.encode('cp866')
Привет world Привет world

Что я только что сделал?

Команды вводятся через стандартный входной поток, использующий кодировку cp866 (sys.stdin.encoding). Таким образом, в первом предложении присваивания литерал u'Привет world' преобразуется в строку unicode из кодировки cp866 и полученная строка unicode присваивается переменной u. Далее я проверил тип и значение переменной u и увидел, что русские буквы представлены двухбайтовыми кодами Unicode. Наконец, предложение print посылает в стандартный выходной поток данную строку unicode и строку str, полученную преобразованием строки u в кодировку cp866. Результат вывода обеих строк одинаков, поскольку строка unicode неявно преобразуется при выводе в стандартый выходной поток в кодировку cp866 (sys.stdout.encoding).

В Python 3 получим такой результат (предлагаю интерпретировать его самостоятельно):


>>> s = 'Привет world'
>>> type(s)
<class 'str'>
>>> s
'Привет world'
>>> print(s, s.encode('cp866'))
Привет world b'\x8f\xe0\xa8\xa2\xa5\xe2 world'

Идем дальше.

В Python 2 для использования в скриптах не-ASCII символов нужно явно указывать кодировку исходного файла, поскольку системная кодировка по умолчанию, ascii, подразумевает использование в файле только символов ASCII. А в Python 3 исходный файл по умолчанию содержит символы Unicode в кодировке utf-8, что позволяет использовать в нем практически любые символы без явного указания кодировки.

Файл hello.py в кодировке UTF-8 для Python 2:


# -*- coding: utf-8 -*-

s = u'Привет world!'
print type(s), s, s.encode('cp866')

Выполню его в консоли Windows:


C:\_sandbox> c:\Python27\python.exe hello.py
<type 'unicode'> Привет world! Привет world!

Файл hello3.py в кодировке UTF-8 для Python 3:


# нас устраивает кодировка по умолчанию utf-8 

s = 'Привет world!'
print(type(s), s, s.encode('cp866'))

Выполню его в консоли Windows:


C:\_sandbox> c:\Python33\python.exe hello3.py
<class 'str'> Привет world! b'\x8f\xe0\xa8\xa2\xa5\xe2 world!'

Разница с работой скрипта hello.py в том, что теперь к кодировке стандартного потока вывода приводится значение типа str, и строка байтов bytes не интерпретируется как строка читабельных символов.

Следующий скрипт helloname.py демонстрирует ввод и вывод кириллических символов в Python 2, используя для их хранения строки unicode:


# -*- coding: utf-8 -*-

import sys

def uraw_input(prompt):
    return unicode(raw_input(prompt.encode(sys.stdout.encoding)), sys.stdin.encoding)

name = uraw_input(u'Привет! Ваше имя? ')
print u'Привет, %s!' % name

Выполню скрипт в консоли Windows:


C:\_dev\GOLD>c:\Python27\python.exe helloname.py
Привет! Ваше имя? Андрей
Привет, Андрей!

Посмотрим теперь, что происходит при записи строк символов Unicode в файл. Если в Python 2 явно не преобразовывать выводимые в файл строки unicode в строки str с нужной кодировкой, то получим ошибку.


# -*- coding: UTF-8 -*-

with open('hello.txt', 'w') as f:
    print 'File encoding:', f.encoding
    f.write(u'Привет world!')

Выполню скрипт в консоли Windows:


C:\_sandbox> c:\Python27\python.exe hellofile.py
File encoding: None
Traceback (most recent call last):
  File "hellofile.py", line 5, in 
    f.write(u'╨Я╤А╨╕╨▓╨╡╤В world!')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

При записи в файл Python 2 пытается преобразовать строку unicode в системную кодировку по умолчанию, ascii, поскольку атрибут encoding открытого нами файла имеет значение None. В кодировке ascii нельзя представить кириллические символы, из-за чего и возникает ошибка.

Во избежание ошибок, при записи строк unicode в файл нужно явно приводить их к желаемой кодировке:


# -*- coding: UTF-8 -*-

with open('hello.txt', 'w') as f:
    print 'File encoding:', f.encoding
    f.write(u'Привет world!'.encode('cp866'))

Выполню исправленный скрипт в консоли Windows:


C:\_sandbox> c:\Python27\python.exe hellofile.py
File encoding: None

C:\_sandbox> type hello.txt
Привет world!

Аналогичный эксперимент с Python 3 показывает, что строки пишутся в файл в кодировке, определяемой локалью пользователя!


with open('hello3.txt', 'w') as f:
    print('File encoding:', f.encoding)
    f.write('Привет world!')

Выполняю скрипт в консоли Windows:


C:\_sandbox> c:\Python33\python.exe hellofile3.py
File encoding: cp1251

Выше мы видели, что на русифицированной Windows функция locale.getpreferredencoding(False) возвращает 'cp1251'. Именно с этой кодировкой открывается новый файл по умолчанию, и, как следствие, к ней приводятся строки, записываемые в этот файл.

С кодировкой файла cp1251 запись в него смешанной латино-кириллической строки проходит на ура, а вот попытка записи кандзи вместе с кириллицей приводит к уже знакомой нам ошибке:


with open('hello31.txt', 'w') as f:
    print('File encoding:', f.encoding)
    f.write('Привет 世界!')

Выполняю скрипт в консоли Windows:


C:\_sandbox> c:\Python33\python.exe hellofile31.py
File encoding: cp1251
Traceback (most recent call last):
  File "hellofile31.py", line 3, in 
    f.write('Привет \u4e16\u754c!')
  File "c:\Python33\lib\encodings\cp1251.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 7-8: character maps to 

Кодировка cp1251 не кодирует кандзи!

Хорошая новость в том, что в Python 3, в отличие от Python 2, при открытии файла можно явно указать кодировку файла. В эту кодировку и будут преобразовываться строки str при записи в файл; из этой кодировки будут преобразовываться в str читаемые из файла строки байтов.

Укажу явно кодировку открываемых файлов в скрипте hello32.py:


with open('hello31.txt', 'w', encoding='utf-8') as f:
    print('File encoding:', f.encoding)
    f.write('Привет world!')

with open('hello32.txt', 'w', encoding='utf-8') as f:
    print('File encoding:', f.encoding)
    f.write('Привет 世界!')

with open('hello31.txt', encoding='utf-8') as f:
    print('File encoding:', f.encoding)
    print(f.read())

Выполняю скрипт в консоли Windows:


C:\_sandbox> c:\Python33\python.exe hellofile32.py
File encoding: utf-8
File encoding: utf-8
File encoding: utf-8
Привет world!

Как видим, скрипт пишет в файл и читает из файла строки в кодировке utf-8.

В заключение, экзотический пример кода. Вследствие того, что в Python 3 системной кодировкой по умолчанию является utf-8, в Python 3 можно использовать в идентификаторах не только латиницу, но и другие символы Unicode:


>>> def привет_5_раз(имя):
...     for i in range(5):
...             print('Привет,', имя)
...
>>> привет_5_раз('Медвет')
Привет, Медвет
Привет, Медвет
Привет, Медвет
Привет, Медвет
Привет, Медвет

Сопровождать такой код и вносить в него изменения интернациональной команде разработчиков будет проблематично!

Проделанные сравнительные эксперименты не дали мне достаточно оснований, чтобы решительно встать на одну из сторон в священной войне между защитниками Python 2 и энтузиастами Python 3 :). Хотя Unicode-ориентированность Python 3 и то, как это сказывается на прикладном программировании, мне нравится.

2 комментария:

  1. Этот текст мне очень помог, спасибо.

    ОтветитьУдалить
  2. Спасибо! Приятно читать человека понимающего о чем он пишет.

    ОтветитьУдалить