Python 中的按位運(yùn)算符2 |【生長(zhǎng)吧!Python!】 【生長(zhǎng)吧!Python】有獎(jiǎng)?wù)魑幕馃徇M(jìn)行中:https://bbs.huaweicloud.com/blogs/278897
Python 中的位串
歡迎您在本文的其余部分使用筆和紙。它甚至可以作為一個(gè)很好的鍛煉!但是,在某些時(shí)候,您需要驗(yàn)證您的二進(jìn)制序列或位字符串是否與 Python 中的預(yù)期數(shù)字相對(duì)應(yīng)。就是這樣。
轉(zhuǎn)換int為二進(jìn)制
要在 Python 中顯示組成整數(shù)的位,您可以打印格式化的字符串文字,它可以讓您選擇指定要顯示的前導(dǎo)零的數(shù)量:
>>>
>>> print(f"{42:b}") # Print 42 in binary 101010 >>> print(f"{42:032b}") # Print 42 in binary on 32 zero-padded digits 00000000000000000000000000101010
Python 中的位串
歡迎您在本文的其余部分使用筆和紙。它甚至可以作為一個(gè)很好的鍛煉!但是,在某些時(shí)候,您需要驗(yàn)證您的二進(jìn)制序列或位字符串是否與 Python 中的預(yù)期數(shù)字相對(duì)應(yīng)。就是這樣。
轉(zhuǎn)換int為二進(jìn)制
要在 Python 中顯示組成整數(shù)的位,您可以打印格式化的字符串文字,它可以讓您選擇指定要顯示的前導(dǎo)零的數(shù)量:
>>>
>>> print(f"{42:b}") # Print 42 in binary 101010 >>> print(f"{42:032b}") # Print 42 in binary on 32 zero-padded digits 00000000000000000000000000101010
或者,您可以bin()使用號(hào)碼作為參數(shù)調(diào)用:
>>>
>>> bin(42) '0b101010'
這個(gè)全局內(nèi)置函數(shù)返回一個(gè)由二進(jìn)制文字組成的字符串,它以前綴開(kāi)頭,0b后跟一和零。它始終顯示不帶前導(dǎo)零的最小位數(shù)。
您也可以在代碼中逐字使用此類文字:
>>>
>>> age = 0b101010 >>> print(age) 42
Python 中可用的其他整數(shù)文字是十六進(jìn)制和八進(jìn)制文字,您可以分別使用hex()和oct()函數(shù)獲取它們:
>>>
>>> hex(42) '0x2a' >>> oct(42) '0o52'
請(qǐng)注意十六進(jìn)制系統(tǒng)如何利用字母A直通F來(lái)擴(kuò)充可用數(shù)字集。其他編程語(yǔ)言中的八進(jìn)制文字通常以純零作為前綴,這可能會(huì)造成混淆。Python 明確禁止此類文字以避免出錯(cuò):
>>>
>>> 052 File "
您可以使用任何提到的整數(shù)文字以不同的方式表達(dá)相同的值:
>>>
>>> 42 == 0b101010 == 0x2a == 0o52 True
選擇在上下文中最有意義的一項(xiàng)。例如,習(xí)慣上用十六進(jìn)制表示法來(lái)表示位掩碼。另一方面,現(xiàn)在很少看到八進(jìn)制文字。
Python 中的所有數(shù)字文字都不區(qū)分大小寫(xiě),因此您可以使用小寫(xiě)或大寫(xiě)字母作為前綴:
>>>
>>> 0b101 == 0B101 True
這也適用于使用科學(xué)記數(shù)法的浮點(diǎn)數(shù)文字以及復(fù)數(shù)文字。
將二進(jìn)制轉(zhuǎn)換為?int
準(zhǔn)備好位字符串后,您可以通過(guò)利用二進(jìn)制文字來(lái)獲取其十進(jìn)制表示:
>>>
>>> 0b101010 42
這是在交互式 Python 解釋器中工作時(shí)進(jìn)行轉(zhuǎn)換的一種快速方法。不幸的是,它不會(huì)讓您轉(zhuǎn)換在運(yùn)行時(shí)合成的位序列,因?yàn)樗形淖侄夹枰谠创a中進(jìn)行硬編碼。
注意:您可能會(huì)想用 來(lái)評(píng)估Python 代碼eval("0b101010"),但這是一種危及程序安全的簡(jiǎn)單方法,所以不要這樣做!
int()在動(dòng)態(tài)生成的位串的情況下,使用兩個(gè)參數(shù)調(diào)用會(huì)更好:
>>>
>>> int("101010", 2) 42 >>> int("cafe", 16) 51966
第一個(gè)參數(shù)是一串?dāng)?shù)字,而第二個(gè)參數(shù)確定數(shù)字系統(tǒng)的基數(shù)。與二進(jìn)制文字不同,字符串可以來(lái)自任何地方,甚至是用戶在鍵盤上打字。要更深入地了解int(),您可以展開(kāi)下面的框。
的其他用途?int()顯示隱藏
到現(xiàn)在為止還挺好。但是負(fù)數(shù)呢?
模擬符號(hào)位
當(dāng)你調(diào)用bin()一個(gè)負(fù)整數(shù)時(shí),它只是在從相應(yīng)的正值獲得的位串前面加上減號(hào):
>>>
>>> print(bin(-42), bin(42), sep="\n ") -0b101010 0b101010
更改數(shù)字的符號(hào)不會(huì)影響 Python 中的底層位串。相反,在將位串轉(zhuǎn)換為十進(jìn)制形式時(shí),允許在位串前加上減號(hào):
>>>
>>> int("-101010", 2) -42
這在 Python 中是有意義的,因?yàn)樵趦?nèi)部,它不使用符號(hào)位。您可以將 Python 中整數(shù)的符號(hào)視為與模數(shù)分開(kāi)存儲(chǔ)的一條信息。
但是,有一些變通方法可以讓您模擬包含符號(hào)位的固定長(zhǎng)度位序列:
位掩碼
模運(yùn)算 (?%)
ctypes?模塊
array?模塊
struct?模塊
您從前面的部分知道,為了確保數(shù)字的某個(gè)位長(zhǎng)度,您可以使用漂亮的位掩碼。例如,要保留一個(gè)字節(jié),您可以使用恰好由八個(gè)開(kāi)啟位組成的掩碼:
>>>
>>> mask = 0b11111111 # Same as 0xff or 255 >>> bin(-42 & mask) '0b11010110'
掩碼強(qiáng)制 Python 暫時(shí)將數(shù)字的表示從符號(hào)幅度更改為二進(jìn)制補(bǔ)碼,然后再返回。如果您忘記了結(jié)果二進(jìn)制文字的十進(jìn)制值(等于 214?10 ),那么它將以二進(jìn)制補(bǔ)碼形式表示 -42?10。最左邊的位將是符號(hào)位。
或者,您可以利用之前使用的模運(yùn)算來(lái)模擬 Python 中的邏輯右移:
>>>
>>> bin(-42 % (1 << 8)) # Give me eight bits '0b11010110'
如果這對(duì)您的口味來(lái)說(shuō)看起來(lái)過(guò)于復(fù)雜,那么您可以使用標(biāo)準(zhǔn)庫(kù)中的一個(gè)模塊來(lái)更清楚地表達(dá)相同的意圖。例如,使用ctypes將產(chǎn)生相同的效果:
>>>
>>> from ctypes import c_uint8 as unsigned_byte >>> bin(unsigned_byte(-42).value) '0b11010110'
您以前見(jiàn)過(guò)它,但作為提醒,它會(huì)搭載 C 中的無(wú)符號(hào)整數(shù)類型。
另一個(gè)可用于 Python 中此類轉(zhuǎn)換的標(biāo)準(zhǔn)模塊是array模塊。它定義了一個(gè)類似于a 的數(shù)據(jù)結(jié)構(gòu),list但只允許保存相同數(shù)字類型的元素。聲明數(shù)組時(shí),需要用對(duì)應(yīng)的字母在前面指明其類型:
>>>
>>> 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"代表一個(gè) 8 位有符號(hào)字節(jié),而"B"代表它的無(wú)符號(hào)等效字節(jié)。還有一些其他預(yù)定義類型,例如帶符號(hào)的 16 位整數(shù)或 32 位浮點(diǎn)數(shù)。
在這兩個(gè)數(shù)組之間復(fù)制原始字節(jié)會(huì)改變位的解釋方式。然而,它需要兩倍的內(nèi)存量,這是相當(dāng)浪費(fèi)的。要執(zhí)行這樣的位重寫(xiě),您可以依賴struct模塊,它使用一組類似的格式字符進(jìn)行類型聲明:
>>>
>>> from struct import pack, unpack >>> unpack("BB", pack("bb", -42, 42)) (214, 42) >>> bin(214) '0b11010110'
打包允許您根據(jù)給定的 C 數(shù)據(jù)類型說(shuō)明符將對(duì)象放置在內(nèi)存中。它返回一個(gè)只讀bytes()對(duì)象,其中包含結(jié)果內(nèi)存塊的原始字節(jié)。稍后,您可以使用一組不同的類型代碼讀回這些字節(jié),以更改它們被轉(zhuǎn)換為 Python 對(duì)象的方式。
到目前為止,您已經(jīng)使用了不同的技術(shù)來(lái)獲得以二進(jìn)制補(bǔ)碼表示的整數(shù)的固定長(zhǎng)度位串。如果你想將這些類型的位序列轉(zhuǎn)換回 Python 整數(shù),那么你可以試試這個(gè)函數(shù):
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)
該函數(shù)接受由二進(jìn)制數(shù)字組成的字符串。首先,它將數(shù)字轉(zhuǎn)換為普通的無(wú)符號(hào)整數(shù),不考慮符號(hào)位。接下來(lái),它使用兩個(gè)位掩碼來(lái)提取符號(hào)位和幅度位,其位置取決于指定的位長(zhǎng)。最后,它使用常規(guī)算術(shù)將它們組合起來(lái),知道與符號(hào)位關(guān)聯(lián)的值是負(fù)數(shù)。
您可以針對(duì)前面示例中可靠的舊位字符串進(jìn)行嘗試:
>>>
>>> int("11010110", 2) 214 >>> from_twos_complement("11010110") 214 >>> from_twos_complement("11010110", num_bits=8) -42
Pythonint()將所有位視為量級(jí),因此沒(méi)有任何意外。但是,默認(rèn)情況下,這個(gè)新函數(shù)假定一個(gè) 32 位長(zhǎng)的字符串,這意味著對(duì)于較短的字符串,符號(hào)位隱式等于 0。當(dāng)您請(qǐng)求與您的位字符串匹配的位長(zhǎng)度時(shí),您將獲得預(yù)期的結(jié)果。
雖然在大多數(shù)情況下整數(shù)是最適合使用按位運(yùn)算符的數(shù)據(jù)類型,但有時(shí)您需要提取和操作結(jié)構(gòu)化二進(jìn)制數(shù)據(jù)的片段,例如圖像像素。該array和struct在這個(gè)主題模塊暫時(shí)觸摸,所以你會(huì)更詳細(xì)地探索它旁邊。
查看二進(jìn)制數(shù)據(jù)
您知道如何讀取和解釋單個(gè)字節(jié)。然而,現(xiàn)實(shí)世界的數(shù)據(jù)通常由多個(gè)字節(jié)組成來(lái)傳達(dá)信息。以float數(shù)據(jù)類型為例。Python 中的單個(gè)浮點(diǎn)數(shù)在內(nèi)存中占用多達(dá) 8 個(gè)字節(jié)。
你怎么看這些字節(jié)?
您不能簡(jiǎn)單地使用按位運(yùn)算符,因?yàn)樗鼈儾贿m用于浮點(diǎn)數(shù):
>>>
>>> 3.14 & 0xff Traceback (most recent call last): File "
您必須忘記您正在處理的特定數(shù)據(jù)類型,并根據(jù)通用字節(jié)流來(lái)考慮它。這樣,字節(jié)在按位運(yùn)算符處理的上下文之外代表什么就無(wú)關(guān)緊要了。
要bytes()在 Python 中獲取浮點(diǎn)數(shù)的 ,您可以使用熟悉的struct模塊對(duì)其進(jìn)行打包:
>>>
>>> from struct import pack >>> pack(">d", 3.14159) b'@\t!\xf9\xf0\x1b\x86n'
忽略通過(guò)第一個(gè)參數(shù)傳遞的格式字符。在您進(jìn)入下面的字節(jié)順序部分之前,它們沒(méi)有意義。在這個(gè)相當(dāng)晦澀的文本表示背后隱藏著一個(gè)包含八個(gè)整數(shù)的列表:
>>>
>>> list(b"@\t!\xf9\xf0\x1b\x86n") [64, 9, 33, 249, 240, 27, 134, 110]
它們的值對(duì)應(yīng)于用于表示二進(jìn)制浮點(diǎn)數(shù)的后續(xù)字節(jié)。您可以將它們組合起來(lái)生成一個(gè)很長(zhǎng)的位串:
>>>
>>> from struct import pack >>> "".join([f"{b:08b}" for b in pack(">d", 3.14159)]) '0100000000001001001000011111100111110000000110111000011001101110'
這 64 位是您之前閱讀的雙精度符號(hào)、指數(shù)和尾數(shù)。要從float類似的位串合成 a?,您可以顛倒過(guò)程:
>>>
>>> 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()返回一個(gè)元組,因?yàn)樗试S您一次讀取多個(gè)值。例如,您可以讀取與四個(gè) 16 位有符號(hào)整數(shù)相同的位串:
>>>
>>> unpack( ... ">hhhh", ... bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8)) ... ) (16393, 8697, -4069, -31122)
如您所見(jiàn),必須預(yù)先了解位字符串的解釋方式,以避免以亂碼數(shù)據(jù)結(jié)束。您需要問(wèn)自己的一個(gè)重要問(wèn)題是您應(yīng)該從字節(jié)流的哪一端開(kāi)始讀取——向左還是向右。請(qǐng)仔細(xì)閱讀,找出答案。
字節(jié)順序
關(guān)于單個(gè)字節(jié)中的位順序沒(méi)有爭(zhēng)議。無(wú)論它們?cè)趦?nèi)存中的物理布局如何,您總是會(huì)在索引 0 處找到最低有效位和在索引 7 處找到最高有效位。按位移位運(yùn)算符依賴于這種一致性。
但是,對(duì)于多字節(jié)數(shù)據(jù)塊中的字節(jié)順序沒(méi)有達(dá)成共識(shí)。例如,可以像英文文本一樣從左到右閱讀包含多于一個(gè)字節(jié)的信息,或者像阿拉伯語(yǔ)一樣從右到左閱讀。計(jì)算機(jī)在二進(jìn)制流中看到字節(jié),就像人類在句子中看到單詞一樣。
計(jì)算機(jī)選擇從哪個(gè)方向讀取字節(jié)并不重要,只要它們?cè)谌魏蔚胤綉?yīng)用相同的規(guī)則即可。不幸的是,不同的計(jì)算機(jī)架構(gòu)使用不同的方法,這使得它們之間的數(shù)據(jù)傳輸具有挑戰(zhàn)性。
大端與小端
讓我們?nèi)∫粋€(gè) 32 位無(wú)符號(hào)整數(shù),對(duì)應(yīng)于數(shù)字 1969?10,這是Monty Python首次出現(xiàn)在電視上的年份。對(duì)于所有前導(dǎo)零,它具有以下二進(jìn)制表示 00000000000000000000011110110001?2。
您將如何在計(jì)算機(jī)內(nèi)存中存儲(chǔ)這樣的值?
如果您將內(nèi)存想象成一個(gè)由字節(jié)組成的一維磁帶,那么您需要將該數(shù)據(jù)分解為單個(gè)字節(jié)并將它們排列在一個(gè)連續(xù)的塊中。有些人覺(jué)得從左端開(kāi)始很自然,因?yàn)檫@是他們閱讀的方式,而另一些人則更喜歡從右端開(kāi)始:
當(dāng)字節(jié)從左到右放置時(shí),最重要的字節(jié)被分配到最低的內(nèi)存地址。這被稱為大端順序。相反,當(dāng)字節(jié)從右到左存儲(chǔ)時(shí),最低有效字節(jié)在前。這就是所謂的小端順序。
哪種方式更好?
從實(shí)際的角度來(lái)看,使用一個(gè)比另一個(gè)沒(méi)有真正的優(yōu)勢(shì)。硬件級(jí)別的性能可能會(huì)有一些小幅提升,但您不會(huì)注意到它們。主要網(wǎng)絡(luò)協(xié)議使用 big-endian 順序,鑒于IP 尋址的分層設(shè)計(jì),這允許它們更快地過(guò)濾數(shù)據(jù)包。除此之外,有些人可能會(huì)發(fā)現(xiàn)在調(diào)試時(shí)使用特定的字節(jié)順序更方便。
無(wú)論哪種方式,如果你沒(méi)有做對(duì)并將兩個(gè)標(biāo)準(zhǔn)混為一談,那么糟糕的事情就會(huì)開(kāi)始發(fā)生:
>>>
>>> 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
當(dāng)您使用一種約定將某個(gè)值序列化為字節(jié)流并嘗試使用另一種約定將其讀回時(shí),您將得到一個(gè)完全無(wú)用的結(jié)果。這種情況最有可能發(fā)生在通過(guò)網(wǎng)絡(luò)發(fā)送數(shù)據(jù)時(shí),但您也可以在讀取特定格式的本地文件時(shí)遇到這種情況。例如,Windows 位圖的標(biāo)頭始終使用小端,而JPEG可以使用兩種字節(jié)順序。
本機(jī)字節(jié)序
要找出平臺(tái)的字節(jié)順序,您可以使用以下sys模塊:
>>>
>>> import sys >>> sys.byteorder 'little'
但是,您無(wú)法更改字節(jié)順序,因?yàn)樗悄腃PU 架構(gòu)的內(nèi)在特征。如果沒(méi)有諸如QEMU 之類的硬件虛擬化,就不可能出于測(cè)試目的模擬它,因此即使是流行的VirtualBox也無(wú)濟(jì)于事。
值得注意的是,為大多數(shù)現(xiàn)代筆記本電腦和臺(tái)式機(jī)提供動(dòng)力的 Intel 和 AMD 的 x86 系列處理器是小端的。移動(dòng)設(shè)備基于低能耗的 ARM 架構(gòu),這是雙端的,而一些較舊的架構(gòu),例如古老的摩托羅拉 68000,僅是大端的。
有關(guān)確定 C 中字節(jié)順序的信息,請(qǐng)展開(kāi)下面的框。
在 C 中檢查字節(jié)順序顯示隱藏
一旦您知道機(jī)器的本機(jī)字節(jié)序,您將需要在操作二進(jìn)制數(shù)據(jù)時(shí)在不同的字節(jié)順序之間進(jìn)行轉(zhuǎn)換。無(wú)論手頭的數(shù)據(jù)類型如何,一種通用的方法是反轉(zhuǎn)通用bytes()對(duì)象或表示這些字節(jié)的整數(shù)序列:
>>>
>>> big_endian = b"\x00\x00\x07\xb1" >>> bytes(reversed(big_endian)) b'\xb1\x07\x00\x00'
但是,使用struct模塊通常更方便,它允許您定義標(biāo)準(zhǔn) C 數(shù)據(jù)類型。除此之外,它還允許您使用可選的修飾符請(qǐng)求給定的字節(jié)順序:
>>>
>>> from struct import pack, unpack >>> pack(">I", 1969) # Big-endian unsigned int b'\x00\x00\x07\xb1' >>> unpack("
大于號(hào) (?>) 表示字節(jié)按大端順序排列,而小于號(hào) (?<) 對(duì)應(yīng)于小端。如果您不指定一個(gè),則假定本機(jī)字節(jié)序。還有一些修飾符,例如感嘆號(hào) (?!),它表示網(wǎng)絡(luò)字節(jié)順序。
網(wǎng)絡(luò)字節(jié)順序
計(jì)算機(jī)網(wǎng)絡(luò)由異構(gòu)設(shè)備組成,例如筆記本電腦、臺(tái)式機(jī)、平板電腦、智能手機(jī),甚至配備 Wi-Fi 適配器的燈泡。它們都需要商定的協(xié)議和標(biāo)準(zhǔn),包括二進(jìn)制傳輸?shù)淖止?jié)順序,才能有效地進(jìn)行通信。
在互聯(lián)網(wǎng)誕生之初,人們決定這些網(wǎng)絡(luò)協(xié)議的字節(jié)順序是big-endian。
想要通過(guò)網(wǎng)絡(luò)進(jìn)行通信的程序可以獲取經(jīng)典的 C API,它通過(guò)套接字層抽象出細(xì)節(jié)。Python 通過(guò)內(nèi)置socket模塊包裝該 API?。但是,除非您正在編寫(xiě)自定義二進(jìn)制協(xié)議,否則您可能希望利用更高級(jí)別的抽象,例如基于文本的HTTP 協(xié)議。
凡socket模塊可能是有用的是在字節(jié)順序轉(zhuǎn)換。它從 C API 中公開(kāi)了一些函數(shù),它們具有獨(dú)特的、令人費(fèi)解的名稱:
>>>
>>> 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
如果您的主機(jī)已經(jīng)使用大端字節(jié)序,則無(wú)需執(zhí)行任何操作。這些值將保持不變。
位掩碼
位掩碼的工作原理類似于涂鴉模板,可阻止油漆噴涂到表面的特定區(qū)域。它允許您隔離這些位以選擇性地對(duì)其應(yīng)用某些功能。位掩碼涉及您已經(jīng)閱讀過(guò)的按位邏輯運(yùn)算符和按位移位運(yùn)算符。
您可以在許多不同的上下文中找到位掩碼。例如,IP 尋址中的子網(wǎng)掩碼實(shí)際上是一個(gè)位掩碼,可以幫助您提取網(wǎng)絡(luò)地址。可以使用位掩碼訪問(wèn)與 RGB 模型中的紅色、綠色和藍(lán)色相對(duì)應(yīng)的像素通道。您還可以使用位掩碼來(lái)定義布爾標(biāo)志,然后您可以將這些標(biāo)志打包到位字段中。
有幾種常見(jiàn)的與位掩碼相關(guān)的操作類型。您將在下面快速瀏覽其中的一些。
得到一點(diǎn)
要讀取給定位置上特定位的值,您可以對(duì)僅由所需索引處的一位組成的位掩碼使用按位與:
>>>
>>> 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
掩碼將抑制除您感興趣的位之外的所有位。它會(huì)導(dǎo)致指數(shù)等于位索引的零或二的冪。如果你想得到一個(gè)簡(jiǎn)單的是或否的答案,那么你可以向右移動(dòng)并檢查最不重要的位:
>>>
>>> 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
這一次,它將對(duì)位值進(jìn)行標(biāo)準(zhǔn)化,使其永遠(yuǎn)不會(huì)超過(guò) 1。然后您可以使用該函數(shù)來(lái)導(dǎo)出布爾值True或False值而不是數(shù)值。
設(shè)置位
設(shè)置一點(diǎn)類似于得到一個(gè)。您可以利用與以前相同的位掩碼,但不是使用按位 AND,而是使用按位 OR 運(yùn)算符:
>>>
>>> def set_bit(value, bit_index): ... return value | (1 << bit_index) ... >>> set_bit(0b10000000, bit_index=5) 160 >>> bin(160) '0b10100000'
掩碼保留所有原始位,同時(shí)在指定索引處強(qiáng)制執(zhí)行二進(jìn)制 1。如果該位已經(jīng)被設(shè)置,它的值就不會(huì)改變。
取消設(shè)置位
要清除一點(diǎn),您希望復(fù)制所有二進(jìn)制數(shù)字,同時(shí)在一個(gè)特定索引處強(qiáng)制為零。您可以通過(guò)再次使用相同的位掩碼來(lái)實(shí)現(xiàn)此效果,但采用反轉(zhuǎn)形式:
>>>
>>> def clear_bit(value, bit_index): ... return value & ~(1 << bit_index) ... >>> clear_bit(0b11111111, bit_index=5) 223 >>> bin(223) '0b11011111'
在正數(shù)上使用按位 NOT 總是在 Python 中產(chǎn)生一個(gè)負(fù)值。雖然這通常是不可取的,但在這里并不重要,因?yàn)槟鷷?huì)立即應(yīng)用按位 AND 運(yùn)算符。反過(guò)來(lái),這會(huì)觸發(fā)掩碼轉(zhuǎn)換為二進(jìn)制補(bǔ)碼表示,從而獲得預(yù)期結(jié)果。
切換一點(diǎn)
有時(shí),能夠定期打開(kāi)和關(guān)閉一些開(kāi)關(guān)很有用。這是按位異或運(yùn)算符的絕佳機(jī)會(huì),它可以像這樣翻轉(zhuǎn)您的位:
>>>
>>> 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
請(qǐng)注意再次使用相同的位掩碼。指定位置上的二進(jìn)制 1 將使該索引處的位反轉(zhuǎn)其值。在其余位置上使用二進(jìn)制零將確保其余位將被復(fù)制。
按位運(yùn)算符重載
按位運(yùn)算符的主要域是整數(shù)。這是他們最有意義的地方。但是,您還看到它們?cè)诓紶柹舷挛闹惺褂茫谄渲兴鼈兲鎿Q了邏輯運(yùn)算符。Python 為其某些運(yùn)算符提供了替代實(shí)現(xiàn),并允許您為新數(shù)據(jù)類型重載它們。
盡管在 Python 中重載邏輯運(yùn)算符的提議被拒絕了,但您可以為任何按位運(yùn)算符賦予新的含義。許多流行的庫(kù),甚至標(biāo)準(zhǔn)庫(kù),都利用了它。
內(nèi)置數(shù)據(jù)類型
Python 按位運(yùn)算符是為以下內(nèi)置數(shù)據(jù)類型定義的:
int
bool
set?和?frozenset
dict?(自 Python 3.9 起)
這不是一個(gè)廣為人知的事實(shí),但按位運(yùn)算符可以從集合代數(shù)執(zhí)行操作,例如并集、交集和對(duì)稱差,以及合并和更新字典。
注意:在撰寫(xiě)本文時(shí),Python 3.9尚未發(fā)布,但您可以使用Docker或pyenv偷看即將推出的語(yǔ)言功能。
當(dāng)a和b是 Python 集時(shí),則按位運(yùn)算符對(duì)應(yīng)以下方法:
它們幾乎做同樣的事情,因此使用哪種語(yǔ)法取決于您。除此之外,還有一個(gè)重載的減號(hào)運(yùn)算符 (?-),它實(shí)現(xiàn)了兩個(gè)集合的差值。要查看它們的實(shí)際效果,假設(shè)您有以下兩組水果和蔬菜:
>>>
>>> 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'}
它們共享一個(gè)難以分類的共同成員,但它們的其余元素是不相交的。
需要注意的一件事是 immutable?frozenset(),它缺少就地更新的方法。但是,當(dāng)您使用它們的按位運(yùn)算符對(duì)應(yīng)項(xiàng)時(shí),含義略有不同:
>>>
>>> const_fruits = frozenset({"apple", "banana", "tomato"}) >>> const_veggies = frozenset({"eggplant", "tomato"}) >>> const_fruits.update(const_veggies) Traceback (most recent call last): File "", line 1, in
frozenset()當(dāng)您使用按位運(yùn)算符時(shí),它看起來(lái)并不是那么一成不變,但問(wèn)題在于細(xì)節(jié)。以下是實(shí)際發(fā)生的情況:
const_fruits = const_fruits | const_veggies
它第二次起作用的原因是您沒(méi)有更改原始的不可變對(duì)象。相反,您創(chuàng)建一個(gè)新變量并再次將其分配給同一個(gè)變量。
Pythondict僅支持按位 OR,其工作方式類似于聯(lián)合運(yùn)算符。您可以使用它來(lái)更新字典或?qū)蓚€(gè)字典合并為一個(gè)新字典:
>>>
>>> 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)
按位運(yùn)算符的增強(qiáng)版本等效于.update()。
第三方模塊
許多流行的庫(kù),包括NumPy、pandas和SQLAlchemy,都為它們的特定數(shù)據(jù)類型重載了按位運(yùn)算符。這是您最有可能在 Python 中找到按位運(yùn)算符的地方,因?yàn)樗鼈儾辉俳?jīng)常以其原始含義使用。
例如,NumPy 以逐點(diǎn)方式將它們應(yīng)用于矢量化數(shù)據(jù):
>>>
>>> import numpy as np >>> np.array([1, 2, 3]) << 2 array([ 4, 8, 12])
這樣,您無(wú)需手動(dòng)將相同的按位運(yùn)算符應(yīng)用于數(shù)組的每個(gè)元素。但是你不能用 Python 中的普通列表做同樣的事情。
pandas 在幕后使用 NumPy,它還為其DataFrame和Series對(duì)象提供了按位運(yùn)算符的重載版本。但是,它們的行為與您期望的一樣。唯一的區(qū)別是他們?cè)跀?shù)字的向量和矩陣上而不是在單個(gè)標(biāo)量上做他們通常的工作。
使用賦予按位運(yùn)算符全新含義的庫(kù),事情會(huì)變得更有趣。例如,SQLAlchemy 提供了一種用于查詢數(shù)據(jù)庫(kù)的簡(jiǎn)潔語(yǔ)法:
session.query(User) \ .filter((User.age > 40) & (User.name == "Doe")) \ .all()
按位 AND 運(yùn)算符 (?&) 最終將轉(zhuǎn)換為一段SQL查詢。然而,這不是很明顯,至少對(duì)我的IDE 來(lái)說(shuō)不是,當(dāng)它在這種類型的表達(dá)式中看到它們時(shí),它會(huì)抱怨按位運(yùn)算符的非pythonic使用。它立即建議用&邏輯替換每個(gè)出現(xiàn)的and,不知道這樣做會(huì)使代碼停止工作!
這種類型的運(yùn)算符重載是一種有爭(zhēng)議的做法,它依賴于您必須預(yù)先了解的隱式魔法。一些編程語(yǔ)言(如 Java)通過(guò)完全禁止運(yùn)算符重載來(lái)防止這種濫用。Python 在這方面更加自由,并且相信您知道自己在做什么。
自定義數(shù)據(jù)類型
要自定義 Python 的按位運(yùn)算符的行為,您必須定義一個(gè)類,然后在其中實(shí)現(xiàn)相應(yīng)的魔術(shù)方法。同時(shí),您不能為現(xiàn)有類型重新定義按位運(yùn)算符的行為。運(yùn)算符重載僅適用于新數(shù)據(jù)類型。
以下是讓您重載按位運(yùn)算符的特殊方法的簡(jiǎn)要概述:
您不需要定義所有這些。例如,要使用稍微更方便的語(yǔ)法將元素附加到deque,僅實(shí)現(xiàn).__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'])
這個(gè)用戶定義的類包裝了一個(gè)雙端隊(duì)列以重用它的實(shí)現(xiàn),并用兩個(gè)額外的方法來(lái)擴(kuò)充它,這些方法允許將項(xiàng)目添加到集合的左端或右端。
最不重要的位隱寫(xiě)術(shù)
哇,要處理的事情太多了!如果您仍在撓頭,想知道為什么要使用按位運(yùn)算符,那么請(qǐng)不要擔(dān)心。是時(shí)候以有趣的方式展示您可以用它們做什么了。
要跟隨本節(jié)中的示例,您可以通過(guò)單擊下面的鏈接下載源代碼:
獲取源代碼:?單擊此處獲取源代碼,您將在本教程中用于了解 Python 的按位運(yùn)算符。
您將學(xué)習(xí)隱寫(xiě)術(shù)并將此概念應(yīng)用于在位圖圖像中秘密嵌入任意文件。
密碼學(xué)與隱寫(xiě)術(shù)
密碼學(xué)是將一條消息變成只有擁有正確密鑰的人才能讀取的消息。其他人仍然可以看到加密的消息,但對(duì)他們來(lái)說(shuō)沒(méi)有任何意義。密碼學(xué)的最初形式之一是替代密碼,例如以朱利葉斯·凱撒命名的凱撒密碼。
隱寫(xiě)術(shù)類似于密碼術(shù),因?yàn)樗€允許您與所需的受眾共享秘密消息。然而,它沒(méi)有使用加密,而是巧妙地將信息隱藏在不引起注意的介質(zhì)中。示例包括使用隱形墨水或?qū)戨x合體,其中每個(gè)單詞或行的第一個(gè)字母形成一個(gè)秘密信息。
除非您知道秘密消息被隱藏以及恢復(fù)它的方法,否則您可能會(huì)忽略運(yùn)營(yíng)商。您可以將這兩種技術(shù)結(jié)合起來(lái)更安全,隱藏加密消息而不是原始消息。
有很多方法可以在數(shù)字世界中走私秘密數(shù)據(jù)。特別是攜帶大量數(shù)據(jù)的文件格式,例如音頻文件、視頻或圖像,非常適合,因?yàn)樗鼈優(yōu)槟峁┝撕艽蟮墓ぷ骺臻g。例如,發(fā)布受版權(quán)保護(hù)的材料的公司可能會(huì)使用隱寫(xiě)術(shù)為單個(gè)副本添加水印并追蹤泄漏源。
下面,您將把秘密數(shù)據(jù)注入到一個(gè)普通的bitmap 中,這在 Python 中很容易讀寫(xiě),不需要外部依賴。
位圖文件格式
位圖一詞通常指的是Windows 位圖(?.bmp) 文件格式,它支持幾種表示像素的替代方法。為了讓生活更輕松,您將假設(shè)像素以 24 位未壓縮RGB(紅色、綠色和藍(lán)色)格式存儲(chǔ)。一個(gè)像素將具有三個(gè)顏色通道,每個(gè)通道可以保存從 0?10到 255?10 的值。
每個(gè)位圖都以文件頭開(kāi)頭,其中包含圖像寬度和高度等元數(shù)據(jù)。以下是一些有趣的字段及其相對(duì)于標(biāo)題開(kāi)頭的位置:
您可以從該標(biāo)頭推斷相應(yīng)的位圖寬 1,954 像素,高 1,301 像素。它不使用壓縮,也沒(méi)有調(diào)色板。每個(gè)像素占用 24 位或 3 個(gè)字節(jié),原始像素?cái)?shù)據(jù)從偏移量 122?10開(kāi)始。
您可以以二進(jìn)制模式打開(kāi)位圖,尋找所需的偏移量,讀取給定的字節(jié)數(shù),然后像以前一樣使用反序列化它們: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("
請(qǐng)注意,位圖中的所有整數(shù)字段都以 little-endian 字節(jié)順序存儲(chǔ)。
您可能已經(jīng)注意到標(biāo)頭中聲明的像素字節(jié)數(shù)與圖像大小導(dǎo)致的像素字節(jié)數(shù)之間存在微小差異。當(dāng)您乘以 1,954 像素 × 1,301 像素 × 3 字節(jié)時(shí),您會(huì)得到一個(gè)比 7,629,064 少 2,602 字節(jié)的值。
這是因?yàn)橄袼刈止?jié)用零填充,因此每一行都是四字節(jié)的倍數(shù)。如果圖像的寬度乘以三個(gè)字節(jié)恰好是四的倍數(shù),則不需要填充。否則,在每一行的末尾添加空字節(jié)。
注意:為避免引起懷疑,您需要通過(guò)跳過(guò)空字節(jié)來(lái)考慮該填充。否則,對(duì)于知道要尋找什么的人來(lái)說(shuō),這將是一個(gè)明顯的贈(zèng)品。
位圖倒置存儲(chǔ)像素行,從底部而不是頂部開(kāi)始。此外,每個(gè)像素都以有點(diǎn)奇怪的 BGR 順序而不是 RGB 序列化為顏色通道向量。然而,這與隱藏秘密數(shù)據(jù)的任務(wù)無(wú)關(guān)。
按位捉迷藏
您可以使用按位運(yùn)算符將自定義數(shù)據(jù)分布在連續(xù)的像素字節(jié)上。這個(gè)想法是用來(lái)自下一個(gè)秘密字節(jié)的位覆蓋每個(gè)中的最低有效位。這將引入最少的噪聲,但您可以嘗試添加更多位以在注入數(shù)據(jù)的大小和像素失真之間取得平衡。
注意:使用最低有效位隱寫(xiě)術(shù)不會(huì)影響生成的位圖的文件大小。它將保持與原始文件相同。
在某些情況下,相應(yīng)的位將相同,導(dǎo)致像素值沒(méi)有任何變化。然而,即使在最壞的情況下,像素顏色的差異也只有百分之幾。這種微小的異常對(duì)人眼來(lái)說(shuō)仍然是不可見(jiàn)的,但可以通過(guò)使用統(tǒng)計(jì)數(shù)據(jù)的隱寫(xiě)分析檢測(cè)到。
看看這些裁剪的圖像:
左邊的一個(gè)來(lái)自原始位圖,而右邊的圖像描繪了一個(gè)處理過(guò)的位圖,嵌入的視頻存儲(chǔ)在最低有效位上。您看得出來(lái)差別嗎?
以下代碼將秘密數(shù)據(jù)編碼到位圖上:
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) ] )
對(duì)于每個(gè)字節(jié)的秘密數(shù)據(jù)和相應(yīng)的八個(gè)字節(jié)的像素?cái)?shù)據(jù),不包括填充字節(jié),它準(zhǔn)備一個(gè)要傳播的位列表。接下來(lái),它使用相關(guān)位掩碼覆蓋八個(gè)字節(jié)中每個(gè)字節(jié)中的最低有效位。結(jié)果被轉(zhuǎn)換為一個(gè)bytes()對(duì)象并分配回它最初來(lái)自的位圖部分。
要從同一個(gè)位圖解碼文件,您需要知道寫(xiě)入了多少秘密字節(jié)。您可以在數(shù)據(jù)流的開(kāi)頭分配幾個(gè)字節(jié)來(lái)存儲(chǔ)這個(gè)數(shù)字,或者您可以使用位圖標(biāo)頭中的保留字段:
@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("
這會(huì)跳轉(zhuǎn)到文件中的正確偏移量,將 Python 序列化為int原始字節(jié),并將它們寫(xiě)下來(lái)。
您可能還想存儲(chǔ)機(jī)密文件的名稱。由于它可以具有任意長(zhǎng)度,因此使用空終止字符串對(duì)其進(jìn)行序列化是有意義的,該字符串將位于文件內(nèi)容之前。要?jiǎng)?chuàng)建這樣的字符串,您需要將 Pythonstr對(duì)象編碼為字節(jié)并在末尾手動(dòng)附加空字節(jié):
>>>
>>> from pathlib import Path >>> path = Path("/home/jsmith/café.pdf") >>> path.name.encode("utf-8") + b"\x00" b'caf\xc3\xa9.pdf\x00'
此外,使用pathlib.
補(bǔ)充本文的示例代碼將使您可以使用以下命令對(duì)給定位圖中的機(jī)密文件進(jìn)行編碼、解碼和擦除:
$ 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
這是一個(gè)可運(yùn)行的模塊,可以通過(guò)調(diào)用其包含的目錄來(lái)執(zhí)行。您還可以根據(jù)其內(nèi)容制作可移植的 ZIP 格式存檔,以利用Python ZIP 應(yīng)用程序支持。
該程序依賴于文章中提到的標(biāo)準(zhǔn)庫(kù)中的模塊以及您以前可能沒(méi)有聽(tīng)說(shuō)過(guò)的其他一些模塊。一個(gè)關(guān)鍵模塊是mmap,它將 Python 接口暴露給內(nèi)存映射文件。它們讓您可以使用標(biāo)準(zhǔn)文件 API 和序列 API 來(lái)操作大文件。就好像文件是一個(gè)可以切片的大可變列表。
繼續(xù)使用附加到支持材料的位圖。它包含一個(gè)小驚喜給你!
結(jié)論
掌握 Python 按位運(yùn)算符可讓您在項(xiàng)目中擁有操作二進(jìn)制數(shù)據(jù)的最大自由。您現(xiàn)在知道它們的語(yǔ)法和不同的風(fēng)格以及支持它們的數(shù)據(jù)類型。您還可以根據(jù)自己的需要自定義他們的行為。
在本教程中,您學(xué)習(xí)了如何:
使用 Python按位運(yùn)算符操作單個(gè)位
以與平臺(tái)無(wú)關(guān)的方式讀取和寫(xiě)入二進(jìn)制數(shù)據(jù)
使用位掩碼將信息打包到單個(gè)字節(jié)上
在自定義數(shù)據(jù)類型中重載Python 按位運(yùn)算符
在數(shù)字圖像中隱藏秘密信息
【生長(zhǎng)吧!Python】有獎(jiǎng)?wù)魑幕馃徇M(jìn)行中:https://bbs.huaweicloud.com/blogs/278897
Python 面向?qū)ο缶幊?/p>
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。