Python 中的按位运算符2 |【生长吧!Python!】

举报
Yuchuan 发表于 2021/06/24 18:52:35 2021/06/24
【摘要】 您还学习了计算机如何使用二进制系统来表示不同种类的数字信息。您看到了几种流行的解释位的方法,以及如何缓解Python 中缺少无符号数据类型的问题,以及 Python 在内存中存储整数的独特方式。

Python 中的位串

欢迎您在本文的其余部分使用笔和纸。它甚至可以作为一个很好的锻炼!但是,在某些时候,您需要验证您的二进制序列或位字符串是否与 Python 中的预期数字相对应。就是这样。

转换int为二进制

要在 Python 中显示组成整数的位,您可以打印格式化的字符串文字,它可以让您选择指定要显示的前导零的数量:

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

或者,您可以bin()使用号码作为参数调用:

>>>
>>> bin(42)
'0b101010'

这个全局内置函数返回一个由二进制文字组成的字符串,它以前缀开头,0b后跟一和零。它始终显示不带前导零的最小位数。

您也可以在代码中逐字使用此类文字:

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

Python 中可用的其他整数文字是十六进制八进制文字,您可以分别使用hex()oct()函数获取它们:

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

请注意十六进制系统如何利用字母A直通F来扩充可用数字集。其他编程语言中的八进制文字通常以纯零作为前缀,这可能会造成混淆。Python 明确禁止此类文字以避免出错:

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

您可以使用任何提到的整数文字以不同的方式表达相同的值:

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

选择在上下文中最有意义的一项。例如,习惯上用十六进制表示法来表示位掩码。另一方面,现在很少看到八进制文字。

Python 中的所有数字文字都不区分大小写,因此您可以使用小写或大写字母作为前缀:

>>>
>>> 0b101 == 0B101
True

这也适用于使用科学记数法的浮点数文字以及复数文字。

将二进制转换为 int

准备好位字符串后,您可以通过利用二进制文字来获取其十进制表示:

>>>
>>> 0b101010
42

这是在交互式 Python 解释器中工作时进行转换的一种快速方法。不幸的是,它不会让您转换在运行时合成的位序列,因为所有文字都需要在源代码中进行硬编码。

注意:您可能会想用 来评估Python 代码eval("0b101010"),但这是一种危及程序安全的简单方法,所以不要这样做!

int()在动态生成的位串的情况下,使用两个参数调用会更好:

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

第一个参数是一串数字,而第二个参数确定数字系统的基数。与二进制文字不同,字符串可以来自任何地方,甚至是用户在键盘上打字。要更深入地了解int(),您可以展开下面的框。

的其他用途 int()显示隐藏

到现在为止还挺好。但是负数呢?

模拟符号位

当你调用bin()一个负整数时,它只是在从相应的正值获得的位串前面加上减号:

>>>
>>> print(bin(-42), bin(42), sep="\n ")
-0b101010
 0b101010

更改数字的符号不会影响 Python 中的底层位串。相反,在将位串转换为十进制形式时,允许在位串前加上减号:

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

这在 Python 中是有意义的,因为在内部,它不使用符号位。您可以将 Python 中整数的符号视为与模数分开存储的一条信息。

但是,有一些变通方法可以让您模拟包含符号位的固定长度位序列:

  • 位掩码
  • 模运算 ( %)
  • ctypes 模块
  • array 模块
  • struct 模块

您从前面的部分知道,为了确保数字的某个位长度,您可以使用漂亮的位掩码。例如,要保留一个字节,您可以使用恰好由八个开启位组成的掩码:

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

掩码强制 Python 暂时将数字的表示从符号幅度更改为二进制补码,然后再返回。如果您忘记了结果二进制文字的十进制值(等于 214 10 ),那么它将以二进制补码形式表示 -42 10。最左边的位将是符号位。

或者,您可以利用之前使用的模运算来模拟 Python 中的逻辑右移:

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

如果这对您的口味来说看起来过于复杂,那么您可以使用标准库中的一个模块来更清楚地表达相同的意图。例如,使用ctypes将产生相同的效果:

>>>
>>> from ctypes import c_uint8 as unsigned_byte
>>> bin(unsigned_byte(-42).value)
'0b11010110'

您以前见过它,但作为提醒,它会搭载 C 中的无符号整数类型。

另一个可用于 Python 中此类转换的标准模块是array模块。它定义了一个类似于a 的数据结构list但只允许保存相同数字类型的元素。声明数组时,需要用对应的字母在前面指明其类型:

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

例如,"b"代表一个 8 位有符号字节,而"B"代表它的无符号等效字节。还有一些其他预定义类型,例如带符号的 16 位整数或 32 位浮点数。

在这两个数组之间复制原始字节会改变位的解释方式。然而,它需要两倍的内存量,这是相当浪费的。要执行这样的位重写,您可以依赖struct模块,它使用一组类似的格式字符进行类型声明:

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

打包允许您根据给定的 C 数据类型说明符将对象放置在内存中。它返回一个只读bytes()对象,其中包含结果内存块的原始字节。稍后,您可以使用一组不同的类型代码读回这些字节,以更改它们被转换为 Python 对象的方式。

到目前为止,您已经使用了不同的技术来获得以二进制补码表示的整数的固定长度位串。如果你想将这些类型的位序列转换回 Python 整数,那么你可以试试这个函数:

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
    return (unsigned & bits_mask) - (unsigned & sign_mask)

该函数接受由二进制数字组成的字符串。首先,它将数字转换为普通的无符号整数,不考虑符号位。接下来,它使用两个位掩码来提取符号位和幅度位,其位置取决于指定的位长。最后,它使用常规算术将它们组合起来,知道与符号位关联的值是负数。

您可以针对前面示例中可靠的旧位字符串进行尝试:

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

Pythonint()将所有位视为量级,因此没有任何意外。但是,默认情况下,这个新函数假定一个 32 位长的字符串,这意味着对于较短的字符串,符号位隐式等于 0。当您请求与您的位字符串匹配的位长度时,您将获得预期的结果。

虽然在大多数情况下整数是最适合使用按位运算符的数据类型,但有时您需要提取和操作结构化二进制数据的片段,例如图像像素。该arraystruct在这个主题模块暂时触摸,所以你会更详细地探索它旁边。

查看二进制数据

您知道如何读取和解释单个字节。然而,现实世界的数据通常由多个字节组成来传达信息。以float数据类型为例。Python 中的单个浮点数在内存中占用多达 8 个字节。

你怎么看这些字节?

您不能简单地使用按位运算符,因为它们不适用于浮点数:

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

您必须忘记您正在处理的特定数据类型,并根据通用字节流来考虑它。这样,字节在按位运算符处理的上下文之外代表什么就无关紧要了。

bytes()在 Python 中获取浮点数的 ,您可以使用熟悉的struct模块对其进行打包:

>>>
>>> from struct import pack
>>> pack(">d", 3.14159)
b'@\t!\xf9\xf0\x1b\x86n'

忽略通过第一个参数传递的格式字符。在您进入下面的字节顺序部分之前,它们没有意义。在这个相当晦涩的文本表示背后隐藏着一个包含八个整数的列表:

>>>
>>> list(b"@\t!\xf9\xf0\x1b\x86n")
[64, 9, 33, 249, 240, 27, 134, 110]

它们的值对应于用于表示二进制浮点数的后续字节。您可以将它们组合起来生成一个很长的位串:

>>>
>>> from struct import pack
>>> "".join([f"{b:08b}" for b in pack(">d", 3.14159)])
'0100000000001001001000011111100111110000000110111000011001101110'

这 64 位是您之前阅读的双精度符号、指数和尾数。要从float类似的位串合成 a ,您可以颠倒过程:

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

unpack()返回一个元组,因为它允许您一次读取多个值。例如,您可以读取与四个 16 位有符号整数相同的位串:

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

如您所见,必须预先了解位字符串的解释方式,以避免以乱码数据结束。您需要问自己的一个重要问题是您应该从字节流的哪一端开始读取——向左还是向右。请仔细阅读,找出答案。

字节顺序

关于单个字节中的位顺序没有争议。无论它们在内存中的物理布局如何,您总是会在索引 0 处找到最低有效位和在索引 7 处找到最高有效位。按位移位运算符依赖于这种一致性。

但是,对于多字节数据块中的字节顺序没有达成共识。例如,可以像英文文本一样从左到右阅读包含多于一个字节的信息,或者像阿拉伯语一样从右到左阅读。计算机在二进制流中看到字节,就像人类在句子中看到单词一样。

计算机选择从哪个方向读取字节并不重要,只要它们在任何地方应用相同的规则即可。不幸的是,不同的计算机架构使用不同的方法,这使得它们之间的数据传输具有挑战性。

大端与小端

让我们取一个 32 位无符号整数,对应于数字 1969 10,这是Monty Python首次出现在电视上的年份。对于所有前导零,它具有以下二进制表示 00000000000000000000011110110001 2

您将如何在计算机内存中存储这样的值?

如果您将内存想象成一个由字节组成的一维磁带,那么您需要将该数据分解为单个字节并将它们排列在一个连续的块中。有些人觉得从左端开始很自然,因为这是他们阅读的方式,而另一些人则更喜欢从右端开始:

字节顺序 地址 N 地址 N+1 地址 N+2 地址 N+3
Big-Endian 000000002 000000002 000001112 101100012
Little-Endian 101100012 000001112 000000002 000000002

当字节从左到右放置时,最重要的字节被分配到最低的内存地址。这被称为大端顺序。相反,当字节从右到左存储时,最低有效字节在前。这就是所谓的小端顺序。

注意:这些幽默的名字从乔纳森·斯威夫特 (Jonathan Swift)的 18 世纪小说《格列佛游记》中汲取灵感。作者描述了 Little-Endians 和 Big-Endians 之间关于打破煮鸡蛋壳的正确方法的冲突。Little-Endians 更喜欢从小端开始,Big-Endians 更喜欢大端。

哪种方式更好?

从实际的角度来看,使用一个比另一个没有真正的优势。硬件级别的性能可能会有一些小幅提升,但您不会注意到它们。主要网络协议使用 big-endian 顺序,鉴于IP 寻址的分层设计,这允许它们更快地过滤数据包。除此之外,有些人可能会发现在调试时使用特定的字节顺序更方便。

无论哪种方式,如果你没有做对并将两个标准混为一谈,那么糟糕的事情就会开始发生:

>>>
>>> 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

当您使用一种约定将某个值序列化为字节流并尝试使用另一种约定将其读回时,您将得到一个完全无用的结果。这种情况最有可能发生在通过网络发送数据时,但您也可以在读取特定格式的本地文件时遇到这种情况。例如,Windows 位图的标头始终使用小端,而JPEG可以使用两种字节顺序。

本机字节序

要找出平台的字节顺序,您可以使用以下sys模块:

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

但是,您无法更改字节顺序,因为它是您的CPU 架构的内在特征。如果没有诸如QEMU 之类的硬件虚拟化,就不可能出于测试目的模拟它,因此即使是流行的VirtualBox也无济于事。

值得注意的是,为大多数现代笔记本电脑和台式机提供动力的 Intel 和 AMD 的 x86 系列处理器是小端的。移动设备基于低能耗的 ARM 架构,这是双端的,而一些较旧的架构,例如古老的摩托罗拉 68000,仅是大端的。

有关确定 C 中字节顺序的信息,请展开下面的框。

在 C 中检查字节顺序显示隐藏

一旦您知道机器的本机字节序,您将需要在操作二进制数据时在不同的字节顺序之间进行转换。无论手头的数据类型如何,一种通用的方法是反转通用bytes()对象或表示这些字节的整数序列:

>>>
>>> big_endian = b"\x00\x00\x07\xb1"
>>> bytes(reversed(big_endian))
b'\xb1\x07\x00\x00'

但是,使用struct模块通常更方便,它允许您定义标准 C 数据类型。除此之外,它还允许您使用可选的修饰符请求给定的字节顺序:

>>>
>>> from struct import pack, unpack
>>> pack(">I", 1969)  # Big-endian unsigned int
b'\x00\x00\x07\xb1'
>>> unpack("<I", b"\x00\x00\x07\xb1")  # Little-endian unsigned int
(2970025984,)

大于号 ( >) 表示字节按大端顺序排列,而小于号 ( <) 对应于小端。如果您不指定一个,则假定本机字节序。还有一些修饰符,例如感叹号 ( !),它表示网络字节顺序。

网络字节顺序

计算机网络由异构设备组成,例如笔记本电脑、台式机、平板电脑、智能手机,甚至配备 Wi-Fi 适配器的灯泡。它们都需要商定的协议和标准,包括二进制传输的字节顺序,才能有效地进行通信。

在互联网诞生之初,人们决定这些网络协议的字节顺序是big-endian

想要通过网络进行通信的程序可以获取经典的 C API,它通过套接字层抽象出细节。Python 通过内置socket模块包装该 API 。但是,除非您正在编写自定义二进制协议,否则您可能希望利用更高级别的抽象,例如基于文本的HTTP 协议

socket模块可能是有用的是在字节顺序转换。它从 C API 中公开了一些函数,它们具有独特的、令人费解的名称:

>>>
>>> from 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

如果您的主机已经使用大端字节序,则无需执行任何操作。这些值将保持不变。

位掩码

位掩码的工作原理类似于涂鸦模板,可阻止油漆喷涂到表面的特定区域。它允许您隔离这些位以选择性地对其应用某些功能。位掩码涉及您已经阅读过的按位逻辑运算符和按位移位运算符。

您可以在许多不同的上下文中找到位掩码。例如,IP 寻址中的子网掩码实际上是一个位掩码,可以帮助您提取网络地址。可以使用位掩码访问与 RGB 模型中的红色、绿色和蓝色相对应的像素通道。您还可以使用位掩码来定义布尔标志,然后您可以将这些标志打包到位字段中

有几种常见的与位掩码相关的操作类型。您将在下面快速浏览其中的一些。

得到一点

要读取给定位置上特定位的值,您可以对仅由所需索引处的一位组成的位掩码使用按位与:

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

掩码将抑制除您感兴趣的位之外的所有位。它会导致指数等于位索引的零或二的幂。如果你想得到一个简单的是或否的答案,那么你可以向右移动并检查最不重要的位:

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

这一次,它将对位值进行标准化,使其永远不会超过 1。然后您可以使用该函数来导出布尔值TrueFalse值而不是数值。

设置位

设置一点类似于得到一个。您可以利用与以前相同的位掩码,但不是使用按位 AND,而是使用按位 OR 运算符:

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

掩码保留所有原始位,同时在指定索引处强制执行二进制 1。如果该位已经被设置,它的值就不会改变。

取消设置位

要清除一点,您希望复制所有二进制数字,同时在一个特定索引处强制为零。您可以通过再次使用相同的位掩码来实现此效果,但采用反转形式:

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

在正数上使用按位 NOT 总是在 Python 中产生一个负值。虽然这通常是不可取的,但在这里并不重要,因为您会立即应用按位 AND 运算符。反过来,这会触发掩码转换为二进制补码表示,从而获得预期结果。

切换一点

有时,能够定期打开和关闭一些开关很有用。这是按位异或运算符的绝佳机会,它可以像这样翻转您的位:

>>>
>>> def toggle_bit(value, bit_index):
...     return value ^ (1 << bit_index)
...
>>> x = 0b10100000
>>> for _ in range(5):
...     x = toggle_bit(x, bit_index=7)
...     print(bin(x))
...
0b100000
0b10100000
0b100000
0b10100000
0b100000

请注意再次使用相同的位掩码。指定位置上的二进制 1 将使该索引处的位反转其值。在其余位置上使用二进制零将确保其余位将被复制。

按位运算符重载

按位运算符的主要域是整数。这是他们最有意义的地方。但是,您还看到它们在布尔上下文中使用,在其中它们替换了逻辑运算符。Python 为其某些运算符提供了替代实现,并允许您为新数据类型重载它们。

尽管在 Python 中重载逻辑运算符的提议被拒绝了,但您可以为任何按位运算符赋予新的含义。许多流行的库,甚至标准库,都利用了它。

内置数据类型

Python 按位运算符是为以下内置数据类型定义的:

这不是一个广为人知的事实,但按位运算符可以从集合代数执行操作,例如并集、交集和对称差,以及合并和更新字典

注意:在撰写本文时,Python 3.9尚未发布,但您可以使用Dockerpyenv偷看即将推出的语言功能。

ab是 Python 集时,则按位运算符对应以下方法:

设置方法 按位运算符
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

它们几乎做同样的事情,因此使用哪种语法取决于您。除此之外,还有一个重载的减号运算符 ( -),它实现了两个集合的差值。要查看它们的实际效果,假设您有以下两组水果和蔬菜:

>>>
>>> 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'}

它们共享一个难以分类的共同成员,但它们的其余元素是不相交的。

需要注意的一件事是 immutable frozenset(),它缺少就地更新的方法。但是,当您使用它们的按位运算符对应项时,含义略有不同:

>>>
>>> const_fruits = frozenset({"apple", "banana", "tomato"})
>>> const_veggies = frozenset({"eggplant", "tomato"})
>>> const_fruits.update(const_veggies)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    const_fruits.update(const_veggies)
AttributeError: 'frozenset' object has no attribute 'update'
>>> const_fruits |= const_veggies
>>> const_fruits
frozenset({'tomato', 'apple', 'eggplant', 'banana'})

frozenset()当您使用按位运算符时,它看起来并不是那么一成不变,但问题在于细节。以下是实际发生的情况:

const_fruits = const_fruits | const_veggies

它第二次起作用的原因是您没有更改原始的不可变对象。相反,您创建一个新变量并再次将其分配给同一个变量。

Pythondict仅支持按位 OR,其工作方式类似于联合运算符。您可以使用它来更新字典或将两个字典合并为一个新字典:

>>>
>>> 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)

按位运算符的增强版本等效于.update()

第三方模块

许多流行的库,包括NumPypandasSQLAlchemy,都为它们的特定数据类型重载了按位运算符。这是您最有可能在 Python 中找到按位运算符的地方,因为它们不再经常以其原始含义使用。

例如,NumPy 以逐点方式将它们应用于矢量化数据:

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

这样,您无需手动将相同的按位运算符应用于数组的每个元素。但是你不能用 Python 中的普通列表做同样的事情。

pandas 在幕后使用 NumPy,它还为其DataFrameSeries对象提供了按位运算符的重载版本。但是,它们的行为与您期望的一样。唯一的区别是他们在数字的向量和矩阵上而不是在单个标量上做他们通常的工作。

使用赋予按位运算符全新含义的库,事情会变得更有趣。例如,SQLAlchemy 提供了一种用于查询数据库的简洁语法:

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

按位 AND 运算符 ( &) 最终将转换为一段SQL查询。然而,这不是很明显,至少对我的IDE 来说不是,当它在这种类型的表达式中看到它们时,它会抱怨按位运算符的非pythonic使用。它立即建议用&逻辑替换每个出现的and,不知道这样做会使代码停止工作!

这种类型的运算符重载是一种有争议的做法,它依赖于您必须预先了解的隐式魔法。一些编程语言(如 Java)通过完全禁止运算符重载来防止这种滥用。Python 在这方面更加自由,并且相信您知道自己在做什么。

自定义数据类型

要自定义 Python 的按位运算符的行为,您必须定义一个,然后在其中实现相应的魔术方法。同时,您不能为现有类型重新定义按位运算符的行为。运算符重载仅适用于新数据类型。

以下是让您重载按位运算符的特殊方法的简要概述:

魔法方法 表达
.__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

您不需要定义所有这些。例如,要使用稍微更方便的语法将元素附加到deque,仅实现.__lshift__()and就足够了.__rrshift__()

>>>
>>> from 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'])

这个用户定义的类包装了一个双端队列以重用它的实现,并用两个额外的方法来扩充它,这些方法允许将项目添加到集合的左端或右端。

最不重要的位隐写术

哇,要处理的事情太多了!如果您仍在挠头,想知道为什么要使用按位运算符,那么请不要担心。是时候以有趣的方式展示您可以用它们做什么了。

要跟随本节中的示例,您可以通过单击下面的链接下载源代码:

获取源代码: 单击此处获取源代码,您将在本教程中用于了解 Python 的按位运算符。

您将学习隐写术并将此概念应用于在位图图像中秘密嵌入任意文件。

密码学与隐写术

密码学是将一条消息变成只有拥有正确密钥的人才能读取的消息。其他人仍然可以看到加密的消息,但对他们来说没有任何意义。密码学的最初形式之一是替代密码,例如以朱利叶斯·凯撒命名的凯撒密码

隐写术类似于密码术,因为它还允许您与所需的受众共享秘密消息。然而,它没有使用加密,而是巧妙地将信息隐藏在不引起注意的介质中。示例包括使用隐形墨水或写离合体,其中每个单词或行的第一个字母形成一个秘密信息。

除非您知道秘密消息被隐藏以及恢复它的方法,否则您可能会忽略运营商。您可以将这两种技术结合起来更安全,隐藏加密消息而不是原始消息。

有很多方法可以在数字世界中走私秘密数据。特别是携带大量数据的文件格式,例如音频文件、视频或图像,非常适合,因为它们为您提供了很大的工作空间。例如,发布受版权保护的材料的公司可能会使用隐写术为单个副本添加水印并追踪泄漏源。

下面,您将把秘密数据注入到一个普通的bitmap 中,这在 Python 中很容易读写,不需要外部依赖。

位图文件格式

位图一词通常指的是Windows 位图.bmp) 文件格式,它支持几种表示像素的替代方法。为了让生活更轻松,您将假设像素以 24 位未压缩RGB(红色、绿色和蓝色)格式存储。一个像素将具有三个颜色通道,每个通道可以保存从 0 10到 255 10 的值

每个位图都以文件头开头,其中包含图像宽度和高度等元数据。以下是一些有趣的字段及其相对于标题开头的位置:

场地 字节偏移 字节长度 类型 样本值
签名 0x00 2 细绳 BM
文件大小 0x02 4 无符号整数 7,629,186
保留 #1 0x06 2 字节 0
保留 #2 0x08 2 字节 0
像素偏移 0x0a 4 无符号整数 122
像素大小 0x22 4 无符号整数 7,629,064
图像宽度 0x12 4 无符号整数 1,954
图像高度 0x16 4 无符号整数 1,301
每像素位数 0x1c 2 无符号空头 24
压缩 0x1e 4 无符号整数 0
调色板 0x2e 4 无符号整数 0

您可以从该标头推断相应的位图宽 1,954 像素,高 1,301 像素。它不使用压缩,也没有调色板。每个像素占用 24 位或 3 个字节,原始像素数据从偏移量 122 10开始。

您可以以二进制模式打开位图,寻找所需的偏移量,读取给定的字节数,然后像以前一样使用反序列化它们:struct

from struct import unpack

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

请注意,位图中的所有整数字段都以 little-endian 字节顺序存储。

您可能已经注意到标头中声明的像素字节数与图像大小导致的像素字节数之间存在微小差异。当您乘以 1,954 像素 × 1,301 像素 × 3 字节时,您会得到一个比 7,629,064 少 2,602 字节的值。

这是因为像素字节用零填充,因此每一行都是四字节的倍数。如果图像的宽度乘以三个字节恰好是四的倍数,则不需要填充。否则,在每一行的末尾添加空字节。

注意:为避免引起怀疑,您需要通过跳过空字节来考虑该填充。否则,对于知道要寻找什么的人来说,这将是一个明显的赠品。

位图倒置存储像素行,从底部而不是顶部开始。此外,每个像素都以有点奇怪的 BGR 顺序而不是 RGB 序列化为颜色通道向量。然而,这与隐藏秘密数据的任务无关。

按位捉迷藏

您可以使用按位运算符将自定义数据分布在连续的像素字节上。这个想法是用来自下一个秘密字节的位覆盖每个中的最低有效位。这将引入最少的噪声,但您可以尝试添加更多位以在注入数据的大小和像素失真之间取得平衡。

注意:使用最低有效位隐写术不会影响生成的位图的文件大小。它将保持与原始文件相同。

在某些情况下,相应的位将相同,导致像素值没有任何变化。然而,即使在最坏的情况下,像素颜色的差异也只有百分之几。这种微小的异常对人眼来说仍然是不可见的,但可以通过使用统计数据的隐写分析检测到。

看看这些裁剪的图像:

原始位图与带有秘密数据的更改位图

左边的一个来自原始位图,而右边的图像描绘了一个处理过的位图,嵌入的视频存储在最低有效位上。您看得出来差别吗?

以下代码将秘密数据编码到位图上:

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

对于每个字节的秘密数据和相应的八个字节的像素数据,不包括填充字节,它准备一个要传播的位列表。接下来,它使用相关位掩码覆盖八个字节中每个字节中的最低有效位。结果被转换为一个bytes()对象并分配回它最初来自的位图部分。

要从同一个位图解码文件,您需要知道写入了多少秘密字节。您可以在数据流的开头分配几个字节来存储这个数字,或者您可以使用位图标头中的保留字段:

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

这会跳转到文件中的正确偏移量,将 Python 序列化为int原始字节,并将它们写下来。

您可能还想存储机密文件的名称。由于它可以具有任意长度,因此使用空终止字符串对其进行序列化是有意义的,该字符串将位于文件内容之前。要创建这样的字符串,您需要将 Pythonstr对象编码为字节并在末尾手动附加空字节:

>>>
>>> from pathlib import Path
>>> path = Path("/home/jsmith/café.pdf")
>>> path.name.encode("utf-8") + b"\x00"
b'caf\xc3\xa9.pdf\x00'

此外,使用pathlib.

补充本文的示例代码将使您可以使用以下命令对给定位图中的机密文件进行编码解码擦除

$ 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

这是一个可运行的模块,可以通过调用其包含的目录来执行。您还可以根据其内容制作可移植的 ZIP 格式存档,以利用Python ZIP 应用程序支持。

该程序依赖于文章中提到的标准库中的模块以及您以前可能没有听说过的其他一些模块。一个关键模块是mmap,它将 Python 接口暴露给内存映射文件。它们让您可以使用标准文件 API 和序列 API 来操作大文件。就好像文件是一个可以切片的大可变列表。

继续使用附加到支持材料的位图。它包含一个小惊喜给你!

结论

掌握 Python 按位运算符可让您在项目中拥有操作二进制数据的最大自由。您现在知道它们的语法和不同的风格以及支持它们的数据类型。您还可以根据自己的需要自定义他们的行为。

在本教程中,您学习了如何:

  • 使用 Python按位运算符操作单个位
  • 以与平台无关的方式读取和写入二进制数据
  • 使用位掩码将信息打包到单个字节上
  • 在自定义数据类型中重载Python 按位运算符
  • 在数字图像中隐藏秘密信息

【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。