Obsah
1. Manipulace s binárními datovými strukturami v Pythonu
2. Základní datové typy programovacího jazyka Python
3. Datový typ bytes: neměnitelné sekvence bajtů
5. Reprezentace sekvence bajtů řetězcem s hexadecimálními hodnotami
6. Převod řetězce s hexadecimálními ciframi na hodnotu typu bytes
7. Převod řetězce na sekvenci bajtů se specifikací kódování
8. Datový typ bytearray: měnitelné sekvence bajtů
9. Převody mezi typem bytearray a řetězcem s hexadecimálními hodnotami
10. Převod řetězce na pole bajtů se specifikací kódování
12. Otestování velikosti objektů typu řetězec v Pythonu 3.12
13. Práce s datovými strukturami uloženými v binárním formátu: knihovna struct
14. Uložení numerických hodnot do binárního formátu
16. Praktická ukázka: uložení části chunku do rastrového formátu PNG
17. Zápis celého chunku i s kontrolním součtem
18. Celý skript pro vygenerování souboru ve formátu PNG
19. Repositář s demonstračními příklady
1. Manipulace s binárními datovými strukturami v Pythonu
Ve vývojářské praxi se poměrně často (i když se to nemusí zdát, tak prakticky každý den) setkáme s nutností zpracování dat uložených v binární podobě, tj. například v binárních souborech atd. Programovací jazyk Python nám v této oblasti nabízí tři úrovně abstrakce.
Na nejvyšší úrovni této abstrakce existují knihovny pro zpracování binárních souborů se známým formátem. Pravděpodobně nejpoužívanější jsou knihovny pro načítání a ukládání rastrových obrázků (Pillow resp. PIL). Ale podobné knihovny jsou k dispozici i pro další binární formáty.
Na nejnižší úrovni abstrakce můžeme pracovat s jednotlivými bajty. K tomuto účelu slouží základní datové typy bytes a bytearray, s nimiž se dnes seznámíme.
A na prostřední úrovni abstrakce – která je z programátorského pohledu možná nejzajímavější – můžeme pracovat s datovými strukturami uloženými v binárním formátu s využitím standardní knihovny struct. I s možnostmi této knihovny se dnes seznámíme.
2. Základní datové typy programovacího jazyka Python
Nejprve se v dnešním článku musíme alespoň ve stručnosti zmínit o základních datových typech programovacího jazyka Python. Jedná se o typy, které jsou automaticky přiřazeny hodnotám a které dokáže interpret Pythonu správně rozeznat na základě zapsané hodnoty (někdy se setkáme i s pojmem literál, což ovšem nemusí být někdy zcela přesné) nebo které jsou získány jako návratová hodnota nějakého konstruktoru popř. nějaké konverzní funkce:
Typ | Stručný popis typu |
---|---|
bool | pravdivostní typ se dvěma povolenými hodnotami True a False (interně se ovšem jedná o hodnoty 1 a 0) |
int | celočíselné hodnoty s neomezeným rozsahem |
float | hodnoty s plovoucí řádovou čárkou (interně se jedná o typ double podle IEEE 754) |
complex | komplexní čísla (což je dvojice hodnot s plovoucí řádovou čárkou, každá hodnota je typu double) |
list | seznamy |
tuple | n-tice |
range | objekt typu range |
str | řetězce (v Pythonu 3 se přitom vždy jedná o Unicode řetězce) |
bytes | neměnitelná (immutable) sekvence bajtů |
bytearray | měnitelná (mutable) sekvence bajtů |
set | měnitelné (mutable) množiny |
frozenset | neměnitelné (immutable) množiny |
NoneType | typ s jedinou hodnotou None |
V dalším textu nás budou zajímat vztahy (a taktéž rozdíly) mezi datovými typy nazvanými str, bytes a bytearray, protože by se mohlo na první pohled zdát, že se jedná o zaměnitelné typy (resp. přesněji řečeno jejich hodnoty). Ve skutečnosti tomu tak ovšem v praxi v žádném případě není.
3. Datový typ bytes: neměnitelné sekvence bajtů
Začneme tím nejjednodušším datovým typem určeným pro ukládání libovolných binárních hodnot (navíc o libovolné délce). Tento datový typ, který se jmenuje bytes, je součástí balíčku builtins, jenž není nutné explicitně importovat:
Help on class bytes in module builtins: class bytes(object) | bytes(iterable_of_ints) -> bytes | bytes(string, encoding[, errors]) -> bytes | bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer | bytes(int) -> bytes object of size given by the parameter initialized with null bytes | bytes() -> empty bytes object | | Construct an immutable array of bytes from: | - an iterable yielding integers in range(256) | - a text string encoded using the specified encoding | - any object implementing the buffer API. | - an integer | | Methods defined here: | | __add__(self, value, /) | Return self+value. | | __buffer__(self, flags, /) | Return a buffer object that exposes the underlying memory of the object.
Hodnoty datového typu bytes můžeme chápat jako sekvenci bajtů libovolné hodnoty, přičemž důležité je, že tato sekvence je neměnitelná (immutable). To znamená, že nelze měnit hodnoty bajtů, ale taktéž nejde do sekvence přidat další bajty. Tento datový typ se používá velmi často, i když mnohdy „skrytě“ (hodnota typu bytes bývá získána konverzí například řetězce atd., což si ostatně ještě ukážeme). Najdeme ho u vstupně-výstupních operací atd. Je tedy vhodné znát, jakým způsobem se s tímto datovým typem v Pythonu pracuje.
Hodnotu typu bytes lze získat několika způsoby, přičemž základní způsob využívá konstruktor, který je taktéž nazvaný bytes. Tento konstruktor se používá následujícím způsobem:
x = bytes(10) print(type(x)) print(x) print() y = bytes((1, 2, 3, 254, 255)) print(type(y)) print(y) print() z = bytes() print(type(z)) print(z)
Výsledky:
<class 'bytes'> b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' <class 'bytes'> b'\x01\x02\x03\xfe\xff' <class 'bytes'> b''
O tom, že hodnoty typu bytes jsou skutečně neměnitelné, se lze velmi snadno přesvědčit, například pokusem o modifikaci jednoho bajtu ze sekvence:
x = bytes(10) print(type(x)) print(x) x[0] = 42 print(x)
Vyhozená výjimka má v tomto případě poněkud matoucí typ TypeError:
Traceback (most recent call last): File "/home/ptisnovs/src/most-popular-python-libs/binary_structs/bytes_are_immutable.py", line 5, in <module> x[0] = 42 ~^^^ TypeError: 'bytes' object does not support item assignment
4. Literál typu bytes
Poměrně často se ve zdrojových kódech setkáme i se zápisem literálu popisujícího hodnotu typu bytes. Jedná se o (pseudo)řetězec s prefixem b. Tento (pseudo)řetězec přitom může obsahovat pouze ASCII znaky (což je v runtime kontrolováno):
# prefix b s uvozovkami x = bytes(b"foo") print(type(x)) print(x) print() # prefix b s apostrofy y = bytes(b'Write "Hello world"') print(type(y)) print(y) print() # uvozovky nebo apostrofy lze "ztrojit" a poté využít " i ' uvnitř literálu z = bytes(b"""We can use 'a' and "b" there""") print(type(z)) print(z)
Výsledky:
<class 'bytes'> b'foo' <class 'bytes'> b'Write "Hello world"' <class 'bytes'> b'We can use \'a\' and "b" there'
V případě, že v literálu hodnoty typu bytes použijeme znak mimo základní sadu ASCII, vyhodí se výjimka. To si pochopitelně opět velmi snadno ověříme:
x = bytes(b"ěščřžýáíé") print(type(x)) print(x)
Vyhozená výjimka by měla být typu SyntaxError:
File "/home/ptisnovs/src/most-popular-python-libs/binary_structs/bytes_literal_no_ascii.py", line 1 x = bytes(b"ěščřžýáíé") ^^^^^^^^^^^^ SyntaxError: bytes can only contain ASCII literal characters
5. Reprezentace sekvence bajtů řetězcem s hexadecimálními hodnotami
Hodnoty bajtů, které leží mimo rozsah znakové sady ASCII, se na terminálu nebo ve zdrojovém kódu zobrazují poměrně nešikovně (a ještě hůř se zapisují) a proto se velmi často setkáme s tím, že se každý bajt zapíše formou dvojice hexadecimálních cifer. Programovací jazyk Python nám přitom umožňuje provést převod „řetězec s hexadecimálními ciframi“ → „hodnota typu bytes“, tak opačný převod „hodnota typu bytes“ → „řetězec s hexadecimálními ciframi“.
Nejprve se podívejme na způsob převodu hodnoty typu bytes na řetězec obsahující hexadecimálně zapsané hodnoty. Tato operace se provádí metodou hex, které lze předat oddělovač jednotlivých bajtů a taktéž dalším parametrem určit, kolik bajtů má být sdruženo do jedné skupiny:
x = bytes(b"The quick brown fox jumps over the lazy dog") print(x.hex()) print(x.hex(" ")) print(x.hex(" ", 2)) print(x.hex(" ", 4))
Ze zobrazených výsledků je patrné, jaký je význam obou nepovinných parametrů:
54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67 54 68 65 20 71 75 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a 79 20 64 6f 67 54 6865 2071 7569 636b 2062 726f 776e 2066 6f78 206a 756d 7073 206f 7665 7220 7468 6520 6c61 7a79 2064 6f67 546865 20717569 636b2062 726f776e 20666f78 206a756d 7073206f 76657220 74686520 6c617a79 20646f67
6. Převod řetězce s hexadecimálními ciframi na hodnotu typu bytes
Opačný převod, tedy konkrétně převod z řetězce s hexadecimálními hodnotami na hodnotu typu bytes zajišťuje metoda nazvaná fromhex. V dalším ukázkovém příkladu jsou uvedeny korektní zápisy:
x = bytes.fromhex("") print(type(x)) print(x) print() y = bytes.fromhex("00 0f 1f ff") print(type(y)) print(y) print() z = bytes.fromhex("000f1fff") print(type(z)) print(z)
Výsledky:
<class 'bytes'> b'' <class 'bytes'> b'\x00\x0f\x1f\xff' <class 'bytes'> b'\x00\x0f\x1f\xff'
Python však hlídá, aby v každé skupině (pokud je použit oddělovač) byly zapsány vždy celé bajty. Nelze tedy zapsat například „0 “ a očekávat, že si Python doplní druhou nulu:
x = bytes.fromhex("0 0f 1f ff") print(type(x)) print(x)
V tomto případě je vyhozena výjimka:
Traceback (most recent call last): File "/home/ptisnovs/src/most-popular-python-libs/binary_structs/bytes_fromhex_err.py", line 1, in <module> x = bytes.fromhex("0 0f 1f ff") ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ValueError: non-hexadecimal number found in fromhex() arg at position 1
7. Převod řetězce na sekvenci bajtů se specifikací kódování
Poslední operací s hodnotami typu bytes, kterou si prozatím ukážeme, je převod řetězce na sekvenci bajtů s tím, že je možné si zvolit výsledné kódování. Python, resp. přesněji řečeno Python 3.x, totiž zcela rozlišuje mezi řetězci (interně se ukládají v Unicode) a sekvencemi bajtů, které mohou být použity pro fyzické uložení řetězců do souborů atd.. Podívejme se na jednoduchý příklad s několika kódováními, a to včetně kódování šestnáctibitových (resp. takových, kde je znak uložen ve dvou či ve čtyřech bajtech). Postfixy „le“ a „be“ značí „little endian“ a „big endian“:
s = "The quick brown fox jumps over the lazy dog" x = bytes(s, "ascii") print(type(x)) print(x) print() y = bytes(s, "utf-8") print(type(y)) print(y) print() z = bytes(s, "utf-16-le") print(type(z)) print(z) print() w = bytes(s, "utf-16-be") print(type(w)) print(w) print() # UTF-8, ovšem s BOM (používá Notepad a další divné aplikace) q = bytes(s, "utf-8-sig") print(type(q)) print(q) print()
A takto vypadají výsledné sekvence bajtů:
<class 'bytes'> b'The quick brown fox jumps over the lazy dog' <class 'bytes'> b'The quick brown fox jumps over the lazy dog' <class 'bytes'> b'T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g\x00' <class 'bytes'> b'\x00T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g' <class 'bytes'> b'\xef\xbb\xbfThe quick brown fox jumps over the lazy dog'
Podobného výsledku ovšem můžeme dosáhnout i jinak, konkrétně použitím metody string.encode, a to opět se specifikací výsledného kódování:
s = "The quick brown fox jumps over the lazy dog" x = s.encode("ascii") print(type(x)) print(x) print() y = s.encode("utf-8") print(type(y)) print(y) print() z = s.encode("utf-16-le") print(type(z)) print(z) print() w = s.encode("utf-16-be") print(type(w)) print(w) print() # UTF-8, ovšem s BOM (používá Notepad a další divné aplikace) q = s.encode("utf-8-sig") print(type(q)) print(q) print()
Výsledky:
<class 'bytes'> b'The quick brown fox jumps over the lazy dog' <class 'bytes'> b'The quick brown fox jumps over the lazy dog' <class 'bytes'> b'T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g\x00' <class 'bytes'> b'\x00T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g' <class 'bytes'> b'\xef\xbb\xbfThe quick brown fox jumps over the lazy dog'
8. Datový typ bytearray: měnitelné sekvence bajtů
Ve druhé části článku se budeme zabývat standardním datovým typem nazvaným bytearray:
Help on class bytearray in module builtins: class bytearray(object) | bytearray(iterable_of_ints) -> bytearray | bytearray(string, encoding[, errors]) -> bytearray | bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer | bytearray(int) -> bytes array of size given by the parameter initialized with null bytes | bytearray() -> empty bytes array | | Construct a mutable bytearray object from: | - an iterable yielding integers in range(256) | - a text string encoded using the specified encoding | - a bytes or a buffer object | - any object implementing the buffer API. | - an integer | | Methods defined here: ... ... ...
Konstruktor bytearray se používá stejně, jako v případě datového typu bytes:
x = bytearray(10) print(type(x)) print(x) print() y = bytearray((1, 2, 3, 254, 255)) print(type(y)) print(y) print() z = bytearray() print(type(z)) print(z)
Výsledek bude vypadat takto:
<class 'bytearray'> bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') <class 'bytearray'> bytearray(b'\x01\x02\x03\xfe\xff') <class 'bytearray'> bytearray(b'')
Hodnoty bytearray jsou ovšem měnitelné (mutable):
x = bytearray(10) print(type(x)) print(x) x[5] = 42 print(x) x[3] = 0xff print(x)
Ze zobrazených výsledků je patrné, že došlo k modifikaci hodnot bajtů, a to bez vyhození výjimky:
<class 'bytearray'> bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') bytearray(b'\x00\x00\x00\x00\x00*\x00\x00\x00\x00') bytearray(b'\x00\x00\x00\xff\x00*\x00\x00\x00\x00')
9. Převody mezi typem bytearray a řetězcem s hexadecimálními hodnotami
Podívejme se nyní na způsob převodu mezi hodnotou typu bytearray a řetězcem, který bude reprezentovat všechny bajty z tohoto pole s využitím hexadecimálních hodnot (každý bajt odpovídá dvěma znakům ve výsledném řetězci). Dále můžeme specifikovat mezery mezi skupinami hexadecimálních cifer a taktéž to, kolik bajtů má být uloženo (v hexadecimální podobě) v jedné skupině. Nejedná se tudíž o nic nového, protože tento postup již dobře známe z popisu typu bytes. V obou případech postačí zavolat stejně pojmenovanou metodu hex:
x = bytearray(b"The quick brown fox jumps over the lazy dog") print(x.hex()) print(x.hex(" ")) print(x.hex(" ", 2)) print(x.hex(" ", 4))
A takto budou vypadat výsledky získané tímto příkladem:
54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67 54 68 65 20 71 75 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a 79 20 64 6f 67 54 6865 2071 7569 636b 2062 726f 776e 2066 6f78 206a 756d 7073 206f 7665 7220 7468 6520 6c61 7a79 2064 6f67 546865 20717569 636b2062 726f776e 20666f78 206a756d 7073206f 76657220 74686520 6c617a79 20646f67
Poznámka: typy bytes a bytearray se tedy v tomto ohledu neliší.
Prakticky totožným způsobem je realizován i opačný převod, což si lze opět velmi snadno ověřit:
x = bytearray.fromhex("") print(type(x)) print(x) print() y = bytearray.fromhex("00 0f 1f ff") print(type(y)) print(y) print() z = bytearray.fromhex("000f1fff") print(type(z)) print(z)
<class 'bytearray'> bytearray(b'') <class 'bytearray'> bytearray(b'\x00\x0f\x1f\xff') <class 'bytearray'> bytearray(b'\x00\x0f\x1f\xff')
10. Převod řetězce na pole bajtů se specifikací kódování
Jen pro úplnost si ukažme, že modifikovatelná pole bajtů je možné získat i z řetězců, konkrétně zakódováním Unicode řetězců do pole bajtů na základě zvoleného způsobu zakódování znaků. Stejný příklad, jaký jsme si ukázali v souvislosti s typem bytes lze přepsat i pro bytearray:
s = "The quick brown fox jumps over the lazy dog" x = bytearray(s, "ascii") print(type(x)) print(x) print() y = bytearray(s, "utf-8") print(type(y)) print(y) print() z = bytearray(s, "utf-16-le") print(type(z)) print(z) print() w = bytearray(s, "utf-16-be") print(type(w)) print(w) print() # UTF-8 s BOM, pouziva ho napriklad Notepad a dalsi divne aplikace q = bytearray(s, "utf-8-sig") print(type(q)) print(q) print()
Výsledky:
<class 'bytearray'> bytearray(b'The quick brown fox jumps over the lazy dog') <class 'bytearray'> bytearray(b'The quick brown fox jumps over the lazy dog') <class 'bytearray'> bytearray(b'T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g\x00') <class 'bytearray'> bytearray(b'\x00T\x00h\x00e\x00 \x00q\x00u\x00i\x00c\x00k\x00 \x00b\x00r\x00o\x00w\x00n\x00 \x00f\x00o\x00x\x00 \x00j\x00u\x00m\x00p\x00s\x00 \x00o\x00v\x00e\x00r\x00 \x00t\x00h\x00e\x00 \x00l\x00a\x00z\x00y\x00 \x00d\x00o\x00g') <class 'bytearray'> bytearray(b'\xef\xbb\xbfThe quick brown fox jumps over the lazy dog')
Zajímavější je situace u řetězců obsahujících znaky mimo standardní ASCII. V tomto případě pochopitelně není možné provést zakódování do ASCII, takže příslušnou část zdrojového kódu zakomentuji:
s = "ěščřžýáíéů" # nelze převést na čisté ASCII # x = bytearray(s, "ascii") # print(type(x)) # print(x) # print() y = bytearray(s, "utf-8") print(type(y)) print(y) print() z = bytearray(s, "utf-16-le") print(type(z)) print(z) print() w = bytearray(s, "utf-16-be") print(type(w)) print(w) print() # UTF-8 s BOM, pouziva ho napriklad Notepad a dalsi divne aplikace q = bytearray(s, "utf-8-sig") print(type(q)) print(q) print()
Na této ukázce si povšimněte, jak se původní řetězec s pouhými deseti znaky „natáhl“ do mnohem delšího pole bajtů:
<class 'bytearray'> bytearray(b'\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99\xc5\xbe\xc3\xbd\xc3\xa1\xc3\xad\xc3\xa9\xc5\xaf') <class 'bytearray'> bytearray(b'\x1b\x01a\x01\r\x01Y\x01~\x01\xfd\x00\xe1\x00\xed\x00\xe9\x00o\x01') <class 'bytearray'> bytearray(b'\x01\x1b\x01a\x01\r\x01Y\x01~\x00\xfd\x00\xe1\x00\xed\x00\xe9\x01o') <class 'bytearray'> bytearray(b'\xef\xbb\xbf\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99\xc5\xbe\xc3\xbd\xc3\xa1\xc3\xad\xc3\xa9\xc5\xaf')
11. Datový typ str
V souvislosti s binárními daty je vhodné se zmínit i o řetězcích, a to ze dvou důvodů. Prvním důvodem je fakt, že v některých programovacích jazycích se (stále) předpokládá, že jednotlivé znaky a bajty jsou stejný či podobně se chovající datový typ. A za druhé v Pythonu 2.x se skutečně s řetězci a znaky v nich uloženými do jisté míry pracovalo jako s neměnitelnou sekvencí bajtů. V Pythonu 3.x je to ovšem jinak a řetězce jsou od sekvencí bajtů či od polí bajtů sémanticky odděleny. S tím souvisí i nutnost explicitního převodu mezi těmito datovými typy. Jeden z příkladů jsme ostatně již mohli vidět.
V programovacím jazyku Python verze 3.3 došlo k poměrně významné změně, která se týká způsobu interního uložení řetězců. Autoři Pythonu si totiž uvědomili, že na jednu stranu je sice důležité podporovat Unicode (a to celé Unicode, žádný subset typu BMP, vzhledem k normě GB 18030) ovšem v mnoha případech to vede k tomu, že se do operační paměti ukládá až čtyřikrát větší množství dat, než je skutečně nutné, protože mnoho řetězců používaných v každodenní praxi obsahuje pouze ASCII znaky. Navíc větší množství dat uložených v paměti znamená, že se při manipulaci s nimi bude hůře využívat procesorová cache. Proto došlo v rámci PEP 393 k takové úpravě, která zajistí možnost uložení řetězců ve třech formátech, což je naznačeno v tabulce:
Šířka znaku | Kódování | Prefix při zápisu kódu |
---|---|---|
1 bajt | Latin-1 | \x |
2 bajty | UCS-2 | \u |
4 bajty | UCS-4 | \U |
Tyto změny by měly být pro programátory i uživatele zcela transparentní, takže by se nemělo stát, že by například do řetězce původně uloženého s kódováním Latin-1 (nadmnožina ASCII) nešel uložit například znak v azbuce – ostatně řetězce jsou v Pythonu neměnitelné, takže se konverze provede v rámci prováděné operace automaticky.
V každém případě ale ještě více než v předchozích verzích Pythonu platí, že řetězce jsou nezaměnitelné s typem bytes (a už vůbec ne s bytearray, které je navíc měnitelné).
12. Otestování velikosti objektů typu řetězec v Pythonu 3.12
Podívejme se nyní, jak je výběr formátu pro uložení řetězce prováděn při interpretaci řetězcového literálu. Nejprve importujeme modul sys, který nabízí funkci getsizeof():
Python 3.12.7 (main, Oct 1 2024, 00:00:00) [GCC 14.2.1 20240912 (Red Hat 14.2.1–3)] on linux Type „help“, „copyright“, „credits“ or „license“ for more information. >>> import sysZjistíme velikost objektu reprezentujícího prázdný řetězec. Tato velikost se může lišit podle verze Pythonu a použité architektury, nás však budou zajímat rozdíly oproti této hodnotě:
>>> sys.getsizeof("") 42 >>> sys.getsizeof("e 123456789") 52
Nyní vytvoříme řetězec s ne-ASCII znakem. Velikost příslušného objektu se zvětší (opět nás však bude zajímat rozdíl oproti této velikosti, ne její absolutní hodnota):
>>> sys.getsizeof("ě") 60
Dále vypočteme velikost řetězce s ne-ASCII znakem, po němž následuje deset ASCII znaků. Vidíme, že každý znak je uložen ve dvou bajtech – prvním znakem byl totiž určen interní formát celého řetězce:
>>> sys.getsizeof("ě 123456789") 80
Zkusme si nyní vytvořit řetězec s jediným znakem, který nepatří do BPM, tedy ho nelze reprezentovat v UCS-2. Posléze k tomuto znaku přidáme dalších deset znaků a snadno zjistíme, že v tomto případě je každý znak reprezentován čtyřmi bajty ((104–64)/10=4):
>>> sys.getsizeof("\U0001ffff") 64 >>> sys.getsizeof("\U0001ffff 123456789") 104
13. Práce s datovými strukturami uloženými v binárním formátu: knihovna struct
Pro práci s binárními daty na vyšší úrovni abstrakce, než představuje pouhá sekvence bajtů, slouží standardní balíček nazvaný struct. Tento balíček nabízí možnost specifikace binárního formátu pomocí řetězce, v němž se (například) specifikuje, jak široké numerické hodnoty se mají ukládat, jaká má být jejich endianita (viz další text) atd. Dnes si ukážeme jak základní možnosti použití tohoto balíčků, tak i to, jak je možné tento balíček použít pro tvorbu PNG obrázků, a to bez toho, abychom využili nějakou specializovanou knihovnu pro práci s PNG.
Help on module struct: NAME struct MODULE REFERENCE https://docs.python.org/3.12/library/struct.html The following documentation is automatically generated from the Python source files. It may be incomplete, incorrect or include features that are considered implementation detail and may vary between Python implementations. When in doubt, consult the module reference at the location listed above. DESCRIPTION Functions to convert between Python values and C structs. Python bytes objects are used to hold the data representing the C struct and also as format strings (explained below) to describe the layout of data in the C struct.
14. Uložení numerických hodnot do binárního formátu
Nejprve si ukažme, jak se do binárního formátu ukládají celočíselné numerické hodnoty. Pro tento účel použijeme funkci nazvanou struct.pack, které se předá takzvaný řetězec s popisem výsledného formátu a taktéž hodnota či hodnoty, které se mají do výsledného binárního formátu uložit.
Nejjednodušší je ukládání malých čísel bez znaménka či se znaménkem (řízeno velikostí formátovacího znaku):
import struct bytes1 = struct.pack("b", 42) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("B", 42) print(type(bytes2)) print(bytes2.hex(" ", 1))
Výsledky jsou v obou případech totožné:
<class 'bytes'> 2a <class 'bytes'> 2a
Funkce struct.pack testuje, zda je hodnota v požadovaném rozsahu a taktéž to, zda obsahuje znaménko (to má vliv na formát B – tedy hodnoty 0..255):
import struct bytes1 = struct.pack("b", -42) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("B", -42) print(type(bytes2)) print(bytes2.hex(" ", 1))
První převod proběhne v pořádku, druhý podle očekávání skončí vyhozením výjimky:
<class 'bytes'> d6 Traceback (most recent call last): File "/home/ptisnovs/src/most-popular-python-libs/binary_structs/pack_byte_negative.py", line 9, in <module> bytes2 = struct.pack("B", -42) ^^^^^^^^^^^^^^^^^^^^^ struct.error: 'B' format requires 0 <= number <= 255
S využitím znaků b, h, i, l, q lze volit počet bajtů pro uložení celočíselné hodnoty. Postupně se jedná o 8, 16, 32, 64 a 128 bitů. Velká písmena značí stejný rozsah, ovšem pro hodnoty bez znaménka:
import struct for word_type in "bhilq": print(f"Packing as word type '{word_type}'") bytes1 = struct.pack(word_type, 42) print(type(bytes1)) print(bytes1.hex(" ", 1)) print()
Výsledky:
Packing as word type 'b' <class 'bytes'> 2a Packing as word type 'h' <class 'bytes'> 2a 00 Packing as word type 'i' <class 'bytes'> 2a 00 00 00 Packing as word type 'l' <class 'bytes'> 2a 00 00 00 00 00 00 00 Packing as word type 'q' <class 'bytes'> 2a 00 00 00 00 00 00 00
Podobně lze postupovat i u numerických hodnot s plovoucí řádovou čárkou. V tomto případě se pomocí znaků e, f a d volí typ float16, float32 (single) nebo float64 (double):
import struct for float_type in "efd": print(f"Packing as word type '{float_type}'") bytes1 = struct.pack(float_type, 1.0) print(type(bytes1)) print(bytes1.hex(" ", 1)) print()
Výsledky:
Packing as word type 'e' <class 'bytes'> 00 3c Packing as word type 'f' <class 'bytes'> 00 00 80 3f Packing as word type 'd' <class 'bytes'> 00 00 00 00 00 00 f0 3f
15. Řízení endianity
Při ukládání hodnot širších než jeden bajt, tj. při práci s celými čísly i s čísly s plovoucí řádovou čárkou, je nutné řešit problém takzvané endianity. Svět IT je totiž stále rozdělen na systémy a software ukládající bajty v pořadí od bajtu nejvyššího k bajtu nejnižšímu a na systémy, které bajty ukládají v opačném pořadí (a několik systémů pro jistotu obě možnosti kombinuje). Způsob uložení bajtů neboli little endian nebo big endian lze v řetězci s formátem volit znaky < a > popř. použít znak = značící nativní způsob uložení, nebo ! značící uložení odpovídající síťovým protokolům. Ostatně ukažme si, jak bude vypadat uložení celočíselných hodnot do jednoho bajtu, dvou bajtů atd. s různým nastavením endianity. Všechny možné kombinace jsou otestovány v tomto příkladu:
import struct for endianess in "=<>": for word_type in "bhilq": print(f"Packing as word type '{word_type}' using endianess set to {endianess}") bytes1 = struct.pack(f"{endianess}{word_type}", 42) print(type(bytes1)) print(bytes1.hex(" ", 1)) print()
Výsledky jasně ukazují vliv endianity na výslednou sekvenci bajtů:
Packing as word type 'b' using endianess set to = <class 'bytes'> 2a Packing as word type 'h' using endianess set to = <class 'bytes'> 2a 00 Packing as word type 'i' using endianess set to = <class 'bytes'> 2a 00 00 00 Packing as word type 'l' using endianess set to = <class 'bytes'> 2a 00 00 00 Packing as word type 'q' using endianess set to = <class 'bytes'> 2a 00 00 00 00 00 00 00 Packing as word type 'b' using endianess set to < <class 'bytes'> 2a Packing as word type 'h' using endianess set to < <class 'bytes'> 2a 00 Packing as word type 'i' using endianess set to < <class 'bytes'> 2a 00 00 00 Packing as word type 'l' using endianess set to < <class 'bytes'> 2a 00 00 00 Packing as word type 'q' using endianess set to < <class 'bytes'> 2a 00 00 00 00 00 00 00 Packing as word type 'b' using endianess set to > <class 'bytes'> 2a Packing as word type 'h' using endianess set to > <class 'bytes'> 00 2a Packing as word type 'i' using endianess set to > <class 'bytes'> 00 00 00 2a Packing as word type 'l' using endianess set to > <class 'bytes'> 00 00 00 2a Packing as word type 'q' using endianess set to > <class 'bytes'> 00 00 00 00 00 00 00 2a
16. Praktická ukázka: uložení části chunku do rastrového formátu PNG
Podívejme se na nepatrně praktičtější ukázky. Ukážeme si, jak lze s využitím struct.pack vygenerovat sekvenci bajtů s částí chunku s hlavičkou obrázku rastrového formátu PNG. Tato hlavička je uložena v chunku IHDR. Hlavička obrázku obsahuje základní metainformace o uloženém obrázku, zejména jeho rozlišení, bitovou hloubku, typ kódování barev a použitou filtraci. Vzhledem k tomu, že datová část hlavičky obrázku má vždy délku 13 bytů, začíná chunk posloupností 0×00 0×00 0×00 0×0d. Hlavička obrázku musí být uvedena ihned za hlavičkou PNG, při čtení je tedy nutné otestovat, zda se ihned po přečtení hlavičky PNG objeví tato posloupnost. Pokud tomu tak není, mohlo dojít k poškození souboru.
Datová část chunku IHDR obsahuje následující položky:
Offset | Počet byte | Význam |
---|---|---|
00 | 4 | šířka obrázku uvedená v pixelech |
04 | 4 | výška obrázku uvedená v pixelech |
08 | 1 | bitová hloubka pixelů v barvovém kanálu (povolené hodnoty jsou 1, 2, 4, 8 a 16) |
09 | 1 | typ kódování barev (viz další tabulka) |
0a | 1 | použitá metoda komprimace (musí zde být 0-deflate) |
0b | 1 | použitá metoda filtrace (musí zde být 0-adaptivní filtrace) |
0c | 1 | prokládání obrázku (0-bez prokládání, 1-prokládání) |
Metody komprimace i filtrace musí být v současnosti nastaveny na nulu. Pokud je obrázek prokládaný, je příslušný byte nastaven na 0×01, v opačném případě na 0×00. Zbývá nám vysvětlit vztah mezi bitovou hloubkou pixelů v barvovém kanálu a typem kódování barev. V současnosti je možné využít následující kombinace:
Typ kódování barev | Bitová hloubka | Popis |
---|---|---|
0 | 1,2,4,8,16 | Obrázek ve stupních šedi (popř. černobílá pérovka) |
2 | 8,16 | Plnobarevný (true color) obrázek typu RGB |
3 | 1,2,4,8 | Obrázek s barvovou paletou (v souboru musí existovat chunk PLTE) |
4 | 8,16 | Obrázek ve stupních šedi a průhledností (alfa kanálem) – nepříliš používaná kombinace |
6 | 8,16 | Obrázek, kde každý pixel obsahuje všechny čtyři hodnoty RGBA (tj. kromě tří barvových složek i alfa kanál) |
Zápis hlavičky lze realizovat například takto. Povšimněte si především specifikace formátu binárního výstupu ve formátovacím řetězci:
import struct # Width: 4 bytes # Height: 4 bytes # Bit depth: 1 byte # Color type: 1 byte # Compression method: 1 byte # Filter method: 1 byte # Interlace method: 1 byte width = 1024 height = 768 bit_depth = 8 color_type = 6 # RGBA compression_method = 0 # deflate filter_method = 0 # adaptive filter interlace_method = 0 # no interlace bytes1 = b"IHDR" + struct.pack( "!IIBBBBB", width, height, bit_depth, color_type, compression_method, filter_method, interlace_method, ) print(type(bytes1)) print(bytes1.hex(" ", 1))
Výsledek:
<class 'bytes'> 49 48 44 52 00 00 04 00 00 00 03 00 08 06 00 00 00
Navíc lze specifikaci formátu zkrátit tak, že se čísly určí počet opakování hodnot daného typu (viz zvýrazněnou část kódu):
import struct width = 1024 height = 768 bit_depth = 8 color_type = 6 # RGBA compression_method = 0 # deflate filter_method = 0 # adaptive filter interlace_method = 0 # no interlace bytes1 = b"IHDR" + struct.pack( "!2I5B", width, height, bit_depth, color_type, compression_method, filter_method, interlace_method, ) print(type(bytes1)) print(bytes1.hex(" ", 1))
17. Zápis celého chunku i s kontrolním součtem
Nyní si ukažme složitější operaci, a to zápis celého chunku. Ve formátu PNG jsou veškeré informace i metainformace o zpracovávaném obrázku jsou uloženy v blocích dat nazvaných chunky (přibližný překlad je blok). Každý chunk se přito skládá ze čtyř částí:
- První část má konstantní velikost čtyři byty a obsahuje celkovou délku datové části chunku. Teoreticky je tedy maximální délka datové části rovna 232-1 bytům; avšak pro snazší implementaci, například v těch programovacích jazycích, které nemají implementovaný beznaménkový datový typ (v té době se jednalo například o Javu), je maximální hodnota délky rovna „pouze“ 231-1 bytům. V praxi jsou však délky chunků pochopitelně o několik řádů menší.
- Druhá část chunku má opět velikost čtyři byty. Obsahuje jméno (typ) chunku ve formátu čtyř ASCII znaků malé i velké anglické abecedy. Jedná se o ASCII kódy v rozsahu 65–90 a 97–122 decimálně. Jedná se o velmi dobře navržený způsob pojmenování, protože jméno chunku může být načteno a následně rozpoznáno pouhou jednou operací porovnání (32 bitových integerů) bez nutnosti implementace řetězcového porovnání a současně je typ chunku velmi dobře čitelný i pro člověka. Velikost znaků ve jménu (minusky/verzálky) dále určuje, zda je daný chunk pro zpracování obrázku volitelný či povinný.
- Třetí část chunku je tvořena vlastními daty. Tato část může v některých případech mít nulovou délku.
- Poslední čtvrtá část chunku má délku čtyři byty a obsahuje kontrolní součet (CRC) druhé a třetí části, tj. jména (typu) chunku a uložených dat. Vzhledem k tomu, že délka chunku není do kontrolního součtu zahrnuta, je možné CRC generovat přímo při zápisu dat bez nutnosti druhého průchodu v případě, že délka chunku není dopředu známá (například při komprimaci). Použitý polynom má tvar:
x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
Zápis chunku můžeme v Pythonu realizovat například takto:
def png_chunk(png_tag, chunk_data): """Konstrukce jednoho PNG chunku s tagem i závěrečným kontrolním kódem.""" chunk_header = png_tag + chunk_data return (struct.pack("!I", len(chunk_data)) + chunk_header + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_header)))
Povšimněte si, že zde spojujeme sekvence bajtů přetíženým operátorem +. Pro výpočet kontrolního součtu se používá funkce zlib.crc32 a kontrolní součet se počítá z celé hlavičky: délka + typ chunku.
Nyní se podívejme na způsob zápisu celého PNG souboru. Minimální PNG obsahuje trojici chunků IHDR, IDAT a IEND, před nimiž je uvedena hlavička PNG (nikoli hlavička obrázku!). Hlavička PNG má délku pouhých osmi bytů, které mají vždy konstantní hodnoty (ve výpisu uvedené hexadecimálně):
89 50 4e 47 0d 0a 1a 0a
Neboli programově:
PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n'
Zápis rastrových dat do PNG může vypadat následovně:
return b''.join([ PNG_SIGNATURE, png_chunk(b'IHDR', struct.pack("!2I5B", width, height, 8, 2, 0, 0, 0)), png_chunk(b'IDAT', zlib.compress(raw_data, level=9, wbits=15)), png_chunk(b'IEND', b'')])
Ovšem nejdříve je nutné vhodným způsobem upravit buffer (typu bytes nebo bytearray) tak, aby byl před začátkem každého obrazového řádku uložen bajt s hodnotou 0. Ten specifikuje použitý filtr (může zlepšit komprimaci), ovšem my žádný filtr nepoužíváme, takže zde skutečně bude pouze nula:
def prepare_raw_data(buffer, width, height): """Konverze barev pixelů z bufferu do podoby se specifikací filtru na každém řádku.""" raw_data = bytearray() offset = 0 for _ in range(height): # nastavit filtr + zkopirovat jeden radek (scanline) raw_data += FILTER_TYPE + buffer[offset:offset+width*3] # na dalsi radek ve zdrojovem bufferu offset += width*3 return raw_data
Samotný buffer obsahuje barvy jednotlivých pixelů v RGB. Jedná se o typ bytearray, protože potřebujeme mít možnost měnit barvy pixelů:
# buffer pro rastrová data pixels = bytearray(WIDTH*HEIGHT*3) # vybarvení testovacího obrázku index = 0 for i in range(HEIGHT): for j in range(WIDTH): pixels[index] = 0xff index+=1 pixels[index] = i index+=1 pixels[index] = j index+=1
18. Celý skript pro vygenerování souboru ve formátu PNG
Na závěr si ukažme úplný zdrojový kód demonstračního příkladu, po jehož spuštění vznikne následující obrázek:

Obrázek 1: Výsledek běhu demonstračního příkladu, který vygeneruje rastrový obrázek ve formátu PNG.
""" Zápis rastrového obrázku do formátu PNG.""" # Inspirace: # https://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image import struct import zlib PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n' FILTER_TYPE = b'\x00' def prepare_raw_data(buffer, width, height): """Konverze barev pixelů z bufferu do podoby se specifikací filtru na každém řádku.""" raw_data = bytearray() offset = 0 for _ in range(height): # nastavit filtr + zkopirovat jeden radek (scanline) raw_data += FILTER_TYPE + buffer[offset:offset+width*3] # na dalsi radek ve zdrojovem bufferu offset += width*3 return raw_data def png_chunk(png_tag, chunk_data): """Konstrukce jednoho PNG chunku s tagem i závěrečným kontrolním kódem.""" chunk_header = png_tag + chunk_data return (struct.pack("!I", len(chunk_data)) + chunk_header + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_header))) def write_png(buffer, width, height): """Uložení rastrového obrázku z bufferu do PNG.""" raw_data = prepare_raw_data(buffer, width, height) return b''.join([ PNG_SIGNATURE, png_chunk(b'IHDR', struct.pack("!2I5B", width, height, 8, 2, 0, 0, 0)), png_chunk(b'IDAT', zlib.compress(raw_data, level=9, wbits=15)), png_chunk(b'IEND', b'')]) WIDTH = 256 HEIGHT = 256 # buffer pro rastrová data pixels = bytearray(WIDTH*HEIGHT*3) # vybarvení testovacího obrázku index = 0 for i in range(HEIGHT): for j in range(WIDTH): pixels[index] = 0xff index+=1 pixels[index] = i index+=1 pixels[index] = j index+=1 data = write_png(pixels, WIDTH, HEIGHT) with open("test.png", 'wb') as fout: fout.write(data)
19. Repositář s demonstračními příklady
Všechny demonstrační příklady využívající knihovnu PyTorch lze nalézt v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:
20. Odkazy na Internetu
- Python standard types: bytes
https://docs.python.org/3.8/library/stdtypes.html#bytes - Python standard types: bytearray
https://docs.python.org/3.8/library/stdtypes.html#bytearray-objects - Bytes and Bytearray Operations
https://docs.python.org/3.8/library/stdtypes.html#bytes-methods - Standard encodings
https://docs.python.org/3.8/library/codecs.html#standard-encodings - class memoryview
https://docs.python.org/3.8/library/stdtypes.html#memoryview - struct – Interpret bytes as packed binary data
https://docs.python.org/3/library/struct.html - C-like structures in Python
https://stackoverflow.com/questions/35988/c-like-structures-in-python - python3: bytes vs bytearray, and converting to and from strings
https://stackoverflow.com/questions/62903377/python3-bytes-vs-bytearray-and-converting-to-and-from-strings - Základní informace o MessagePacku
https://msgpack.org/ - Balíček msgpack na PyPi
https://pypi.org/project/msgpack/ - MessagePack na Wikipedii
https://en.wikipedia.org/wiki/MessagePack - Comparison of data-serialization formats (Wikipedia)
https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats - Repositáře msgpacku
https://github.com/msgpack - Specifikace ukládání různých typů dat
https://github.com/msgpack/msgpack/blob/master/spec.md - Podpora MessagePacku v různých programovacích jazycích
https://msgpack.org/#languages - Základní implementace formátu msgpack pro programovací jazyk Go
https://github.com/msgpack/msgpack-go - go-codec
https://github.com/ugorji/go - Gobs of data (odlišný serializační formát)
https://blog.golang.org/gobs-of-data - Formát BSON (odlišný serializační formát)
http://bsonspec.org/ - Problematika nulových hodnot v Go, aneb proč nil != nil
https://www.root.cz/clanky/problematika-nulovych-hodnot-v-go-aneb-proc-nil-nil/ - IEEE-754 Floating Point Converter
https://www.h-schmidt.net/FloatConverter/IEEE754.html - Base Convert: IEEE 754 Floating Point
https://baseconvert.com/ieee-754-floating-point - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/ - Marshalling (computer science)
https://en.wikipedia.org/wiki/Marshalling_(computer_science) - Protocol Buffers
https://protobuf.dev/ - Protocol Buffers
https://en.wikipedia.org/wiki/Protocol_Buffers - What is the difference between Serialization and Marshaling?
https://stackoverflow.com/questions/770474/what-is-the-difference-between-serialization-and-marshaling - Comparison of data-serialization formats
https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats - PNG (Portable Network Graphics) Specification, Version 1.2
http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html