На рівні апаратного забезпечення транзистори можуть бути лише увімкненими або вимкненими: два стани, які ми традиційно позначаємо як 1 і 0.
Це binary digits, скорочено bits.
Розуміння bits і binary особливо важливе для системних програмістів, які працюють із низькорівневими мовами.
Однак протягом більшої частини історії обчислювальної техніки пріоритетом програмування був пошук дедалі складніших способів абстрагуватися від цієї двійкової реальності.
У Python (і багатьох інших високорівневих мовах програмування) ми працюємо з int, float, string та іншими визначеними типами, аж до аудіо- та відеоформатів.
Ми дозволяємо внутрішнім механізмам Python (зрештою) подбати про те, щоб перетворити все на біти.
Тим не менш, використання побітових операторів і побітових операцій іноді може дати значні переваги у швидкості та ефективності використання памʼяті, навіть у такій високорівневій мові, як Python.
Як не дивно, Python взаємодіє з користувачем за допомогою десяткових чисел, але програміст може змінити цю типову поведінку.
Насправді Python охоче приймає int у binary, hexadecimal або octal форматі й так само охоче виконує математичні операції між ними.
Щоб дізнатися більше, можна переглянути концепцію binary-octal-hexadecimal.
Двійкові числа вводяться з префіксом 0b, так само як 0x можна використати для шістнадцяткових чисел (шістнадцяткові числа - стислий спосіб подати групи з 4 бітів), а oct для вісімкових чисел.
Існує кілька способів перетворити цілі числа на двійкові рядки тексту (англ. string), які різняться тим, чи містять вони префікс 0b і чи підтримують доповнення нулями зліва:
# Binary entry.
>>> 0b10111
23
# Converting an int display to binary string, with prefix.
>>> bin(23)
'0b10111'
>>> number = 23
# Binary without prefix, padded to 8 digits.
>>> format(number, '08b')
'00010111'
# Same format, but using an f-string.
>>> f"{number} in decimal is {number:08b} in binary and {number:x} in hex"
'23 in decimal is 00010111 in binary and 17 in hex'
Bitwise LogicУ концепції bools ми обговорювали логічні оператори and, or і not, які використовують із булевими значеннями (англ. Boolean), такими як True і False.
Ті самі правила логіки діють і під час роботи з бітами.
Однак побітові відповідники логічних операторів & (and), | (or), ~ (not) і ^ (XOR) застосовуються до кожного біта у двійковому поданні, трактуючи 1 як True («увімкнено»), а 0 як False («вимкнено»).
Приклад із побітовим & допоможе це зрозуміти:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
Лише ті позиції, у яких 1 стоїть в обох вхідних числах, мають 1 у вихідних даних.
Побітовий & часто використовують, щоб виділити окремі біти в упакованому наборі значень True/False, як-от налаштування застосунку, які може змінювати користувач.
Це дає змогу керувати логікою програми за допомогою значень окремих бітів:
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
Для побітового | (or) у вихідних даних встановлюється 1, якщо 1 є в хоч одному з входів:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
Оператор ^ відповідає побітовому виключному або (xor): 1 встановлюється, якщо він є в одному з двох входів, але не в обох.
Цей символ може здатися знайомим із концепції sets, де його використовують для симетричної різниці set, яка збігається з XOR, застосованим до множин.
Якщо xor ^ здається дивним, знайте: це найпоширеніша операція в криптографії.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
Нарешті, є оператор ~ (символ тильда), тобто побітове not, яке приймає один вхід і інвертує всі біти, а результат може виявитися не таким, як ми очікували!
Кожен 1 у поданні змінюється на 0, і навпаки.
Докладніше про це йдеться в розділі нижче.
У десятковому поданні ми розрізняємо додатні та відʼємні числа за допомогою знака + або - ліворуч від цифр.
Використання цих символів на двійковому рівні виявилося неефективним для цифрових обчислень і породило проблему: +0 не те саме, що -0.
Замість використання - і + всі сучасні компʼютери застосовують для відʼємних чисел подання two's complement, аж до рівня кремнієвих чипів.
Це означає, що всі біти інвертуються, а число інтерпретується як відʼємне, якщо найлівіший біт (його ще називають «старшим бітом», або MSB) дорівнює 1.
У додатних чисел старший біт дорівнює 0.
Це подання має ту перевагу, що нуль існує лише в одній версії, тож програмісту не доводиться мати справу з -0 і +0.
Такий спосіб подання відʼємних і додатних чисел додає складнощів для Python: у самій мові, в її ядрі, немає понять цілих чисел фіксованого розміру, як-от int32 чи int64.
У «сучасному» Python int мають необмежений розмір (обмежений лише можливостями апаратного забезпечення), і відʼємне чи побітово інвертоване число має (теоретично) нескінченну кількість 1 ліворуч, так само як додатне число має необмежену кількість 0.
Через це складно навести змістовний приклад bitwise not:
>>> x = 0b01100110
>>> format(x, '08b')
'01100110'
# This is a negative binary (not twos-complement display).
>>> format(~x, '08b')
'-1100111'
# Decimal representation.
>>> x
102
# Using the Bitwise not, with an unintuitive result.
>>> ~x
-103
Це не той 0b10011001, який ми побачили б у мовах із цілими числами фіксованого розміру.
Оператор ~ працює як очікується лише з беззнаковими байтовими чи цілочисельними типами або з типами цілих чисел фіксованого розміру.
Ці числові типи підтримуються в сторонніх пакетах, як-от NumPy, pandas і sympy, але не в ядрі Python.
На практиці програмісти Python досить часто використовують &, |, ^ і оператори зсуву, описані нижче, лише з додатними числами.
Побітові операції з відʼємними числами трапляються набагато рідше.
Один прийом - додати 2**32 (or 1 << 32) до відʼємного значення, щоб зробити int беззнаковим, але цим стає важко керувати.
Інша стратегія - працювати з модулем ctypes і використовувати цілочисельні типи в стилі C, але це так само незручно.
Shift operatorsОператор зсуву вліво x << y переміщує всі біти в x на y позицій уліво, заповнюючи нові проміжки нулями.
Зауважмо, що арифметично це те саме, що помножити число на (2**y).
Оператор зсуву вправо x >> y робить протилежне.
Арифметично це те саме, що цілочисельне ділення x // (2**y).
Памʼятаймо про попередній розділ, присвячений відʼємним числам, і про пастки, які чатують на них під час зсуву в Python.
>>> x = 8
>>> format(x, '08b')
'00001000'
# A left bit shift.
>>> x << 2
32
>>> format(x << 2, '08b')
'00100000'
# A right bit shift.
>>> format(x >> 2, '08b')
'00000010'