Opérateurs binaires en Python – Real Python

By | décembre 9, 2020

python pour débutant

Les ordinateurs stockent toutes sortes d'informations sous forme de flux de chiffres binaires appelés morceaux. Que vous travailliez avec du texte, des images ou des vidéos, ils se résument tous à des uns et des zéros. Python opérateurs au niveau du bit vous permettent de manipuler ces bits individuels de données au niveau le plus granulaire.

Vous pouvez utiliser des opérateurs au niveau du bit pour implémenter des algorithmes tels que la compression, le cryptage et la détection d'erreurs, ainsi que pour contrôler les périphériques physiques dans votre projet Raspberry Pi ou ailleurs. Souvent, Python vous isole des bits sous-jacents avec des abstractions de haut niveau. Vous êtes plus susceptible de trouver les saveurs surchargées des opérateurs bit à bit dans la pratique. Mais lorsque vous travaillez avec eux dans leur forme originale, vous serez surpris par leurs bizarreries!

Dans ce didacticiel, vous apprendrez à:

  • Utilisez Python opérateurs au niveau du bit pour manipuler des bits individuels
  • Lire et écrire des données binaires dans un indépendant de la plateforme façon
  • Utilisation masques de bit pour regrouper les informations sur un seul octet
  • Surcharge Opérateurs bit à bit Python dans les types de données personnalisés
  • Cacher messages secrets en images numériques

Pour obtenir le code source complet de l'exemple de filigrane numérique et pour extraire une friandise secrète cachée dans une image, cliquez sur le lien ci-dessous:

Présentation des opérateurs binaires de Python

Python est livré avec différents types d'opérateurs, tels que les opérateurs arithmétiques, logiques et de comparaison. Vous pouvez les considérer comme des fonctions qui tirent parti d'un plus compact préfixe et infixe syntaxe.

Les opérateurs au niveau du bit ont pratiquement la même apparence dans différents langages de programmation:

Opérateur Exemple Sens
& un B ET au niveau du bit
| a | b OU au niveau du bit
^ a ^ b XOR au niveau du bit (OU exclusif)
~ ~ un Pas au niveau du bit
<< a << n Décalage gauche au niveau du bit
>> a >> n Décalage à droite au niveau du bit

Comme vous pouvez le voir, ils sont indiqués par des symboles étranges au lieu de mots. Cela les fait ressortir en Python comme étant légèrement moins verbeux que ce que vous pourriez avoir l'habitude de voir. Vous ne seriez probablement pas en mesure de comprendre leur signification simplement en les regardant.

La plupart des opérateurs binaires sont binaire, ce qui signifie qu'ils s'attendent à ce que deux opérandes fonctionnent avec, généralement appelés opérande gauche et le opérande droit. PAS au niveau du bit (~) est le seul unaire opérateur au niveau du bit car il n'attend qu'un seul opérande.

Tous les opérateurs binaires au niveau du bit ont un correspondant opérateur composé qui effectue une affectation augmentée:

Opérateur Exemple Équivalent à
& = a & = b a = a & b
| = a | = b a = a | b
^ = a ^ = b a = a ^ b
<< = a << = n a = a << n
>> = a >> = n a = a >> n

Ce sont des notations abrégées pour mettre à jour l'opérande gauche en place.

C'est tout ce qu'il y a dans la syntaxe d'opérateur binaire de Python! Vous êtes maintenant prêt à examiner de plus près chacun des opérateurs pour comprendre où ils sont les plus utiles et comment vous pouvez les utiliser. Tout d'abord, vous obtiendrez un rappel rapide sur le système binaire avant d'examiner deux catégories d'opérateurs bit à bit: le bitwise logique opérateurs et le bitwise décalage les opérateurs.

Système binaire en cinq minutes

Avant de poursuivre, prenez un moment pour rafraîchir vos connaissances sur le système binaire, ce qui est essentiel pour comprendre les opérateurs au niveau du bit. Si vous êtes déjà à l'aise avec cela, allez-y et passez à la section Opérateurs logiques au niveau du bit ci-dessous.

Pourquoi utiliser binaire?

Il existe un nombre infini de façons de représenter les nombres. Depuis l'Antiquité, les gens ont développé différentes notations, telles que les chiffres romains et les hiéroglyphes égyptiens. La plupart des civilisations modernes utilisent la notation positionnelle, qui est efficace, flexible et bien adaptée pour faire de l'arithmétique.

Une caractéristique notable de tout système positionnel est sa base, qui représente le nombre de chiffres disponibles. Les gens préfèrent naturellement le base dix système numérique, également connu sous le nom de système décimal, car il joue bien avec le fait de compter sur les doigts.

Les ordinateurs, en revanche, traitent les données comme un ensemble de nombres exprimés base deux système numérique, plus communément appelé binaire système. Ces nombres sont composés de seulement deux chiffres, zéro et un.

Par exemple, le nombre binaire 100111002 équivaut à 156dix dans le système base dix. Comme il y a dix chiffres dans le système décimal, de zéro à neuf, il faut généralement moins de chiffres pour écrire le même nombre en base dix qu'en base deux.

Le système binaire nécessite plus d'espace de stockage que le système décimal mais est beaucoup moins compliqué à implémenter dans le matériel. Bien que vous ayez besoin de plus de blocs de construction, ils sont plus faciles à fabriquer et il y en a moins de types. C'est comme décomposer votre code en éléments plus modulaires et réutilisables.

Plus important encore, le système binaire est parfait pour les appareils électroniques, qui traduisent les chiffres en différents niveaux de tension. Parce que la tension aime dériver de haut en bas en raison de divers types de bruit, vous voulez garder une distance suffisante entre les tensions consécutives. Sinon, le signal pourrait être déformé.

En utilisant seulement deux états, vous rendez le système plus fiable et plus résistant au bruit. Alternativement, vous pouvez augmenter la tension, mais cela augmenterait également le consommation d'énergie, ce que vous voulez absolument éviter.

Comment fonctionne le binaire?

Imaginez un instant que vous n'aviez que deux doigts sur lesquels compter. Vous pouvez compter un zéro, un un et deux. Mais lorsque vous êtes à court de doigts, vous devez noter le nombre de fois que vous avez déjà compté jusqu'à deux, puis recommencer jusqu'à ce que vous ayez à nouveau atteint deux:

Décimal Les doigts Huit Quatre Deux Les Binaire
0dix 0 0 0 0 02
1dix ☝️ 0 0 0 1 12
2dix ✌️ 0 0 1 0 dix2
3dix ✌️ + ☝️ 0 0 1 1 112
4dix ✌️✌️ 0 1 0 0 1002
5dix ✌️✌️ + ☝️ 0 1 0 1 1012
6dix ✌️✌️ + ✌️ 0 1 1 0 1102
septdix ✌️✌️ + ✌️ + ☝️ 0 1 1 1 1112
8dix ✌️✌️✌️✌️ 1 0 0 0 10002
9dix ✌️✌️✌️✌️ + ☝️ 1 0 0 1 10012
dixdix ✌️✌️✌️✌️ + ✌️ 1 0 1 0 10102
11dix ✌️✌️✌️✌️ + ✌️ + ☝️ 1 0 1 1 10112
12dix ✌️✌️✌️✌️ + ✌️✌️ 1 1 0 0 11002
13dix ✌️✌️✌️✌️ + ✌️✌️ + ☝️ 1 1 0 1 11012

Chaque fois que vous notez une autre paire de doigts, vous devez également les regrouper par puissances de deux, ce qui est la base du système. Par exemple, pour compter jusqu'à treize, vous devrez utiliser vos deux doigts six fois, puis utiliser un autre doigt. Vos doigts pourraient être disposés en un huit, un quatre, et une un.

Ces puissances de deux correspondent à des positions de chiffres dans un nombre binaire et vous indiquent exactement les bits à activer. Ils poussent de droite à gauche, en commençant par bit le moins significatif, qui détermine si le nombre est pair ou impair.

La notation positionnelle est comme l'odomètre de votre voiture: une fois qu'un chiffre dans une position particulière atteint sa valeur maximale, qui est un dans le système binaire, il roule à zéro et celui se reporte vers la gauche. Cela peut avoir un effet en cascade s'il y en a déjà à gauche du chiffre.

Comment les ordinateurs utilisent le binaire

Maintenant que vous connaissez les principes de base du système binaire et Pourquoi les ordinateurs l'utilisent, vous êtes prêt à apprendre Comment ils représentent des données avec lui.

Avant qu'une information puisse être reproduite sous forme numérique, vous devez la décomposer en nombres, puis les convertir en système binaire. Par exemple, le texte brut peut être considéré comme une chaîne de caractères. Vous pouvez attribuer un nombre arbitraire à chaque caractère ou choisir un encodage de caractères existant tel que ASCII, ISO-8859-1 ou UTF-8.

En Python, les chaînes sont représentées comme des tableaux de points de code Unicode. Pour révéler leurs valeurs ordinales, appelez ord () sur chacun des personnages:

>>>

>>> [[[[ord(personnage) pour personnage dans "€ uro"]
[8364, 117, 114, 111]

Les nombres résultants identifient de manière unique les caractères de texte dans l'espace Unicode, mais ils sont affichés sous forme décimale. Vous souhaitez les réécrire à l'aide de chiffres binaires:

Personnage Point de code décimal Point de code binaire
8364dix 100000101011002
u 117dix 11101012
r 114dix 11100102
o 111dix 11011112

Remarquerez que longueur en bits, qui est le nombre de chiffres binaires, varie considérablement selon les caractères. Le signe de l'euro () nécessite quatorze bits, tandis que le reste des caractères peut tenir confortablement sur sept bits.

Les longueurs de bits variables sont problématiques. Si vous deviez mettre ces nombres binaires les uns à côté des autres sur un disque optique, par exemple, vous vous retrouveriez avec un long flux de bits sans limites claires entre les caractères:

100000101011001110101111001011011112

Une façon de savoir comment interpréter ces informations est de désigner des modèles de bits de longueur fixe pour tous les caractères. Dans l'informatique moderne, la plus petite unité d'information, appelée octuor ou un octet, comprend huit bits pouvant stocker 256 valeurs distinctes.

Vous pouvez remplir vos points de code binaire avec des zéros non significatifs pour les exprimer en octets:

Personnage Point de code décimal Point de code binaire
8364dix 00100000 101011002
u 117dix 00000000 011101012
r 114dix 00000000 011100102
o 111dix 00000000 011011112

Maintenant, chaque caractère prend deux octets, ou 16 bits. Au total, votre texte d'origine a presque doublé de taille, mais au moins il est codé de manière fiable.

Vous pouvez utiliser le codage Huffman pour trouver des modèles de bits sans ambiguïté pour chaque caractère dans un texte particulier ou utiliser un codage de caractères plus approprié. Par exemple, pour gagner de la place, UTF-8 privilégie intentionnellement les lettres latines par rapport aux symboles que vous êtes moins susceptible de trouver dans un texte anglais:

>>>

>>> len("€ uro".encoder("utf-8"))
6

Encodé selon la norme UTF-8, le texte entier prend six octets. Étant donné que UTF-8 est un sur-ensemble d'ASCII, les lettres u, r, et o occupent un octet chacun, alors que le symbole de l'euro prend trois octets dans ce codage:

>>>

>>> pour carboniser dans "€ uro":
...     impression(carboniser, len(carboniser.encoder("utf-8")))
...
3 €
u 1
r 1
o 1

D'autres types d'informations peuvent être numérisés de la même manière que du texte. Les images matricielles sont constituées de pixels, chaque pixel ayant des canaux qui représentent les intensités de couleur sous forme de nombres. Les formes d'ondes sonores contiennent des nombres correspondant à la pression de l'air à un intervalle d'échantillonnage donné. Les modèles tridimensionnels sont construits à partir de formes géométriques définies par leurs sommets, etc.

À la fin de la journée, tout est un nombre.

Opérateurs logiques au niveau du bit

Vous pouvez utiliser des opérateurs au niveau du bit pour exécuter une logique booléenne sur des bits individuels. Cela revient à utiliser des opérateurs logiques tels que et, ou, et ne pas, mais à un niveau un peu. Les similitudes entre les opérateurs binaires et logiques vont au-delà de cela.

Il est possible d’évaluer les expressions booléennes avec des opérateurs binaires au lieu d’opérateurs logiques, mais une telle surutilisation est généralement déconseillée. Si vous êtes intéressé par les détails, vous pouvez développer la zone ci-dessous pour en savoir plus.

La manière ordinaire de spécifier des expressions booléennes composées en Python est d'utiliser les opérateurs logiques qui connectent les prédicats adjacents, comme ceci:

si âge > = 18 et ne pas is_self_excluded:
    impression("Vous pouvez jouer")

Ici, vous vérifiez si l'utilisateur a au moins dix-huit ans et s'il n'a pas choisi de ne pas jouer. Vous pouvez réécrire cette condition à l'aide d'opérateurs au niveau du bit:

si âge > = 18 & ~is_self_excluded:
    impression("Vous pouvez jouer")

Bien que cette expression soit syntaxiquement correcte, elle présente quelques problèmes. Premièrement, il est sans doute moins lisible. Deuxièmement, cela ne fonctionne pas comme prévu pour tous les groupes de données. Vous pouvez le démontrer en choisissant des valeurs d'opérande spécifiques:

>>>

>>> âge = 18
>>> is_self_excluded = Vrai
>>> âge > = 18 & ~is_self_excluded  # Opérateurs logiques au niveau du bit
Vrai
>>> âge > = 18 et ne pas is_self_excluded  # Opérateurs logiques
Faux

L'expression composée des opérateurs au niveau du bit est évaluée à Vrai, tandis que la même expression construite à partir des opérateurs logiques s'évalue à Faux. En effet, les opérateurs au niveau du bit ont priorité sur les opérateurs de comparaison, ce qui modifie la façon dont toute l'expression est interprétée:

>>>

>>> âge > = (18 & ~is_self_excluded)
Vrai

C'est comme si quelqu'un mettait des parenthèses implicites autour des mauvais opérandes. Pour résoudre ce problème, vous pouvez mettre des parenthèses explicites, ce qui imposera le bon ordre d'évaluation:

>>>

>>> (âge > = 18) & ~is_self_excluded
0

Cependant, vous n'obtenez plus de résultat booléen. Les opérateurs binaires Python ont été conçus principalement pour fonctionner avec des entiers, de sorte que leurs opérandes sont automatiquement convertis si nécessaire. Cela n'est cependant pas toujours possible.

Alors que vous pouvez utiliser vérité et faux entiers dans un contexte booléen, c'est un antipattern connu qui peut vous coûter de longues heures de débogage inutile. Vous feriez mieux de suivre le zen de Python pour vous éviter les ennuis.

Enfin, vous souhaiterez peut-être délibérément utiliser des opérateurs au niveau du bit pour désactiver évaluation des courts-circuits des expressions booléennes. Les expressions utilisant des opérateurs logiques sont évaluées paresseusement de gauche à droite. En d'autres termes, l'évaluation s'arrête dès que le résultat de l'expression entière est connu:

>>>

>>> def appel(X):
...     impression(F"appel(X=) ")
...     revenir X
...
>>> appel(Faux) ou appel(Vrai)  # Les deux opérandes évalués
appel (x = Faux)
appel (x = Vrai)
Vrai
>>> appel(Vrai) ou appel(Faux)  # Seul l'opérande de gauche évalué
appel (x = Vrai)
Vrai

Dans le deuxième exemple, l'opérande de droite n'est pas du tout appelé car la valeur de l'expression entière a déjà été déterminée par la valeur de l'opérande de gauche. Quel que soit le bon opérande, il n’affectera pas le résultat, il est donc inutile de l’appeler à moins que vous ne vous fiez aux effets secondaires.

Il existe des expressions idiomatiques, comme le retour à une valeur par défaut, qui tirent parti de cette particularité:

>>>

>>> (1 + 1) ou "défaut"  # L'opérande de gauche est vrai
2
>>> (1 - 1) ou "défaut"  # L'opérande de gauche est faux
'défaut'

Une expression booléenne prend la valeur du dernier opérande évalué. L'opérande devient véridique ou faux dans l'expression mais conserve son type et sa valeur d'origine par la suite. En particulier, un entier positif à gauche se propage, tandis qu'un zéro est rejeté.

Contrairement à leurs homologues logiques, les opérateurs binaires sont évalués avec empressement:

>>>

>>> appel(Vrai) | appel(Faux)
appel (x = Vrai)
appel (x = Faux)
Vrai

Même si la connaissance de l'opérande gauche est suffisante pour déterminer la valeur de l'expression entière, tous les opérandes sont toujours évalués sans condition.

Sauf si vous avez une bonne raison et que vous savez ce que vous faites, vous ne devez utiliser des opérateurs de bits que pour contrôler les bits. Sinon, il est trop facile de se tromper. Dans la plupart des cas, vous souhaiterez transmettre des entiers comme arguments aux opérateurs au niveau du bit.

ET au niveau du bit

L'opérateur AND au niveau du bit (&) effectue une conjonction logique sur les bits correspondants de ses opérandes. Pour chaque paire de bits occupant la même position dans les deux nombres, elle n'en renvoie un que lorsque les deux bits sont activés:

Animation illustrant l'opérateur AND au niveau du bit

Le motif binaire résultant est un intersection des arguments de l’opérateur. Il a deux bits activés dans les positions où les deux opérandes sont des uns. Dans tous les autres endroits, au moins une des entrées a un bit nul.

Arithmétiquement, cela équivaut à un produit de deux valeurs binaires. Vous pouvez calculer le ET au niveau du bit des nombres une et b en multipliant leurs bits à chaque index je:

La formule arithmétique de l'opérateur binaire AND

Voici un exemple concret:

Expression Valeur binaire Valeur décimale
une 100111002 156dix
b 1101002 52dix
un B 101002 20dix

Un un multiplié par un donne un, mais tout ce qui est multiplié par zéro donnera toujours zéro. Vous pouvez également prendre le minimum des deux bits de chaque paire. Notez que lorsque les opérandes ont des longueurs de bits inégales, le plus court est automatiquement complété par des zéros à gauche.

OU au niveau du bit

L'opérateur OR au niveau du bit (|) effectue une disjonction logique. Pour chaque paire de bits correspondante, il renvoie un un si au moins l'un d'entre eux est activé:

Animation illustrant l'opérateur OR au niveau du bit

Le motif binaire résultant est un syndicat des arguments de l’opérateur. Il a cinq bits activés là où l'un des opérandes en a un. Seule une combinaison de deux zéros donne un zéro dans la sortie finale.

L'arithmétique derrière elle est une combinaison d'un somme et un produit des valeurs de bit. Pour calculer le OU au niveau du bit des nombres une et b, vous devez appliquer la formule suivante à leurs bits à chaque index je:

La formule arithmétique de l'opérateur OR au niveau du bit

Voici un exemple concret:

Expression Valeur binaire Valeur décimale
une 100111002 156dix
b 1101002 52dix
a | b 101111002 188dix

C'est presque comme une somme de deux bits, mais serrée à l'extrémité supérieure afin qu'elle ne dépasse jamais la valeur de un. Vous pouvez également prendre le maximum des deux bits de chaque paire pour obtenir le même résultat.

XOR au niveau du bit

Contrairement aux bits AND, OR et NOT, l'opérateur XOR bit à bit (^) n'a pas d'équivalent logique en Python. Cependant, vous pouvez le simuler en construisant au-dessus des opérateurs existants:

def xor(une, b):
    revenir (une et ne pas b) ou (ne pas une et b)

Il évalue deux conditions mutuellement exclusives et vous indique si exactement l'une d'elles est remplie. Par exemple, une personne peut être soit un mineur soit un adulte, mais pas les deux à la fois. Inversement, il n’est pas possible qu’une personne ne soit ni mineure ni adulte. Le choix est obligatoire.

Le nom XOR signifie «exclusif ou» car il effectue une disjonction exclusive sur les paires de bits. En d'autres termes, chaque paire de bits doit contenir des valeurs de bits opposées pour en produire un:

Animation représentant l'opérateur XOR au niveau du bit

Visuellement, c'est un différence symétrique des arguments de l’opérateur. Il y a trois bits activés dans le résultat où les deux nombres ont des valeurs de bit différentes. Les bits dans les positions restantes s'annulent car ils sont identiques.

De la même manière que l'opérateur OR au niveau du bit, l'arithmétique de XOR implique une somme. Cependant, alors que le OU au niveau du bit limite les valeurs à un, l'opérateur XOR les enveloppe avec un somme modulo deux:

La formule arithmétique de l'opérateur XOR bit à bit

Modulo est une fonction de deux nombres – le dividende et le diviseur—Qui effectue une division et renvoie son reste. En Python, il existe un opérateur modulo intégré indiqué par le signe pour cent (%).

Encore une fois, vous pouvez confirmer la formule en regardant un exemple:

Expression Valeur binaire Valeur décimale
une 100111002 156dix
b 1101002 52dix
a ^ b 101010002 168dix

La somme de deux zéros ou de deux uns donne un nombre entier lorsqu'elle est divisée par deux, donc le résultat a un reste de zéro. Cependant, lorsque vous divisez la somme de deux différent valeurs de bits par deux, vous obtenez une fraction avec un reste de un. Une formule plus simple pour l'opérateur XOR est la différence entre le maximum et le minimum des deux bits dans chaque paire.

Pas au niveau du bit

Le dernier des opérateurs logiques au niveau du bit est l'opérateur NOT au niveau du bit (~), qui n'attend qu'un seul argument, ce qui en fait le seul opérateur unaire au niveau du bit. Il effectue une négation logique sur un nombre donné en retournant tous ses bits:

Animation illustrant l'opérateur NOT au niveau du bit

Les bits inversés sont un complément à un, qui transforme les zéros en uns et les uns en zéros. Il peut être exprimé arithmétiquement comme le soustraction des valeurs de bit individuelles à partir d'un:

La formule arithmétique de l'opérateur NOT au niveau du bit

Voici un exemple montrant l'un des nombres utilisés auparavant:

Expression Valeur binaire Valeur décimale
une 100111002 156dix
~ un 11000112 99dix

Bien que l'opérateur NOT au niveau du bit semble être le plus simple de tous, vous devez faire preuve d'une extrême prudence lorsque vous l'utilisez en Python. Tout ce que vous avez lu jusqu'à présent est basé sur l'hypothèse que les nombres sont représentés par non signé entiers.

Bien qu'il existe des moyens de simuler des entiers non signés, Python ne les prend pas en charge de manière native. Cela signifie que tous les nombres sont associés à un signe implicite, que vous en spécifiiez un ou non. Cela s'affiche lorsque vous effectuez un PAS au niveau du bit de n'importe quel nombre:

Au lieu des 99 attendusdix, vous obtenez une valeur négative! La raison en deviendra claire une fois que vous en saurez plus sur les différentes représentations de nombres binaires. Pour l'instant, la solution rapide consiste à tirer parti de l'opérateur AND au niveau du bit:

C'est un exemple parfait de masque binaire, que vous explorerez dans l'une des sections à venir.

Opérateurs de décalage au niveau du bit

Les opérateurs de décalage de bits sont un autre type d'outil pour la manipulation de bits. Ils vous permettent de déplacer les bits, ce qui sera pratique pour créer des bitmasks plus tard. Dans le passé, ils étaient souvent utilisés pour améliorer la vitesse de certaines opérations mathématiques.

Décalage à gauche

L'opérateur de décalage gauche au niveau du bit (<<) déplace les bits de son premier opérande vers la gauche du nombre de places spécifié dans son deuxième opérande. Il prend également soin d'insérer suffisamment de bits de zéro pour combler le vide qui apparaît sur le bord droit du nouveau motif de bits:

Animation représentant l'opérateur de décalage gauche

Le déplacement d'un seul bit vers la gauche d'une place double sa valeur. Par exemple, au lieu d'un deux, le bit indiquera un quatre après le décalage. Le déplacer de deux endroits vers la gauche quadruplera la valeur résultante. Lorsque vous additionnez tous les bits d'un nombre donné, vous remarquerez qu'il est également doublé à chaque place décalée:

Expression Valeur binaire Valeur décimale
une 1001112 39dix
a << 1 10011102 78dix
a << 2 100111002 156dix
a << 3 1001110002 312dix

En général, décaler les bits vers la gauche correspond à multiplier le nombre par un puissance de deux, avec un exposant égal au nombre de places décalées:

La formule arithmétique de l'opérateur de décalage gauche

Le décalage à gauche était un optimisation technique car le décalage de bits est une instruction unique et est moins coûteux à calculer que l'exposant ou le produit. Aujourd'hui, cependant, les compilateurs et les interpréteurs, y compris ceux de Python, sont tout à fait capables d'optimiser votre code en arrière-plan.

Sur le papier, le motif de bits résultant d'un décalage vers la gauche s'allonge d'autant d'endroits que vous le déplacez. Cela est également vrai pour Python en général en raison de la façon dont il gère les entiers. Cependant, dans la plupart des cas pratiques, vous souhaiterez contraindre la longueur d'un motif binaire à être un multiple de huit, qui est la longueur d'octet standard.

Par exemple, si vous travaillez avec un seul octet, le décaler vers la gauche devrait supprimer tous les bits qui dépassent sa limite gauche:

Animation représentant l'opérateur de décalage gauche avec un masque de bits

C'est un peu comme regarder un flux illimité de bits à travers une fenêtre de longueur fixe. Il existe quelques astuces qui vous permettent de faire cela en Python. Par exemple, vous pouvez appliquer un masque de bits avec l'opérateur AND au niveau du bit:

>>>

>>> 39 << 3
312
>>> (39 << 3) & 255
56

Changement de vitesse 39dix de trois emplacements vers la gauche renvoie un nombre supérieur à la valeur maximale que vous pouvez stocker sur un seul octet. Il prend neuf bits, alors qu'un octet n'en a que huit. Pour couper ce bit supplémentaire sur la gauche, vous pouvez appliquer un masque binaire avec la valeur appropriée. Si vous souhaitez conserver plus ou moins de bits, vous devrez modifier la valeur du masque en conséquence.

Shift vers la droite

L'opérateur de décalage droit au niveau du bit (>>) est analogue à celui de gauche, mais au lieu de déplacer les bits vers la gauche, il les pousse vers la droite du nombre de places spécifié. Les bits les plus à droite sont toujours supprimés:

Animation illustrant le bon opérateur de décalage

Chaque fois que vous décalez un peu vers la droite d'une position, vous divisez par deux sa valeur sous-jacente. Déplacer le même bit de deux endroits vers la droite produit un quart de la valeur d'origine, et ainsi de suite. Lorsque vous additionnez tous les bits individuels, vous verrez que la même règle s'applique au nombre qu'ils représentent:

Expression Valeur binaire Valeur décimale
une 100111012 157dix
a >> 1 10011102 78dix
a >> 2 1001112 39dix
a >> 3 100112 19dix

Réduire de moitié un nombre impair tel que 157dix produirait une fraction. Pour s'en débarrasser, le bon opérateur de quart fixe automatiquement le résultat. C’est pratiquement la même chose qu’un division de plancher par une puissance de deux:

La formule arithmétique de l'opérateur de décalage droit

Là encore, l'exposant correspond au nombre de places décalées vers la droite. En Python, vous pouvez utiliser un opérateur dédié pour effectuer une division d'étage:

>>>

>>> 5 >> 1  # Décalage à droite au niveau du bit
2
>>> 5 // 2  # Division de plancher (division entière)
2
>>> 5 / 2   # Division en virgule flottante
2,5

L'opérateur de décalage à droite au niveau du bit et l'opérateur de division d'étage fonctionnent tous deux de la même manière, même pour les nombres négatifs. Cependant, la division par étage vous permet de choisir n'importe quel diviseur et pas seulement une puissance de deux. L'utilisation du décalage vers la droite au niveau du bit était un moyen courant d'améliorer les performances de certaines divisions arithmétiques.

Tout comme avec l'opérateur de décalage gauche, le motif de bits change de taille après un décalage vers la droite. Bien que le déplacement des bits vers la droite raccourcisse la séquence binaire, cela n’a généralement pas d’importance car vous pouvez placer autant de zéros devant une séquence de bits que vous le souhaitez sans changer la valeur. Par exemple, 1012 est le même que 01012, tout comme 000001012, à condition que vous ayez affaire à des nombres non négatifs.

Parfois, vous souhaiterez conserver une longueur en bits donnée après avoir effectué un décalage vers la droite pour l'aligner sur une autre valeur ou pour l'intégrer quelque part. Vous pouvez le faire en appliquant un masque de bits:

Animation représentant l'opérateur de décalage droit avec un masque de bits

Il ne découpe que les bits qui vous intéressent et remplit le motif de bits avec des zéros non significatifs si nécessaire.

La gestion des nombres négatifs en Python est légèrement différente de l'approche traditionnelle du décalage au niveau du bit. Dans la section suivante, vous examinerez cela plus en détail.

Arithmétique vs décalage logique

Vous pouvez classer davantage les opérateurs de décalage au niveau du bit comme arithmétique et logique opérateurs de quart. Bien que Python ne vous permette que d'effectuer le décalage arithmétique, il est intéressant de savoir comment d'autres langages de programmation implémentent les opérateurs de décalage au niveau du bit pour éviter toute confusion et surprises.

Cette distinction vient de la façon dont ils gèrent le bit de signe, qui se trouve généralement à l'extrémité gauche d'une séquence binaire signée. En pratique, il n'est pertinent que pour l'opérateur de décalage droit, ce qui peut provoquer le retournement d'un nombre par son signe, entraînant un débordement d'entier.

De manière classique, un bit de signe activé indique des nombres négatifs, ce qui permet de conserver les propriétés arithmétiques d'une séquence binaire:

Valeur décimale Valeur binaire signée Bit de signe Signe Sens
-100dix 100111002 1 Nombre négatif
28dix 000111002 0 + Nombre positif ou zéro

Looking from the left at these two binary sequences, you can see that their first bit carries the sign information, while the remaining part consists of the magnitude bits, which are the same for both numbers.

A logical right shift, also known as an unsigned right shift or a zero-fill right shift, moves the entire binary sequence, including the sign bit, and fills the resulting gap on the left with zeros:

Animation depicting the logical right shift operator

Notice how the information about the sign of the number is lost. Regardless of the original sign, it’ll always produce a nonnegative integer because the sign bit gets replaced by zero. As long as you aren’t interested in the numeric values, a logical right shift can be useful in processing low-level binary data.

However, because signed binary numbers are typically stored on a fixed-length bit sequence in most languages, it can make the result wrap around the extreme values. You can see this in an interactive Java Shell tool:

jshell> -100 >>> 1
$1 ==> 2147483598

The resulting number changes its sign from negative to positive, but it also overflows, ending up very close to Java’s maximum integer:

jshell> Integer.MAX_VALUE
$2 ==> 2147483647

This number may seem arbitrary at first glance, but it’s directly related to the number of bits that Java allocates for the Integer data type:

jshell> Integer.toBinaryString(-100)
$3 ==> "11111111111111111111111110011100"

It uses 32 bits to store signed integers in two’s complement representation. When you take the sign bit out, you’re left with 31 bits, whose maximum decimal value is equal to 231 – 1, or 2147483647dix.

Python, on the other hand, stores integers as if there were an infinite number of bits at your disposal. Consequently, a logical right shift operator wouldn’t be well defined in pure Python, so it’s missing from the language. You can still simulate it, though.

One way of doing so is to take advantage of the unsigned data types available in C that are exposed through the built-in ctypes module:

>>>

>>> de ctypes import c_uint32 as unsigned_int32
>>> unsigned_int32(-100).value >> 1
2147483598

They let you pass in a negative number but don’t attach any special meaning to the sign bit. It’s treated like the rest of the magnitude bits.

While there are only a few predefined unsigned integer types in C, which differ in bit-length, you can create a custom function in Python to handle arbitrary bit-lengths:

>>>

>>> def logical_rshift(signed_integer, places, num_bits=32):
...     unsigned_integer = signed_integer % (1 << num_bits)
...     revenir unsigned_integer >> places
...
>>> logical_rshift(-100, 1)
2147483598

This converts a signed bit sequence to an unsigned one and then performs the regular arithmetic right shift.

However, since bit sequences in Python aren’t fixed in length, they don’t really have a sign bit. Moreover, they don’t use the traditional two’s complement representation like in C or Java. To mitigate that, you can take advantage of the modulo operation, which will keep the original bit patterns for positive integers while appropriately wrapping around the negative ones.

An arithmetic right shift (>>), sometimes called the signed right shift operator, maintains the sign of a number by replicating its sign bit before moving bits to the right:

Animation depicting the arithmetic right shift operator

In other words, it fills the gap on the left with whatever the sign bit was. Combined with the two’s complement representation of signed binary, this results in an arithmetically correct value. Regardless of whether the number is positive or negative, an arithmetic right shift is equivalent to floor division.

As you’re about to find out, Python doesn’t always store integers in plain two’s complement binary. Instead, it follows a custom adaptive strategy that works like sign-magnitude with an unlimited number of bits. It converts numbers back and forth between their internal representation and two’s complement to mimic the standard behavior of the arithmetic shift.

Binary Number Representations

You’ve experienced firsthand the lack of unsigned data types in Python when using the bitwise negation (~) and the right shift operator (>>). You’ve seen hints about the unusual approach to storing integers in Python, which makes handling negative numbers tricky. To use bitwise operators effectively, you need to know about the various representations of numbers in binary.

Unsigned Integers

In programming languages like C, you choose whether to use the signed or unsigned flavor of a given numeric type. Unsigned data types are more suitable when you know for sure that you’ll never need to deal with negative numbers. By allocating that one extra bit, which would otherwise serve as a sign bit, you practically double the range of available values.

It also makes things a little safer by increasing the maximum limit before an overflow happens. However, overflows happen only with fixed bit-lengths, so they’re irrelevant to Python, which doesn’t have such constraints.

The quickest way to get a taste of the unsigned numeric types in Python is to use the previously mentioned ctypes module:

>>>

>>> de ctypes import c_uint8 as unsigned_byte
>>> unsigned_byte(-42).value
214

Since there’s no sign bit in such integers, all their bits represent the magnitude of a number. Passing a negative number forces Python to reinterpret the bit pattern as if it had only the magnitude bits.

Signed Integers

The sign of a number has only two states. If you ignore zero for a moment, then it can be either positive or negative, which translates nicely to the binary system. Yet there are a few alternative ways to represent signed integers in binary, each with its own pros and cons.

Probably the most straightforward one is the sign-magnitude, which builds naturally on top of unsigned integers. When a binary sequence is interpreted as sign-magnitude, the most significant bit plays the role of a sign bit, while the rest of the bits work the same as usual:

Binary Sequence Sign-Magnitude Value Unsigned Value
001010102 42dix 42dix
101010102 -42dix 170dix

A zero on the leftmost bit indicates a positive (+) number, and a one indicates a negative (-) number. Notice that a sign bit doesn’t contribute to the number’s absolute value in sign-magnitude representation. It’s there only to let you flip the sign of the remaining bits.

Why the leftmost bit?

It keeps bit indexing intact, which, in turn, helps maintain backward compatibility of the bit weights used to calculate the decimal value of a binary sequence. However, not everything about sign-magnitude is so great.

The range of values that you can store in a sign-magnitude bit pattern is symmetrical. But it also means that you end up with two ways to convey zero:

Binary Sequence Sign-Magnitude Value Unsigned Value
000000002 +0dix 0dix
100000002 -0dix 128dix

Zero doesn’t technically have a sign, but there’s no way not to include one in sign-magnitude. While having an ambiguous zero isn’t ideal in most cases, it’s not the worst part of the story. The biggest downside of this method is cumbersome binary arithmetic.

When you apply standard binary arithmetic to numbers stored in sign-magnitude, it may not give you the expected results. For example, adding two numbers with the same magnitude but opposite signs won’t make them cancel out:

Expression Binary Sequence Sign-Magnitude Value
a 001010102 42dix
b 101010102 -42dix
a + b 110101002 -84dix

The sum of 42 and -42 doesn’t produce zero. Also, the carryover bit can sometimes propagate from magnitude to the sign bit, inverting the sign and yielding an unexpected result.

To address these problems, some of the early computers employed one’s complement representation. The idea was to change how decimal numbers are mapped to particular binary sequences so that they can be added up correctly. For a deeper dive into one’s complement, you can expand the section below.

In one’s complement, positive numbers are the same as in sign-magnitude, but negative numbers are obtained by flipping the positive number’s bits using a bitwise NOT:

Positive Sequence Negative Sequence Magnitude Value
000000002 111111112 ±0dix
000000012 111111102 ±1dix
000000102 111111012 ±2dix
011111112 100000002 ±127dix

This preserves the original meaning of the sign bit, so positive numbers still begin with a binary zero, while negative ones start with a binary one. Likewise, the range of values remains symmetrical and continues to have two ways to represent zero. However, the binary sequences of negative numbers in one’s complement are arranged in reverse order as compared to sign-magnitude:

One’s Complement Sign-Magnitude Decimal Value
111111112 100000002 -0dix
111111102 100000012 -1dix
111111012 100000102 -2dix
100000102 111111012 -125dix
100000012 111111102 -126dix
100000002 111111112 -127dix

Thanks to that, you can now add two numbers more reliably because the sign bit doesn’t need special treatment. If a carryover originates from the sign bit, it’s fed back at the right edge of the binary sequence instead of just being dropped. This ensures the correct result.

Nevertheless, modern computers don’t use one’s complement to represent integers because there’s an even better way called two’s complement. By applying a small modification, you can eliminate double zero and simplify the binary arithmetic in one go. To explore two’s complement in more detail, you can expand the section below.

When finding bit sequences of negative values in two’s complement, the trick is to add one to the result after negating the bits:

Positive Sequence One’s Complement (NOT) Two’s Complement (NOT+1)
000000002 111111112 000000002
000000012 111111102 111111112
000000102 111111012 111111102
011111112 100000002 100000012

This pushes the bit sequences of negative numbers down by one place, eliminating the notorious minus zero. A more useful minus one will take over its bit pattern instead.

As a side effect, the range of available values in two’s complement becomes asymmetrical, with a lower bound that’s a power of two and an odd upper bound. For example, an 8-bit signed integer will let you store numbers from -128dix to 127dix in two’s complement:

Two’s Complement One’s Complement Decimal Value
100000002 N / A -128dix
100000012 100000002 -127dix
100000102 100000012 -126dix
111111102 111111012 -2dix
111111112 111111102 -1dix
N / A 111111112 -0dix
000000002 000000002 0dix
000000012 000000012 1dix
000000102 000000102 2dix
011111112 011111112 127dix

Another way to put it is that the most significant bit carries both the sign and part of the number magnitude:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
-2sept 26 25 24 23 22 21 20
-128 64 32 16 8 4 2 1

Notice the minus sign next to the leftmost bit weight. Deriving a decimal value from a binary sequence like that is only a matter of adding appropriate columns. For example, the value of 110101102 in 8-bit two’s complement representation is the same as the sum: -128dix + 64dix + 16dix + 4dix + 2dix = -42dix.

With the two’s complement representation, you no longer need to worry about the carryover bit unless you want to use it as an overflow detection mechanism, which is kind of neat.

There are a few other variants of signed number representations, but they’re not as popular.

Floating-Point Numbers

The IEEE 754 standard defines a binary representation for real numbers consisting of the sign, exponent, and mantissa bits. Without getting into too many technical details, you can think of it as the scientific notation for binary numbers. The decimal point “floats” around to accommodate a varying number of significant figures, except it’s a binary point.

Two data types conforming to that standard are widely supported:

  1. Single precision: 1 sign bit, 8 exponent bits, 23 mantissa bits
  2. Double precision: 1 sign bit, 11 exponent bits, 52 mantissa bits

Python’s float data type is equivalent to the double-precision type. Note that some applications require more or fewer bits. For example, the OpenEXR image format takes advantage of half precision to represent pixels with a high dynamic range of colors at a reasonable file size.

The number Pi (π) has the following binary representation in single precision when rounded to five decimal places:

Signe Exponent Mantissa
02 100000002 .100100100001111110100002

The sign bit works just like with integers, so zero denotes a positive number. For the exponent and mantissa, however, different rules can apply depending on a few edge cases.

First, you need to convert them from binary to the decimal form:

  • Exponent: 128dix
  • Mantissa: 2-1 + 2-4 + … + 2-19 = 299261dix/524288dix ≈ 0.570795dix

The exponent is stored as an unsigned integer, but to account for negative values, it usually has a bias equal to 127dix in single precision. You need to subtract it to recover the actual exponent.

Mantissa bits represent a fraction, so they correspond to negative powers of two. Additionally, you need to add one to the mantissa because it assumes an implicit leading bit before the radix point in this particular case.

Putting it all together, you arrive at the following formula to convert a floating-point binary number into a decimal one:

The Formula for Floating-Point Binary to Decimal Conversion

When you substitute the variables for the actual values in the example above, you’ll be able to decipher the bit pattern of a floating-point number stored in single precision:

The Number Pi in Floating-Point Binary

There it is, granted that Pi has been rounded to five decimal places. You’ll learn how to display such numbers in binary later on.

Fixed-Point Numbers

While floating-point numbers are a good fit for engineering purposes, they fail in monetary calculations due to their limited precision. For example, some numbers with a finite representation in decimal notation have only an infinite representation in binary. That often results in a rounding error, which can accumulate over time:

>>>

>>> 0.1 + 0.2
0.30000000000000004

In such cases, you’re better off using Python’s decimal module, which implements fixed-point arithmetic and lets you specify where to put the decimal point on a given bit-length. For example, you can tell it how many digits you want to preserve:

>>>

>>> de decimal import Decimal, localcontext
>>> avec localcontext() as context:
...     context.prec = 5  # Number of digits
...     impression(Decimal("123.456") * 1)
...
123.46

However, it includes all digits, not just the fractional ones.

If you can’t or don’t want to use a fixed-point data type, a straightforward way to reliably store currency values is to scale the amounts to the smallest unit, such as cents, and represent them with integers.

Integers in Python

In the old days of programming, computer memory was at a premium. Therefore, languages would give you pretty granular control over how many bytes to allocate for your data. Let’s take a quick peek at a few integer types from C as an example:

Type Taille Minimum Value Maximum Value
char 1 byte -128 127
short 2 bytes -32,768 32,767
int 4 bytes -2,147,483,648 2,147,483,647
long 8 bytes -9,223,372,036,854,775,808 9,223,372,036,854,775,807

These values might vary from platform to platform. However, such an abundance of numeric types allows you to arrange data in memory compactly. Remember that these don’t even include unsigned types!

On the other end of the spectrum are languages such as JavaScript, which have just one numeric type to rule them all. While this is less confusing for beginning programmers, it comes at the price of increased memory consumption, reduced processing efficiency, and decreased precision.

When talking about bitwise operators, it’s essential to understand how Python handles integer numbers. After all, you’ll use these operators mainly to work with integers. There are a couple of wildly different representations of integers in Python that depend on their values.

Interned Integers

In CPython, very small integers between -5dix and 256dix are interned in a global cache to gain some performance because numbers in that range are commonly used. In practice, whenever you refer to one of those values, which are singletons created at the interpreter startup, Python will always provide the same instance:

>>>

>>> a = 256
>>> b = 256
>>> a est b
True
>>> impression(id(a), id(b), sep="n")
94050914330336
94050914330336

Both variables have the same identity because they refer to the exact same object in memory. That’s typical of reference types but not immutable values such as integers. However, when you go beyond that range of cached values, Python will start creating distinct copies during variable assignment:

>>>

>>> a = 257
>>> b = 257
>>> a est b
False
>>> impression(id(a), id(b), sep="n")
140282370939376
140282370939120

Despite having equal values, these variables point to separate objects now. But don’t let that fool you. Python will occasionally jump in and optimize your code behind the scenes. For example, it’ll cache a number that occurs on the same line multiple times regardless of its value:

>>>

>>> a = 257
>>> b = 257
>>> impression(id(a), id(b), sep="n")
140258768039856
140258768039728
>>> impression(id(257), id(257), sep="n")
140258768039760
140258768039760

Variables a et b are independent objects because they reside at different memory locations, while the numbers used literally in print() are, in fact, the same object.

Interestingly, there’s a similar string interning mechanism in Python, which kicks in for short texts comprised of ASCII letters only. It helps speed up dictionary lookups by allowing their keys to be compared by memory addresses, or C pointers, instead of by the individual string characters.

Fixed-Precision Integers

Integers that you’re most likely to find in Python will leverage the C signed long data type. They use the classic two’s complement binary representation on a fixed number of bits. The exact bit-length will depend on your hardware platform, operating system, and Python interpreter version.

Modern computers typically use 64-bit architecture, so this would translate to decimal numbers between -263 and 263 – 1. You can check the maximum value of a fixed-precision integer in Python in the following way:

>>>

>>> import sys
>>> sys.maxsize
9223372036854775807

It’s huge! Roughly 9 million times the number of stars in our galaxy, so it should suffice for everyday use. While the maximum value that you could squeeze out of the unsigned long type in C is even bigger, on the order of 1019, integers in Python have no theoretical limit. To allow this, numbers that don’t fit on a fixed-length bit sequence are stored differently in memory.

Arbitrary-Precision Integers

Do you remember that popular K-pop song “Gangnam Style” that became a worldwide hit in 2012? The YouTube video was the first to break a billion views. Soon after that, so many people had watched the video that it made the view counter overflow. YouTube had no choice but to upgrade their counter from 32-bit signed integers to 64-bit ones.

That might give plenty of headroom for a view counter, but there are even bigger numbers that aren’t uncommon in real life, notably in the scientific world. Nonetheless, Python can deal with them effortlessly:

>>>

>>> de math import factorial
>>> factorial(42)
1405006117752879898543142606244511569936384000000000

This number has fifty-two decimal digits. It would take at least 170 bits to represent it in binary with the traditional approach:

>>>

>>> factorial(42).bit_length()
170

Since they’re well over the limits that any of the C types have to offer, such astronomical numbers are converted into a sign-magnitude positional system, whose base is 230. Yes, you read that correctly. Whereas you have ten fingers, Python has over a billion!

Again, this may vary depending on the platform you’re currently using. When in doubt, you can double-check:

>>>

>>> import sys
>>> sys.int_info
sys.int_info(bits_per_digit=30, sizeof_digit=4)

This will tell you how many bits are used per digit and what the size in bytes is of the underlying C structure. To get the same namedtuple in Python 2, you’d refer to the sys.long_info attribute instead.

While this conversion between fixed- and arbitrary-precision integers is done seamlessly under the hood in Python 3, there was a time when things were more explicit. For more information, you can expand the box below.

In the past, Python explicitly defined two distinct integer types:

  1. Plain integer
  2. Long integer

The first one was modeled after the C signed long type, which typically occupied 32 or 64 bits and offered a limited range of values:

>>>

>>> # Python 2
>>> import sys
>>> sys.maxint
9223372036854775807
>>> type(sys.maxint)

For bigger numbers, you were supposed to use the second type that didn’t come with a limit. Python would automatically promote plain integers to long ones if needed:

>>>

>>> # Python 2
>>> import sys
>>> sys.maxint + 1
9223372036854775808L
>>> type(sys.maxint + 1)

This feature prevented the integer overflow error. Notice the letter L at the end of a literal, which could be used to enforce the given type by hand:

>>>

>>> # Python 2
>>> type(42)

>>> type(42L)

Eventually, both types were unified so that you wouldn’t have to think about it anymore.

Such a representation eliminates integer overflow errors and gives the illusion of infinite bit-length, but it requires significantly more memory. Additionally, performing bignum arithmetic is slower than with fixed precision because it can’t run directly in hardware without an intermediate layer of emulation.

Another challenge is keeping a consistent behavior of the bitwise operators across alternative integer types, which is crucial in handling the sign bit. Recall that fixed-precision integers in Python use the standard two’s complement representation from C, while large integers use sign-magnitude.

To mitigate that difference, Python will do the necessary binary conversion for you. It might change how a number is represented before and after applying a bitwise operator. Here’s a relevant comment from the CPython source code, which explains this in more detail:

Bitwise operations for negative numbers operate as though on a two’s complement representation. So convert arguments from sign-magnitude to two’s complement, and convert the result back to sign-magnitude at the end. (Source)

In other words, negative numbers are treated as two’s complement bit sequences when you apply the bitwise operators on them, even though the result will be presented to you in sign-magnitude form. There are ways to emulate the sign bit and some of the unsigned types in Python, though.

Bit Strings in Python

You’re welcome to use pen and paper throughout the rest of this article. It may even serve as a great exercise! However, at some point, you’ll want to verify whether your binary sequences or bit strings correspond to the expected numbers in Python. Here’s how.

Converting int to Binary

To reveal the bits making up an integer number in Python, you can print a formatted string literal, which optionally lets you specify the number of leading zeros to display:

>>>

>>> impression(f"42:b")  # Print 42 in binary
101010
>>> impression(f"42:032b")  # Print 42 in binary on 32 zero-padded digits
00000000000000000000000000101010

Alternatively, you can call bin() with the number as an argument:

>>>

>>> poubelle(42)
'0b101010'

This global built-in function returns a string consisting of a binary literal, which starts with the prefix 0b and is followed by ones and zeros. It always shows the minimum number of digits without the leading zeros.

You can use such literals verbatim in your code, too:

>>>

>>> age = 0b101010
>>> impression(age)
42

Other integer literals available in Python are the hexadecimal et octal ones, which you can obtain with the hex() et oct() functions, respectively:

>>>

>>> hex(42)
'0x2a'
>>> oct(42)
'0o52'

Notice how the hexadecimal system, which is base sixteen, takes advantage of letters UNE par F to augment the set of available digits. The octal literals in other programming languages are usually prefixed with plain zero, which might be confusing. Python explicitly forbids such literals to avoid making a mistake:

>>>

>>> 052
  File "", line 1
SyntaxError: leading zeros in decimal integer literals are not permitted;
use an 0o prefix for octal integers

You can express the same value in different ways using any of the mentioned integer literals:

>>>

>>> 42 == 0b101010 == 0x2a == 0o52
True

Choose the one that makes the most sense in context. For example, it’s customary to express bitmasks with hexadecimal notation. On the other hand, the octal literal is rarely seen these days.

All numeric literals in Python are case insensitive, so you can prefix them with either lowercase or uppercase letters:

>>>

>>> 0b101 == 0B101
True

This also applies to floating-point number literals that use scientific notation as well as complex number literals.

Converting Binary to int

Once you have your bit string ready, you can get its decimal representation by taking advantage of a binary literal:

This is a quick way to do the conversion while working inside the interactive Python interpreter. Unfortunately, it won’t let you convert bit sequences synthesized at runtime because all literals need to be hard-coded in the source code.

Calling int() with two arguments will work better in the case of dynamically generated bit strings:

>>>

>>> int("101010", 2)
42
>>> int("cafe", 16)
51966

The first argument is a string of digits, while the second one determines the base of the numeral system. Unlike a binary literal, a string can come from anywhere, even a user typing on the keyboard. For a deeper look at int(), you can expand the box below.

There are other ways to call int(). For example, it returns zero when called without arguments:

This feature makes it a common pattern in the defaultdict collection, which needs a default value provider. Take this as an example:

>>>

>>> de collections import defaultdict
>>> word_counts = defaultdict(int)
>>> for word dans "A sentence with a message".split():
...     word_counts[[[[word.lower()] += 1
...
>>> dict(word_counts)
'a': 2, 'sentence': 1, 'with': 1, 'message': 1

Ici, int() helps to count words in a sentence. It’s called automatically whenever defaultdict needs to initialize the value of a missing key in the dictionary.

Another popular use of int() is typecasting. For example, when you pass int() a floating-point value, it truncates the value by removing the fractional component:

When you give it a string, it tries to parse out a number from it:

>>>

>>> int(input("Enter your age: "))
Enter your age: 42
42

In general, int() will accept an object of any type as long as it defines a special method that can handle the conversion.

So far, so good. But what about negative numbers?

Emulating the Sign Bit

When you call bin() on a negative integer, it merely prepends the minus sign to the bit string obtained from the corresponding positive value:

>>>

>>> impression(poubelle(-42), poubelle(42), sep="n    ")
-0b101010
    0b101010

Changing the sign of a number doesn’t affect the underlying bit string in Python. Conversely, you’re allowed to prefix a bit string with the minus sign when transforming it to decimal form:

>>>

>>> int("-101010", 2)
-42

That makes sense in Python because, internally, it doesn’t use the sign bit. You can think of the sign of an integer number in Python as a piece of information stored separately from the modulus.

However, there are a few workarounds that let you emulate fixed-length bit sequences containing the sign bit:

  • Bitmask
  • Modulo operation (%)
  • ctypes module
  • array module
  • struct module

You know from earlier sections that to ensure a certain bit-length of a number, you can use a nifty bitmask. For example, to keep one byte, you can use a mask composed of exactly eight turned-on bits:

>>>

>>> mask = 0b11111111  # Same as 0xff or 255
>>> poubelle(-42 & mask)
'0b11010110'

Masking forces Python to temporarily change the number’s representation from sign-magnitude to two’s complement and then back again. If you forget about the decimal value of the resulting binary literal, which is equal to 214dix, then it’ll represent -42dix in two’s complement. The leftmost bit will be the sign bit.

Alternatively, you can take advantage of the modulo operation that you used previously to simulate the logical right shift in Python:

>>>

>>> poubelle(-42 % (1 << 8))  # Give me eight bits
'0b11010110'

If that looks too convoluted for your taste, then you can use one of the modules from the standard library that express the same intent more clearly. For example, using ctypes will have an identical effect:

>>>

>>> de ctypes import c_uint8 as unsigned_byte
>>> poubelle(unsigned_byte(-42).value)
'0b11010110'

You’ve seen it before, but just as a reminder, it’ll piggyback off the unsigned integer types from C.

Another standard module that you can use for this kind of conversion in Python is the array module. It defines a data structure that’s similar to a list but is only allowed to hold elements of the same numeric type. When declaring an array, you need to indicate its type up front with a corresponding letter:

>>>

>>> de array import array
>>> signed = array("b", [[[[-42, 42])
>>> unsigned = array("B")
>>> unsigned.frombytes(signed.tobytes())
>>> unsigned
array('B', [214, 42])
>>> poubelle(unsigned[[[[0])
'0b11010110'
>>> poubelle(unsigned[[[[1])
'0b101010'

Par exemple, "b" stands for an 8-bit signed byte, while "B" stands for its unsigned equivalent. There are a few other predefined types, such as a signed 16-bit integer or a 32-bit floating-point number.

Copying raw bytes between these two arrays changes how bits are interpreted. However, it takes twice the amount of memory, which is quite wasteful. To perform such a bit rewriting in place, you can rely on the struct module, which uses a similar set of format characters for type declarations:

>>>

>>> de struct import pack, unpack
>>> unpack("BB", pack("bb", -42, 42))
(214, 42)
>>> poubelle(214)
'0b11010110'

Packing lets you lay objects in memory according to the given C data type specifiers. It returns a read-only bytes() object, which contains raw bytes of the resulting block of memory. Later, you can read back those bytes using a different set of type codes to change how they’re translated into Python objects.

Up to this point, you’ve used different techniques to obtain fixed-length bit strings of integers expressed in two’s complement representation. If you want to convert these types of bit sequences back to Python integers instead, then you can try this function:

def from_twos_complement(bit_string, num_bits=32):
    unsigned = int(bit_string, 2)
    sign_mask = 1 << (num_bits - 1)  # For example 0b100000000
    bits_mask = sign_mask - 1        # For example 0b011111111
    revenir (unsigned & bits_mask) - (unsigned & sign_mask)

The function accepts a string composed of binary digits. First, it converts the digits to a plain unsigned integer, disregarding the sign bit. Next, it uses two bitmasks to extract the sign and magnitude bits, whose locations depend on the specified bit-length. Finally, it combines them using regular arithmetic, knowing that the value associated with the sign bit is negative.

You can try it out against the trusty old bit string from earlier examples:

>>>

>>> int("11010110", 2)
214
>>> from_twos_complement("11010110")
214
>>> from_twos_complement("11010110", num_bits=8)
-42

Python’s int() treats all the bits as the magnitude, so there are no surprises there. However, this new function assumes a 32-bit long string by default, which means the sign bit is implicitly equal to zero for shorter strings. When you request a bit-length that matches your bit string, then you’ll get the expected result.

While integer is the most appropriate data type for working with bitwise operators in most cases, you’ll sometimes need to extract and manipulate fragments of structured binary data, such as image pixels. le array et struct modules briefly touch upon this topic, so you’ll explore it in more detail next.

Seeing Data in Binary

You know how to read and interpret individual bytes. However, real-world data often consists of more than one byte to convey information. Take the float data type as an example. A single floating-point number in Python occupies as many as eight bytes in memory.

How do you see those bytes?

You can’t simply use bitwise operators because they don’t work with floating-point numbers:

>>>

>>> 3.14 & 0xff
Traceback (most recent call last):
  File "", line 1, dans 
TypeError: unsupported operand type(s) for &: 'float' and 'int'

You have to forget about the particular data type you’re dealing with and think of it in terms of a generic stream of bytes. That way, it won’t matter what the bytes represent outside the context of them being processed by the bitwise operators.

To get the bytes() of a floating-point number in Python, you can pack it using the familiar struct module:

>>>

>>> de struct import pack
>>> pack(">d", 3.14159)
b'@t!xf9xf0x1bx86n'

Ignore the format characters passed through the first argument. They won’t make sense until you get to the byte order section below. Behind this rather obscure textual representation hides a list of eight integers:

>>>

>>> list(b"@t!xf9xf0x1bx86n")
[64, 9, 33, 249, 240, 27, 134, 110]

Their values correspond to the subsequent bytes used to represent a floating-point number in binary. You can combine them to produce a very long bit string:

>>>

>>> de struct import pack
>>> "".joindre([[[[f"b:08b" for b dans pack(">d", 3.14159)])
'0100000000001001001000011111100111110000000110111000011001101110'

These 64 bits are the sign, exponent, and mantissa in double precision that you read about earlier. To synthesize a float from a similar bit string, you can reverse the process:

>>>

>>> de struct import unpack
>>> bits = "0100000000001001001000011111100111110000000110111000011001101110"
>>> unpack(
...   ">d",
...   bytes(int(bits[[[[je:je+8], 2) for je dans range(0, len(bits), 8))
... )
(3.14159,)

unpack() returns a tuple because it allows you to read more than one value at a time. For example, you could read the same bit string as four 16-bit signed integers:

>>>

>>> unpack(
...   ">hhhh",
...   bytes(int(bits[[[[je:je+8], 2) for je dans range(0, len(bits), 8))
... )
(16393, 8697, -4069, -31122)

As you can see, the way a bit string should be interpreted must be known up front to avoid ending up with garbled data. One important question you need to ask yourself is which end of the byte stream you should start reading from—left or right. Read on to find out.

Byte Order

There’s no dispute about the order of bits in a single byte. You’ll always find the least-significant bit at index zero and the most-significant bit at index seven, regardless of how they’re physically laid out in memory. The bitwise shift operators rely on this consistency.

However, there’s no consensus for the byte order in multibyte chunks of data. A piece of information comprising more than one byte can be read from left to right like an English text or from right to left like an Arabic one, for example. Computers see bytes in a binary stream like humans see words in a sentence.

It doesn’t matter which direction computers choose to read the bytes from as long as they apply the same rules everywhere. Unfortunately, different computer architectures use different approaches, which makes transferring data between them challenging.

Big-Endian vs Little-Endian

Let’s take a 32-bit unsigned integer corresponding to the number 1969dix, which was the year when Monty Python first appeared on TV. With all the leading zeros, it has the following binary representation 000000000000000000000111101100012.

How would you store such a value in computer memory?

If you imagine memory as a one-dimensional tape consisting of bytes, then you’d need to break that data down into individual bytes and arrange them in a contiguous block. Some find it natural to start from the left end because that’s how they read, while others prefer starting at the right end:

Byte Order Address N Address N+1 Address N+2 Address N+3
Big-Endian 000000002 000000002 000001112 101100012
Little-Endian 101100012 000001112 000000002 000000002

When bytes are placed from left to right, the most-significant byte is assigned to the lowest memory address. This is known as the big-endian order. Conversely, when bytes are stored from right to left, the least-significant byte comes first. That’s called little-endian order.

Which way is better?

From a practical standpoint, there’s no real advantage of using one over the other. There might be some marginal gains in performance at the hardware level, but you won’t notice them. Major network protocols use the big-endian order, which allows them to filter data packets more quickly given the hierarchical design of IP addressing. Other than that, some people may find it more convenient to work with a particular byte order when debugging.

Either way, if you don’t get it right and mix up the two standards, then bad things start to happen:

>>>

>>> raw_bytes = (1969).to_bytes(length=4, byteorder="big")
>>> int.from_bytes(raw_bytes, byteorder="little")
2970025984
>>> int.from_bytes(raw_bytes, byteorder="big")
1969

When you serialize some value to a stream of bytes using one convention and try reading it back with another, you’ll get a completely useless result. This scenario is most likely when data is sent over a network, but you can also experience it when reading a local file in a specific format. For example, the header of a Windows bitmap always uses little-endian, while JPEG can use both byte orders.

Native Endianness

To find out your platform’s endianness, you can use the sys module:

>>>

>>> import sys
>>> sys.byteorder
'little'

You can’t change endianness, though, because it’s an intrinsic feature of your CPU architecture. It’s impossible to mock it for testing purposes without hardware virtualization such as QEMU, so even the popular VirtualBox won’t help.

Notably, the x86 family of processors from Intel and AMD, which power most modern laptops and desktops, are little-endian. Mobile devices are based on low-energy ARM architecture, which is bi-endian, while some older architectures such as the ancient Motorola 68000 were big-endian only.

For information on determining endianness in C, expand the box below.

Historically, the way to get your machine’s endianness in C was to declare a small integer and then read its first byte with a pointer:

#include 

#define BIG_ENDIAN "big"
#define LITTLE_ENDIAN "little"

char* byteorder() 
    int X = 1;
    char* pointer = (char*) &X; // Address of the 1st byte
    revenir (*pointer > 0) ? LITTLE_ENDIAN : BIG_ENDIAN;


void main() 
    printf("%sn", byteorder());

If the value comes out higher than zero, then the byte stored at the lowest memory address must be the least-significant one.

Once you know the native endianness of your machine, you’ll want to convert between different byte orders when manipulating binary data. A universal way to do so, regardless of the data type at hand, is to reverse a generic bytes() object or a sequence of integers representing those bytes:

>>>

>>> big_endian = b"x00x00x07xb1"
>>> bytes(reversed(big_endian))
b'xb1x07x00x00'

However, it’s often more convenient to use the struct module, which lets you define standard C data types. In addition to this, it allows you to request a given byte order with an optional modifier:

>>>

>>> de struct import pack, unpack
>>> pack(">I", 1969)  # Big-endian unsigned int
b'x00x00x07xb1'
>>> unpack("<I", b"x00x00x07xb1")  # Little-endian unsigned int
(2970025984,)

The greater-than sign (>) indicates that bytes are laid out in the big-endian order, while the less-than symbol (<) corresponds to little-endian. If you don’t specify one, then native endianness is assumed. There are a few more modifiers, like the exclamation mark (!), which signifies the network byte order.

Network Byte Order

Computer networks are made of heterogeneous devices such as laptops, desktops, tablets, smartphones, and even light bulbs equipped with a Wi-Fi adapter. They all need agreed-upon protocols and standards, including the byte order for binary transmission, to communicate effectively.

At the dawn of the Internet, it was decided that the byte order for those network protocols would be big-endian.

Programs that want to communicate over a network can grab the classic C API, which abstracts away the nitty-gritty details with a socket layer. Python wraps that API through the built-in socket module. However, unless you’re writing a custom binary protocol, you’ll probably want to take advantage of an even higher-level abstraction, such as the HTTP protocol, which is text-based.

Where the socket module can be useful is in the byte order conversion. It exposes a few functions from the C API, with their distinctive, tongue-twisting names:

>>>

>>> de socket import htons, htonl, ntohs, ntohl
>>> htons(1969)  # Host to network (short int)
45319
>>> htonl(1969)  # Host to network (long int)
2970025984
>>> ntohs(45319)  # Network to host (short int)
1969
>>> ntohl(2970025984)  # Network to host (long int)
1969

If your host already uses the big-endian byte order, then there’s nothing to be done. The values will remain the same.

Bitmasks

A bitmask works like a graffiti stencil that blocks the paint from being sprayed on particular areas of a surface. It lets you isolate the bits to apply some function on them selectively. Bitmasking involves both the bitwise logical operators and the bitwise shift operators that you’ve read about.

You can find bitmasks in a lot of different contexts. For example, the subnet mask in IP addressing is actually a bitmask that helps you extract the network address. Pixel channels, which correspond to the red, green, and blue colors in the RGB model, can be accessed with a bitmask. You can also use a bitmask to define Boolean flags that you can then pack on a bit field.

There are a few common types of operations associated with bitmasks. You’ll take a quick look at some of them below.

Getting a Bit

To read the value of a particular bit on a given position, you can use the bitwise AND against a bitmask composed of only one bit at the desired index:

>>>

>>> def get_bit(value, bit_index):
...     revenir value & (1 << bit_index)
...
>>> get_bit(0b10000000, bit_index=5)
0
>>> get_bit(0b10100000, bit_index=5)
32

The mask will suppress all bits except for the one that you’re interested in. It’ll result in either zero or a power of two with an exponent equal to the bit index. If you’d like to get a simple yes-or-no answer instead, then you could shift to the right and check the least-significant bit:

>>>

>>> def get_normalized_bit(value, bit_index):
...     revenir (value >> bit_index) & 1
...
>>> get_normalized_bit(0b10000000, bit_index=5)
0
>>> get_normalized_bit(0b10100000, bit_index=5)
1

This time, it will normalize the bit value so that it never exceeds one. You could then use that function to derive a Boolean True ou False value rather than a numeric value.

Setting a Bit

Setting a bit is similar to getting one. You take advantage of the same bitmask as before, but instead of using bitwise AND, you use the bitwise OR operator:

>>>

>>> def set_bit(value, bit_index):
...     revenir value | (1 << bit_index)
...
>>> set_bit(0b10000000, bit_index=5)
160
>>> poubelle(160)
'0b10100000'

The mask retains all the original bits while enforcing a binary one at the specified index. Had that bit already been set, its value wouldn’t have changed.

Unsetting a Bit

To clear a bit, you want to copy all binary digits while enforcing zero at one specific index. You can achieve this effect by using the same bitmask once again, but in the inverted form:

>>>

>>> def clear_bit(value, bit_index):
...     revenir value & ~(1 << bit_index)
...
>>> clear_bit(0b11111111, bit_index=5)
223
>>> poubelle(223)
'0b11011111'

Using the bitwise NOT on a positive number always produces a negative value in Python. While this is generally undesirable, it doesn’t matter here because you immediately apply the bitwise AND operator. This, in turn, triggers the mask’s conversion to two’s complement representation, which gets you the expected result.

Toggling a Bit

Sometimes it’s useful to be able to toggle a bit on and off again periodically. That’s a perfect opportunity for the bitwise XOR operator, which can flip your bit like that:

>>>

>>> def toggle_bit(value, bit_index):
...     revenir value ^ (1 << bit_index)
...
>>> X = 0b10100000
>>> for _ dans range(5):
...     X = toggle_bit(X, bit_index=sept)
...     impression(poubelle(X))
...
0b100000
0b10100000
0b100000
0b10100000
0b100000

Notice the same bitmask being used again. A binary one on the specified position will make the bit at that index invert its value. Having binary zeros on the remaining places will ensure that the rest of the bits will be copied.

Bitwise Operator Overloading

The primary domain of bitwise operators is integer numbers. That’s where they make the most sense. However, you’ve also seen them used in a Boolean context, in which they replaced the logical operators. Python provides alternative implementations for some of its operators and lets you overload them for new data types.

Although the proposal to overload the logical operators in Python was rejected, you can give new meaning to any of the bitwise operators. Many popular libraries, and even the standard library, take advantage of it.

Built-In Data Types

Python bitwise operators are defined for the following built-in data types:

It’s not a widely known fact, but bitwise operators can perform operations from set algebra, such as union, intersection, and symmetric difference, as well as merge and update dictionaries.

When a et b are Python sets, then bitwise operators correspond to the following methods:

Set Method Bitwise Operator
a.union(b) a | b
a.update(b) a |= b
a.intersection(b) a & b
a.intersection_update(b) a &= b
a.symmetric_difference(b) a ^ b
a.symmetric_difference_update(vegies) a ^= b

They do virtually the same thing, so it’s up to you which syntax to use. Apart from that, there’s also an overloaded minus operator (-), which implements a difference of two sets. To see them in action, assume you have the following two sets of fruits and vegetables:

>>>

>>> fruits = "apple", "banana", "tomato"
>>> veggies = "eggplant", "tomato"
>>> fruits | veggies
'tomato', 'apple', 'eggplant', 'banana'
>>> fruits & veggies
'tomato'
>>> fruits ^ veggies
'apple', 'eggplant', 'banana'
>>> fruits - veggies  # Not a bitwise operator!
'apple', 'banana'

They share one common member, which is hard to classify, but the rest of their elements are disjoint.

One thing to watch out for is the immutable frozenset(), which is missing the methods for in-place updates. However, when you use their bitwise operator counterparts, the meaning is slightly different:

>>>

>>> const_fruits = frozenset("apple", "banana", "tomato")
>>> const_veggies = frozenset("eggplant", "tomato")
>>> const_fruits.mise à jour(const_veggies)
Traceback (most recent call last):
  File "", line 1, dans 
    const_fruits.mise à jour(const_veggies)
AttributeError: 'frozenset' object has no attribute 'update'
>>> const_fruits |= const_veggies
>>> const_fruits
frozenset('tomato', 'apple', 'eggplant', 'banana')

It looks like frozenset() isn’t so immutable after all when you use the bitwise operators, but the devil is in the details. Here’s what actually happens:

const_fruits = const_fruits | const_veggies

The reason it works the second time is that you don’t change the original immutable object. Instead, you create a new one and assign it to the same variable again.

Python dict supports only bitwise OR, which works like a union operator. You can use it to update a dictionary in place or merge two dictionaries into a new one:

>>>

>>> fruits = "apples": 2, "bananas": 5, "tomatoes": 0
>>> veggies = "eggplants": 2, "tomatoes": 4
>>> fruits | veggies  # Python 3.9+
'apples': 2, 'bananas': 5, 'tomatoes': 4, 'eggplants': 2
>>> fruits |= veggies  # Python 3.9+, same as fruits.update(veggies)

The augmented version of the bitwise operator is equivalent to .update().

Third-Party Modules

Many popular libraries, including NumPy, pandas, and SQLAlchemy, overload the bitwise operators for their specific data types. This is the most likely place you’ll find bitwise operators in Python because they aren’t used very often in their original meaning anymore.

For example, NumPy applies them to vectorized data in a pointwise fashion:

>>>

>>> import numpy as np
>>> np.array([[[[1, 2, 3]) << 2
array([ 4,  8, 12])

This way, you don’t need to manually apply the same bitwise operator to each element of the array. But you can’t do the same thing with ordinary lists in Python.

pandas uses NumPy behind the scenes, and it also provides overloaded versions of the bitwise operators for its DataFrame et Series objects. However, they behave as you’d expect. The only difference is that they do their usual job on vectors and matrices of numbers instead of on individual scalars.

Things get more interesting with libraries that give the bitwise operators entirely new meanings. For example, SQLAlchemy provides a compact syntax for querying the database:

session.query(Utilisateur) 
       .filter((Utilisateur.age > 40) & (Utilisateur.name == "Doe")) 
       .all()

The bitwise AND operator (&) will eventually translate to a piece of SQL query. However, that’s not very obvious, at least not to my IDE, which complains about the unpythonic use of bitwise operators when it sees them in this type of expression. It immediately suggests replacing every occurrence of & with a logical et, not knowing that doing so would make the code stop working!

This type of operator overloading is a controversial practice that relies on implicit magic you have to know up front. Some programming languages like Java prevent such abuse by disallowing operator overloading altogether. Python is more liberal in that regard and trusts that you know what you’re doing.

Custom Data Types

To customize the behavior of Python’s bitwise operators, you have to define a class and then implement the corresponding magic methods in it. At the same time, you can’t redefine the behavior of the bitwise operators for the existing types. Operator overloading is possible only on new data types.

Here’s a quick rundown of special methods that let you overload the bitwise operators:

Magic Method Expression
.__and__(self, value) instance & value
.__rand__(self, value) value & instance
.__iand__(self, value) instance &= value
.__or__(self, value) instance | value
.__ror__(self, value) value | instance
.__ior__(self, value) instance |= value
.__xor__(self, value) instance ^ value
.__rxor__(self, value) value ^ instance
.__ixor__(self, value) instance ^= value
.__invert__(self) ~instance
.__lshift__(self, value) instance << value
.__rlshift__(self, value) value << instance
.__ilshift__(self, value) instance <<= value
.__rshift__(self, value) instance >> value
.__rrshift__(self, value) value >> instance
.__irshift__(self, value) instance >>= value

You don’t need to define all of them. For example, to have a slightly more convenient syntax for appending and prepending elements to a deque, it’s sufficient to implement only .__lshift__() et .__rrshift__():

>>>

>>> de collections import deque
>>> class DoubleEndedQueue(deque):
...     def __lshift__(self, value):
...         self.append(value)
...     def __rrshift__(self, value):
...         self.appendleft(value)
...
>>> items = DoubleEndedQueue([[[["middle"])
>>> items << "last"
>>> "first" >> items
>>> items
DoubleEndedQueue(['first', 'middle', 'last'])

This user-defined class wraps a deque to reuse its implementation and augment it with two additional methods that allow for adding items to the left or right end of the collection.

Least-Significant Bit Steganography

Whew, that was a lot to process! If you’re still scratching your head, wondering why you’d want to use bitwise operators, then don’t worry. It’s time to showcase what you can do with them in a fun way.

To follow along with the examples in this section, you can download the source code by clicking the link below:

You’re going to learn about steganography and apply this concept to secretly embed arbitrary files in bitmap images.

Cryptography vs Steganography

Cryptography is about changing a message into one that is readable only to those with the right key. Everyone else can still see the encrypted message, but it won’t make any sense to them. One of the first forms of cryptography was the substitution cipher, such as the Caesar cipher named after Julius Caesar.

Steganography is similar to cryptography because it also allows you to share secret messages with your desired audience. However, instead of using encryption, it cleverly hides information in a medium that doesn’t attract attention. Examples include using invisible ink or writing an acrostic in which the first letter of every word or line forms a secret message.

Unless you knew that a secret message was concealed and the method to recover it, you’d probably ignore the carrier. You can combine both techniques to be even safer, hiding an encrypted message rather than the original one.

There are plenty of ways to smuggle secret data in the digital world. In particular, file formats carrying lots of data, such as audio files, videos, or images, are a great fit because they give you a lot of room to work with. Companies that release copyrighted material might use steganography to watermark individual copies and trace the source of a leak, for example.

Below, you’ll inject secret data into a plain bitmap, which is straightforward to read and write in Python without the need for external dependencies.

Bitmap File Format

The word bitmap usually refers to the Windows bitmap (.bmp) file format, which supports a few alternative ways of representing pixels. To make life easier, you’re going to assume that pixels are stored in 24-bit uncompressed RGB (red, green, and blue) format. A pixel will have three color channels that can each hold values from 0dix to 255dix.

Every bitmap begins with a file header, which contains metadata such as the image width and height. Here are a few interesting fields and their positions relative to the start of the header:

Field Byte Offset Bytes Length Type Sample Value
Signature 0x00 2 String BM
File Size 0x02 4 Unsigned int 7,629,186
Reserved #1 0x06 2 Bytes 0
Reserved #2 0x08 2 Bytes 0
Pixels Offset 0x0a 4 Unsigned int 122
Pixels Size 0x22 4 Unsigned int 7,629,064
Image Width 0x12 4 Unsigned int 1,954
Image Height 0x16 4 Unsigned int 1,301
Bits Per Pixel 0x1c 2 Unsigned short 24
Compression 0x1e 4 Unsigned int 0
Colors Palette 0x2e 4 Unsigned int 0

You can infer from this header that the corresponding bitmap is 1,954 pixels wide and 1,301 pixels high. It doesn’t use compression, nor does it have a color palette. Every pixel occupies 24 bits, or 3 bytes, and the raw pixel data starts at offset 122dix.

You can open the bitmap in binary mode, seek the desired offset, read the given number of bytes, and deserialize them using struct like before:

de struct import unpack

avec open("example.bmp", "rb") as file_object:
    file_object.seek(0x22)
    field: bytes = file_object.read(4)
    value: int = unpack("<I", field)[[[[0]

Note that all integer fields in bitmaps are stored in the little-endian byte order.

You might have noticed a small discrepancy between the number of pixel bytes declared in the header and the one that would result from the image size. When you multiply 1,954 pixels × 1,301 pixels × 3 bytes, you get a value that is 2,602 bytes less than 7,629,064.

This is because pixel bytes are padded with zeros so that every row is a multiple of four bytes. If the width of the image times three bytes happens to be a multiple of four, then there’s no need for padding. Otherwise, empty bytes are added at the end of every row.

Bitmaps store pixel rows upside down, starting from the bottom rather than the top. Also, every pixel is serialized to a vector of color channels in a somewhat odd BGR order rather than RGB. However, this is irrelevant to the task of hiding secret data.

Bitwise Hide and Seek

You can use the bitwise operators to spread custom data over consecutive pixel bytes. The idea is to overwrite the least-significant bit in each of them with bits coming from the next secret byte. This will introduce the least amount of noise, but you can experiment with adding more bits to strike a balance between the size of injected data and pixel distortion.

In some cases, the corresponding bits will be the same, resulting in no change in pixel value whatsoever. However, even in the worst-case scenario, a pixel color will differ only by a fraction of a percent. Such a tiny anomaly will remain invisible to the human eye but can be detected with steganalysis, which uses statistics.

Take a look at these cropped images:

Original Bitmap vs Altered Bitmap With Secret Data

The one on the left comes from the original bitmap, while the image on the right depicts a processed bitmap with an embedded video stored on the least-significant bits. Can you spot the difference?

The following piece of code encodes the secret data onto the bitmap:

for secret_byte, eight_bytes dans zip(file.secret_bytes, bitmap.byte_slices):
    secret_bits = [([([([(secret_byte >> je) & 1 for je dans reversed(range(8))]
    bitmap[[[[eight_bytes] = bytes(
        [[[[
            byte | 1 if bit else byte & ~1
            for byte, bit dans zip(bitmap[[[[eight_bytes], secret_bits)
        ]
    )

For every byte of secret data and the corresponding eight bytes of pixel data, excluding the pad bytes, it prepares a list of bits to be spread over. Next, it overwrites the least-significant bit in each of the eight bytes using a relevant bitmask. The result is converted to a bytes() object and assigned back to the part of the bitmap that it originally came from.

To decode a file from the same bitmap, you need to know how many secret bytes were written to it. You could allocate a few bytes at the beginning of the data stream to store this number, or you could use the reserved fields from the bitmap header:

@reserved_field.setter
def reserved_field(self, value: int) -> Aucun:
    """Store a little-endian 32-bit unsigned integer."""
    self._file_bytes.seek(0x06)
    self._file_bytes.write(pack("<I", value))

This jumps to the right offset in the file, serializes the Python int to raw bytes, and writes them down.

You might also want to store the name of your secret file. Since it can have an arbitrary length, it makes sense to serialize it using a null-terminated string, which would precede the file contents. To create such a string, you need to encode a Python str object to bytes and manually append the null byte at the end:

>>>

>>> de pathlib import Path
>>> path = Path("/home/jsmith/café.pdf")
>>> path.name.encode("utf-8") + b"x00"
b'cafxc3xa9.pdfx00'

Also, it doesn’t hurt to drop the redundant parent directory from the path using pathlib.

The sample code supplementing this article will let you encode, decode, et erase a secret file from the given bitmap with the following commands:

$ python -m stegano example.bmp -d
Extracted a secret file: podcast.mp4
$ python -m stegano example.bmp -x
Erased a secret file from the bitmap
$ python -m stegano example.bmp -e pdcast.mp4
Secret file was embedded in the bitmap

This is a runnable module that can be executed by calling its encompassing directory. You could also make a portable ZIP-format archive out of its contents to take advantage of the Python ZIP application support.

This program relies on modules from the standard library mentioned in the article and a few others that you might not have heard about before. A critical module is mmap, which exposes a Python interface to memory-mapped files. They let you manipulate huge files using both the standard file API and the sequence API. It’s as if the file were one big mutable list that you could slice.

Go ahead and play around with the bitmap attached to the supporting materials. It contains a little surprise for you!

Conclusion

Mastering Python bitwise operators gives you the ultimate freedom to manipulate binary data in your projects. You now know their syntax and different flavors as well as the data types that support them. You can also customize their behavior for your own needs.

In this tutorial, you learned how to:

  • Use Python bitwise operators to manipulate individual bits
  • Read and write binary data in a platform-agnostic façon
  • Use bitmasks to pack information on a single byte
  • Overload Python bitwise operators in custom data types
  • Hide secret messages in digital images

You also learned how computers use the binary system to represent different kinds of digital information. You saw several popular ways to interpret bits and how to mitigate the lack of unsigned data types in Python as well as Python’s unique way of storing integer numbers in memory.

With this information, you’re ready to make full use of binary data in your code. To download the source code used in the watermarking example and continue experimenting with bitwise operators, you can click the link below: