Битовые операторы в Python

Битовые операторы в Python

  • Апрель 17, 2020, 10:46 д.п.

Что такое бинарные (или битовые) операции и как из можно использовать в Python?


В фреймворке PyQt (и PySide тоже) часто встречается настройка чего-либо с помощью так называемых флагов.

widget.setWindowFlags(Qt.Window)

Взаимодействие нескольких флагов делается с помощью бинарных (или побитовых) операторов.
Несколько флагов можно указать с помощью оператора "|"

list_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

исключить флаг из уже имеющегося набора можно так

list_item.setFlags(list_item.flags() ^ Qt.ItemIsEnabled)

Добавить новый флаг к имеющимся можно так

list_item.setFlags(list_item.flags() | Qt.ItemIsEnabled)

А проверка наличия делается так

is_enabled = item.flags() & Qt.ItemIsEnabled > 0

Почему именно так? Всё дело в том как именно работают побитовые операторы. Всего есть 6 основных операторов:

|  OR

&  AND

^  XOR (исключающее OR)

~  NOT (унарная операция)

>> сдвиг вправо

<< сдвиг влево

Эти операторы работают с числами в двоичном представлении. Условно говоря, они ставят числа в двоичном виде друг над другом и по очереди обрабатывают каждый столбик с битами.
Например, берём два числа, и сразу смотрим как оно выглядит в двоичном виде (Python отбрасывает ведущие нули, так что рядом допишу более удобную форму)

>>> a = 3
>>> bin(a)
'0b11'  # 011
>>> b = 6
>>> bin(b)
'0b110'  # 110

Оператор OR

В результат пишет 1 если в одном из элементов есть 1

>>> 3|6
7

в двоичном виде это выглядит так (запишем столбиком)

_⁣011
|110
=111

В каждом столбце был найден 1, поэтому в результате все биты равны 1

Оператор AND

В результат ставит 1 только если оба бита равны 1

>>> 3&6 
2

Бинарный вид

_011
&110
=010

Только на 2й позиции оба бита равны 1.

Оператор XOR

Пишет 1 на бит результата, для которого только один из соответствующих битов операндов равен 1.

>>> 3^6
5

_011
&110
=101

Оператор NOT

Заменяет каждый бит на противоположный. Эта операция унарная, то есть поддерживает только один операнд.

>>> ~3
-4

~011
=100

Здесь всё понятно. Но давайте попробуем другое число:

~50
=-51


~110010
=-110011

Вот тут не очень понятно что произошло) Это связано со способом представления отрицательных чисел в двоичном виде. Ведь мы не можем в память записать отрицательные биты. Для этого используется ведущий 0 или 1.
Но это тема не поместится в пост, советую поискать информацию в интернете самостоятельно). Если кратко и из документации, то:Побитовая операция НЕ для числа x соответствует -(x+1)

Сдвиг

Здесь всё просто. Все биты сдвигаются на указанное количество шагов подставляя нули

>>> 3 << 1
6

011 << 1
110

Кстати, преобразовать бинарное представление обратно в число можно с помощью функции int() указав разрядность системы исчисления.

>>> int('11001', 2)
25

Лично я на практике встречал использование побитовых операторов в двух ситуациях (их конечно намного больше).

1. Сдвиг, который соответствует некоторой математической операции (арифметический сдвиг) но работает несравнимо быстрей. Например сдвиг влево равен выражению a*2**b

a<<b == a*2**b

А также определение знака числа или нахождение модуля без условного оператора, что также очень быстро делается. И другие операции.

2. Числовые маски.

Что это такое?

Создаем несколько переменных, в которых в бинарном представлении все ячейки заполнены нулями кроме одной позиции. И у каждой переменной используется своя уникальная позиция для бита 1.

FLAG1 = int('001', 2) # 2
FLAG2 = int('010', 2) # 4
FLAG3 = int('100', 2) # 8

Теперь с помощью оператора OR можем объединять все биты в одну маску

>>> flags = FLAG1 | FLAG2
3 # 011

А после проверить, входит ли определённый флаг в состав битов маски?

>>> flags & FLAG3
0  # 000 нет совпадений
>>> flags & FLAG2
2  # 010 совпал второй бит

Если результат больше 0 то флаг присутствует в маске. Если результат 0 то такого флага нет.

Чтобы получить тип bool можем писать так

bool(flags & FLAG2)

или так

flags & FLAG2 > 0

и, очевидно, так

if flags & FLAG1:
    ...

Где могут пригодиться такие маски? Один пример был про флаги в Qt фреймворке. Также такой способ часто используют в организации прав доступа к ресурсам.

READ = int('001', 2) # 2
WRITE = int('010', 2) # 4
DELETE = int('100', 2) # 8
USER = READ
MODERATOR = READ | WRITE
ADMIN = READ | WRITE | DELETE

can_write = ADMIN & WRITE

Не сложно представить альтернативу на простом Python

READ = 1
WRITE = 2
DELETE = 3
USER = [READ]
MODERATOR = [READ, WRITE]
ADMIN = [READ, WRITE, DELETE]

can_write = WRITE in ADMIN

Оператор in работает довольно шустро, но всё равно медленней чем побитовый оператор.