View on GitHub

Methodics of Python Intro

Practical materials for learning Python

В этой главе мы отступим от объектных тем и рассмотрим более практическую тему работы с файлами.

Быстрый поиск


Основные типы данных

Прежде всего рассмотрим типы данных, с которыми нам предстоит встретиться. Мы уже привыкли к строковым данным. В Python строки — уникальный объект, лишь внешне похожий на массивы символов. На деле это отдельная структура с собственными свойствами. Если же нам необходимо работать именно с массивом символов, нам нужны структуры bytes и bytearray.

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

>>> b = b'QAWlkbav'  
>>> type(b)  
<class 'bytes'>  
>>> b  
b'QAWlkbav'
>>> b.find(b'we')  
-1  
>>> b.find(b'av')  
6  
>>> b'we' + 'we'  
Traceback (most recent call last):  
 File "<python-input-11>", line 1, in <module>
   b'we' + 'we'
   ~~~~~~^~~~~~
TypeError: can't concat str to bytes
>>>
>>> b[1:-1]  
b'AWlkba'  
>>> b[3]  
108  
>>> type(b[3])  
<class 'int'>  
>>>

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

>>> bytearray(b)  
bytearray(b'QAWlkbav')  
>>> ba = bytearray([1, 2, 3])  
>>> ba  
bytearray(b'\x01\x02\x03')  
>>> ba[1] = 100  
>>> ba  
bytearray(b'\x01d\x03')  
>>> list(ba)  
[1, 100, 3]  
>>>

Кодировки

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

Исторически никакой строгой договорённости для кодировок не было. При возникновении необходимости кодировать разные языки было решено унифицировать первые 128 знаков (ASCII 0-127: туда вошли цифры, спец.символы, знаки пунктуации и арифметики и латиница), а все последующие знаки определять согласно предлагаемой кодировкой, у каждого, вообще говоря, своей. Первые кодировки были исключительно однобайтовые, что вызывало, как минимум, проблему количества доступных знаков. Кроме того, без унификации даже для одних и тех же языков возникали различные кодировки, что было говорить о разных языках. В Python можно пронаблюдать эту проблему с кодировками, для перевода знаков в байты необходимо использовать метод encode, для обратного действия — decode.

>>> s = "Вопрос:"
>>> s.encode("CP1251")
b'\xc2\xee\xef\xf0\xee\xf1:'
>>> res = s.encode("CP1251")
>>> res
b'\xc2\xee\xef\xf0\xee\xf1:'
>>> len(res)
7
>>> res.decode("KOI8-R")
'бНОПНЯ:'
>>>

Кроме таких смешных, но всё же поддающихся декодировке случаев встречаются и недекодируемые:

>>> s
'Вопрос:'
>>> s.encode("CP1251").decode("latin3")
Traceback (most recent call last):
 File "<python-input-40>", line 1, in <module>
   s.encode("CP1251").decode("latin3")
   ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
 File "/usr/lib/python3.13/encodings/iso8859_3.py", line 15, in decode
   return codecs.charmap_decode(input,errors,decoding_table)
          ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'charmap' codec can't decode byte 0xf0 in position 3: character maps to <undefined>
decoding with 'latin3' codec failed
>>>

Для унификации всех существующих знаков был разработан общий универсальный способ каталогизации Unicode. В нем собраны элементы всех языков, обозначений, символов и др. На базе Unicode было собрано семейство кодировок UTF. Их особенность — неравномерная многобайтовая кодировка символов. По специальным битам определяется количество байт, соответствующее знаку, после чего проводится декодирование. Таблица расчёта следующая:

0·······            — 1 байт / 7 бит
110·····   10······ — 2 байта / 11 битов
1110···· 2×10······ — 3 байта / 16 битов
11110··· 3×10······ — 4 байта / 21 бит
>>> s.encode("UTF-8")  
b'\xd0\x92\xd0\xbe\xd0\xbf\xd1\x80\xd0\xbe\xd1\x81:'  
>>> res = s.encode("UTF-8")  
>>> type(res)  
<class 'bytes'>  
>>> list(res)  
[208, 146, 208, 190, 208, 191, 209, 128, 208, 190, 209, 129, 58]  
>>> for i in res:  
...     print(f'{i:08b}')
...
11010000  
10010010  
11010000  
10111110  
11010000  
10111111  
11010001  
10000000  
11010000  
10111110  
11010001  
10000001  
00111010  
>>>

В Python используется некоторая модификация классического UTF-8, преимущественно хранящая всё в двубайтовой кодировке (с поддержкой однобайтового ASCII). Причём по возможности Python уменьшает размер, если все символы можно уложить в ASCII.

>>> import sys  
>>> sys.getsizeof("qwe")  
44  
>>> sys.getsizeof("qwer")  
45  
>>> sys.getsizeof("qwerЮ")  
68  
>>> sys.getsizeof("qwerЮЮ")  
70  
>>>

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

Также существует возможность работать напрямую с таблицей Unicode, для этого используется библиотека unicodedata, в которой поиск по названию символа, по его коду, по его виду.

>>> import unicodedata  
>>> unicodedata.lookup("MIDDLE DOT")  
'·'  
>>> unicodedata.name('«')  
'LEFT-POINTING DOUBLE ANGLE QUOTATION MARK'  
>>> '\u2102'  
'ℂ'  
>>> '\N{FOR ALL}'  
'∀'  
>>>

Файлы

Перед работой с файлами изучим доволно важный протокол для работы с объектами, имеющими внутренний контекст — протокол контекстного менеджера. Протокол описывает два метода: __enter__ для входа в контекст и __exit__ для выхода. Он позволяет обрабатывать, во-первых, инициализацию объектов, связанных с контекстом, и, во-вторых, вне зависимости от причины выхода корректно обрабатывать завершение работы с контекстом. Синтаксис контекстного менеджера — ключевое слово with.

>>> class CM:  
...     def __init__(self, val = None):
...         self.val = val
...
...     def __enter__(self):
...         print("ENTER")
...
...     def __exit__(self, *args):
...         print("EXIT",*args)
...
...
>>>
>>> with CM(123) as cm:  
...     print("ASKLFASF")
...
ENTER
ASKLFASF
EXIT None None None
>>> with CM(123) as cm:
...     print("BBB")
...     raise RuntimeError("QQ!")
...     print("CCC")
...
ENTER  
BBB  
EXIT <class 'RuntimeError'> QQ! <traceback object at 0x7fe2c8144180>  
Traceback (most recent call last):  
 File "<python-input-2>", line 5, in <module>
   raise RuntimeError("QQ!")
RuntimeError: QQ!  
>>>

Теперь непосредственно к работе с файлами. Python различает байтовый и текстовый потоки, байтовый будет образатываться без предварительных действий, текстовый (поскольку хранится он всё равно как последовательность байт) будет декодироваться согласно установленной кодировке (при ручном указании) или локали (при автоматической обработке). Для работы с локалью используется специальная библиотека locale.

>>> import locale  
>>> locale.getlocale()  
('en_US', 'UTF-8')  
>>>

Создадим вне Python файл с календарем. Для открытия файла будем использовать контекстный менеджер. Для получения данных используется метод read. Аналогичный эффект можно получить и вручную, просто обрабатывая список строк файла.

>>> with open('o') as file:  
...     print(file.read())
...
    November 2026
Su Mo Tu We Th Fr Sa
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30

  
>>> with open('o') as file:
...     for line in file:
...         print(line, end='')
...
    November 2026
Su Mo Tu We Th Fr Sa
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30

>>>

Теперь откроем файл, как бинарный. Для английской локали при использовании ASCII 0-127 изменения практически не заметны. Но если взять файл с другими знаками, например, записанными с русской локалью, в байтовом представлении будут явно заметны байты кодировки:

>>> with open('calend', 'rb') as file:
...     print(file.read())
...
b'    November 2026   \nSu Mo Tu We Th Fr Sa\n 1  2  3  4  5  6  7\n 8  9 10 11 12 13 14\n15 16 17 18 19 20 21\n22 23 24 25 26 27 28\n29 30               \n                    \n'
>>>
>>> with open('datefile', 'r') as file:
...     print(file.read())
...
Вс 08 ноя 2026 20:26:00 MSK

>>> with open('datefile', 'rb') as file:
...     print(file.read())
...
b'\xd0\x92\xd1\x81 08 \xd0\xbd\xd0\xbe\xd1\x8f 2026 20:26:00 MSK\n'
>>>

С объектами файлов можно работать и без контекстного менеджера. Файлы можно читать построчно:

>>> file = open('calend')
>>> file.readline()
'    November 2026   \n'
>>> file.readline()
'Su Mo Tu We Th Fr Sa\n'
>>> file.readline()
' 1  2  3  4  5  6  7\n'
>>> file.readline()
' 8  9 10 11 12 13 14\n'
>>> file.readline()
'15 16 17 18 19 20 21\n'
>>> file.readline()
'22 23 24 25 26 27 28\n'
>>> file.readline()
'29 30               \n'
>>> file.readline()
'                    \n'
>>> file.readline()
''
>>> file.readline()
''
>>>

Можно управлять указателем ввода-вывода (при этом в случае с многобайтовой кодировки можно попасть в середину знака, и декодирование будет невозможно):

>>> file = open('datefile', 'rb')
>>> file.seek(10)
10
>>> file.readline()
b'\xd0\xbe\xd1\x8f 2026 20:26:00 MSK\n'
>>> file.seek(5)
5
>>> file.readline().decode()
'08 ноя 2026 20:26:00 MSK\n'
>>> file.seek(13)
13
>>> file.readline()
b'\x8f 2026 20:26:00 MSK\n'
>>> file.seek(13)
13
>>> file.readline().decode()
Traceback (most recent call last):
  File "<python-input-27>", line 1, in <module>
    file.readline().decode()
    ~~~~~~~~~~~~~~~~~~~~~~^^
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8f in position 0: invalid start byte
>>>

Pathlib

В Python для работы с файлами всё популярнее становится библиотека pathlib. Она позволяет легко взаимодействовать с файлами, получать данные о файле-объекте, о содержимом. Кроме этого pathlib предоставляет удобный интерфейс работы с именами файлов.

>>> from pathlib import Path
>>> p = Path("calend")
>>> p.exists(), p.stat(), p.expanduser()
(True, os.stat_result(st_mode=33188, st_ino=1985178, st_dev=52, st_nlink=1, st_uid=1000, st_gid=1000, st_size=168, st_atime=1782120520, st_mtime=1782120365, st_ctime=1782120365), PosixPath('calend'))
>>> p.read_bytes()
b'    November 2026   \nSu Mo Tu We Th Fr Sa\n 1  2  3  4  5  6  7\n 8  9 10 11 12 13 14\n15 16 17 18 19 20 21\n22 23 24 25 26 27 28\n29 30               \n                    \n'
>>> p = p / ".." / 'datefile'
>>> p
PosixPath('calend/../datefile')
>>> p.resolve()
PosixPath('/home/papillon_rouge/datefile')
>>> p.exists()
False
>>>

Потоки данных

Все данные передаются в системе по трём потокам — ввода, вывода и [вывода] ошибок. Для прямого обращения к ним используются специальные прееменные в sys. Для строкового формата обращение прямое, для байтового — через указание обращения к буферу:

>>> import sys  
>>> print("aa", file=sys.stderr)  
aa  
>>> w = sys.stdout.buffer.write(b'QWRETY')
QWRETY>>>
>>> w  
6  
>>>

Для строковых или байтовых данных можно создавать динамические файлы-потоки StringIO и BytesIO:

>>> import io  
>>> file = io.StringIO("sadasd\nqweqwe\nzxv\n")  
>>> file.readline()  
'sadasd\n'  
>>> file.readline()  
'qweqwe\n'  
>>> file.readline()  
'zxv\n'  
>>> file.readline()  
''  
>>>

Сериализация объектов

В файлы возможно сохранять и изымать любые данные, поскольку всё представимо в виде байт и при должной кодировке может быть описано. По такому принципу и работает сериализация объектов — запись и последующее чтение объектов в файлы и обратно. Для сериализации используется модуль pickle. Он позволяет сериализовать (причём не одним способом, можно выбрать протокол сериализации) объекты в файл или в строку, а затем произвести обратную операцию.

>>> import pickle  
>>> pickle.dumps(123)  
b'\x80\x04K{.'  
>>> pickle.dumps([1, 2, 3, 4, 5])  
b'\x80\x04\x95\x0f\x00\x00\x00\x00\x00\x00\x00]\x94(K\x01K\x02K\x03K\x04K\x05e.'  
>>>
>>> res = [123, 234.234, "WERWERWER", (1, 2, 3)]  
>>> pickle.dumps(res, 0)  
b'(lp0\nI123\naF234.234\naVWERWERWER\np1\na(I1\nI2\nI3\ntp2\na.'  
>>>
>>> with open('o', 'wb') as file:  
...     pickle.dump(res, file)
...
>>>
~@localhost: hexdump -C o
00000000  80 04 95 24 00 00 00 00  00 00 00 5d 94 28 4b 7b  |...$.......].(K{|
00000010  47 40 6d 47 7c ed 91 68  73 8c 09 57 45 52 57 45  |G@mG|..hs..WERWE|
00000020  52 57 45 52 94 4b 01 4b  02 4b 03 87 94 65 2e     |RWER.K.K.K...e.|
0000002f  
~@localhost: python3.13
Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] on linux  
Type "help", "copyright", "credits" or "license" for more information.  
>>> import pickle  
>>> with open('o', 'rb') as file:  
...     res = pickle.load(file)
...
>>> res  
[123, 234.234, 'WERWERWER', (1, 2, 3)]  
>>>

Упаковка структур

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

~@localhost: cat > file
qqq
qw  
~@localhost: hexdump -C file
00000000  71 71 71 0a 71 77 0a                              |qqq.qw.|
00000007  
~@localhost: python3.13
Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] on linux  
Type "help", "copyright", "credits" or "license" for more information.  
>>> import struct  
>>> FMT = '4sHc'  
>>> with open('file', 'rb') as f:  
...     data = f.read()
...
>>> data  
b'qqq\nqw\n'  
>>> struct.unpack(FMT, data)  
(b'qqq\n', 30577, b'\n')  
>>> struct.pack(FMT, b'qaqa', 200, b'\0')  
b'qaqa\xc8\x00\x00'  
>>>

struct позволяет не просто задавать формат, но и фиксировать порядок байт, в котором должно записываться или читаться полученное сообщение: Big Endian, Little Endian, Network Format