Внутри питона есть ЕЩЕ виртуальные машиныМы все знаем, что сам питон - одна большая стековая виртуальная машина, которая выполняет опкоды. Их мы можем посмотреть через dis:
>>> import dis
>>> dis.dis('x + y')
0 RESUME 0
1 LOAD_NAME 0 (x)
LOAD_NAME 1 (y)
BINARY_OP 0 (+)
RETURN_VALUE
Можем получить список всех опкодов, можем вызвать их оптимизации и посмотреть на результат с оптимизациями.
Но! Внутри CPython есть и другие виртуальные машины. Сегодня поговорим про ту, которой все мы всегда пользовались, но не знали, что она - виртуальная машина.
pickleДа, не удивляйтесь. Встроенный протокол сериализации в питоне работает благодаря отдельной стековой виртуальной машине. Давайте посмотрим.
>>> class User:
... def __init__(self, username: str, tags: list[str]) -> None:
... self.username = username
... self.tags = tags
... def __reduce__(self) -> tuple[type['User'], tuple[Any, ...]]:
... return (type(self), (self.username, self.tags))
Создадим обычный класс и запиклим его объект:
>>> import pickle
>>> user = User('sobolevn', tags=['python', 'tg'])
>>> pickle.dumps(user, protocol=0)
b'c__main__\nUser\np0\n(Vsobolevn\np1\n(lp2\nVpython\np3\naVtg\np4\natp5\nRp6\n.'
Обратите внимание, что в разных протоколах значение будет разное:
>>> pickle.dumps(user, protocol=1)
b'c__main__\nUser\nq\x00(X\x08\x00\x00\x00sobolevnq\x01]q\x02(X\x06\x00\x00\x00pythonq\x03X\x02\x00\x00\x00tgq\x04etq\x05Rq\x06.'
Всегда необходимо тестировать, что pickle работает для всех версий от 0 до pickle.HIGHEST_PROTOCOL для ваших объектов, которые поддерживают такой способ сериализации.
Что внутри?Можно, глядя на значения, подумать, что там просто лежит какой-то бинарный формат сериалиации. Однако, там лежат опкоды виртуальной машины для сериалиации объектов. Их можно задисить:
>>> import pickletools
>>> pickletools.dis(pickle.dumps(user, protocol=1))
0: c GLOBAL '__main__ User'
15: q BINPUT 0
17: ( MARK
18: X BINUNICODE 'sobolevn'
31: q BINPUT 1
33: ] EMPTY_LIST
34: q BINPUT 2
36: ( MARK
37: X BINUNICODE 'python'
48: q BINPUT 3
50: X BINUNICODE 'tg'
57: q BINPUT 4
59: e APPENDS (MARK at 36)
60: t TUPLE (MARK at 17)
61: q BINPUT 5
63: R REDUCE
64: q BINPUT 6
66: . STOP
highest protocol among opcodes = 1
Сравните, как будет отличаться вывод для другого протокола, например пятого.
И окажется, что все "случайные" символы на самом деле просто так же обозначают опкоды. Теперь мы умеем их читать.
Мы можем найти все опкоды и посмотреть их доки:
>>> pickletools.opcodes[25].code
']'
>>> pickletools.opcodes[25].doc
'Push an empty list.'
И мы даже можем
оптимизировать байткод pickle для более быстрой сериализации / десериализации.
Прям полностью настоящая ВМ :)
Вот за счет чего мы можем с помощью pickle сериализовать любой Python объект (почти), а с помощью других средств - получается сильно сложнее.
Обсуждение: Знали о такой детали реализации? Знаете ли вы как работает pickle сам по себе? Зачем нужны протоколы и версии? Или сделать отдельный пост про детали работы? Знаете ли вы, что pickle - фундаментально небезопасный протокол? И нельзя запускать чужие дампы, только свои доверенные?
Загадка: кстати, какие еще виртуальные машины внутри CPython вы знаете? Я назвал только одну из нескольких. Заходите в комменты за ответами, правильные - покажу завтра.