Tout en bas, au niveau matériel, un transistor ne peut être qu'allumé ou éteint : deux états que l'on représente traditionnellement par 1 et 0.
Ce sont les binary digits, abrégés en bits.
Bien connaître les bits et le binary est particulièrement important pour les programmeurs système qui travaillent dans des langages de bas niveau.
Cependant, pendant la majeure partie de l'histoire de l'informatique, la priorité des programmeurs a été de trouver des moyens de plus en plus sophistiqués pour faire abstraction de cette réalité binaire.
En Python (et dans beaucoup d'autres langages de programmation de haut niveau), on travaille avec int, float, string et d'autres types définis, jusqu'aux formats audio et vidéo inclus.
On laisse les rouages internes de Python se charger de tout traduire (au bout du compte) en bits.
Néanmoins, utiliser les opérateurs bit à bit et les opérations bit à bit peut parfois offrir des avantages considérables en termes de vitesse et d'efficacité mémoire, même dans un langage de haut niveau comme Python.
Sans surprise, Python communique avec l'utilisateur en nombres décimaux, mais un programmeur peut passer outre ce comportement par défaut.
En fait, Python accepte sans problème un int au format binary, hexadecimal ou octal, et effectue volontiers des opérations mathématiques entre eux.
Pour plus de détails, tu peux revoir le concept binary-octal-hexadecimal.
Les nombres binaires s'écrivent avec le préfixe 0b, tout comme on peut utiliser 0x pour l'hexadécimal (les nombres hexadécimaux sont un moyen concis de représenter des groupes de 4 bits), et oct pour les nombres octaux.
Il existe plusieurs façons de convertir des entiers en chaînes binaires, qui diffèrent selon qu'elles incluent le préfixe 0b et selon qu'elles permettent un remplissage à gauche par des zéros :
# 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 LogicDans le concept bools, on a parlé des opérateurs logiques and, or et not utilisés avec des valeurs booléennes (True et False).
Les mêmes règles de logique s'appliquent quand on travaille avec des bits.
Cependant, les équivalents bit à bit des opérateurs logiques & (and), | (or), ~ (not) et ^ (XOR) s'appliquent à chaque bit d'une représentation binaire, en traitant 1 comme True (« allumé ») et 0 comme False (« éteint »).
Un exemple avec l'opérateur & bit à bit devrait rendre cela plus clair :
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
Seules les positions qui contiennent un 1 dans les deux nombres d'entrée valent 1 dans le résultat.
L'opérateur & bit à bit est couramment utilisé pour isoler des bits individuels dans un ensemble compact de valeurs True/False, comme les paramètres configurables par l'utilisateur dans une application.
Cela permet à la valeur de bits individuels de contrôler la logique du programme :
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
Avec un | bit à bit (ou), un 1 apparaît dans le résultat s'il y a un 1 dans au moins une des entrées :
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
Avec l'opérateur ^ pour le ou exclusif (xor), un 1 est placé s'il apparaît dans l'une ou l'autre des entrées mais pas dans les deux à la fois.
Tu as peut-être déjà vu ce symbole dans le concept sets, où il sert à calculer la différence symétrique des set, ce qui est identique au xor appliqué aux ensembles.
Si le xor ^ te paraît étrange, sache que c'est de loin l'opération la plus courante en cryptographie.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
Enfin, il y a l'opérateur ~ (le caractère tilde), qui est un not bit à bit : il prend une seule entrée et inverse tous les bits, ce qui n'est peut-être pas le résultat que tu attendais !
Chaque 1 de la représentation devient 0, et inversement.
Voir la section ci-dessous pour plus de détails.
En représentation décimale, on distingue les nombres positifs et négatifs grâce à un signe + ou - placé à gauche des chiffres.
Utiliser ces symboles au niveau binaire s'est révélé peu efficace pour le calcul numérique et posait le problème que +0 n'est pas identique à -0.
Plutôt que d'utiliser - et +, tous les ordinateurs modernes représentent les nombres négatifs en two's complement, jusque dans les puces de silicium.
Cela signifie que tous les bits sont inversés et qu'un nombre est interprété comme négatif si le bit le plus à gauche (aussi appelé « bit de poids fort », ou MSB) vaut 1.
Les nombres positifs ont un MSB de 0.
Cette représentation a l'avantage de n'avoir qu'une seule version de zéro, ce qui évite au programmeur de gérer -0 et +0.
Cette façon de représenter les nombres négatifs et positifs ajoute une complication pour Python : le cœur du langage ne connaît pas de types entiers de taille finie comme int32 ou int64.
Dans le Python « moderne », les int ont une taille illimitée (limitée uniquement par la capacité du matériel), et un nombre négatif ou inversé bit à bit possède un nombre (théoriquement) infini de 1 à sa gauche, tout comme un nombre positif possède un nombre illimité de 0.
Il est donc difficile de donner un exemple utile de 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
Ce n'est pas le 0b10011001 que l'on verrait dans des langages à entiers de taille fixe.
L'opérateur ~ ne fonctionne comme prévu qu'avec des types entiers ou des octets non signés, ou avec des types entiers de taille fixe.
Ces types numériques sont pris en charge par des paquets tiers comme NumPy, pandas et sympy, mais pas par le cœur de Python.
En pratique, les programmeurs Python utilisent assez souvent &, |, ^ et les opérateurs de décalage décrits plus bas uniquement avec des nombres positifs.
Les opérations bit à bit sur des nombres négatifs sont bien plus rares.
Une technique consiste à ajouter 2**32 (or 1 << 32) à une valeur négative pour rendre un int non signé, mais cela devient difficile à gérer.
Une autre stratégie consiste à travailler avec le module ctypes et à utiliser des types entiers de style C, mais c'est tout aussi peu pratique.
Shift operatorsL'opérateur de décalage à gauche x << y décale tous les bits de x de y positions vers la gauche, en remplissant les nouveaux espaces avec des zéros.
Note que cela revient, arithmétiquement, à multiplier un nombre par (2**y).
L'opérateur de décalage à droite x >> y fait l'inverse.
Cela revient, arithmétiquement, à la division entière x // (2**y).
Garde en tête la section précédente sur les nombres négatifs et leurs pièges lorsqu'on les décale en 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'