天天看點

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    前幾天,在我閑逛的時候,發現了一行有趣的Python代碼:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

  這種不合正常的表達式不是那麼的清晰易懂,實際上這個字元串格式操作中将會使用port目前的值替換兩處{port},它們中間用一個冒号分隔,然後再指派給port。是以如果port目前的值是“foo”,指派完之後port最新的值是“foo:foo”。

    一些人當然建議使用更有可讀性的f-strings格式化字元串:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

   但是使用這個表達式的人說,他們能夠使用的Python版本不夠新(f-strings在Python3.6才開始引入)。是以我建議使用這種方式,它在Python2.6以來的所有版本都能運作:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    然後我開玩笑的表示如果有需要的話,我可以編寫可讀性更差的代碼實作上面代碼功能。有人回複說讓我把代碼編寫出來試試,下面我就花一些時間講一下如何實作它。然後再優化一下代碼,最終的代碼如下:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    運作下試試,它将列印出“foo:foo”。或者把port定義的初始值改變為任意你想要的值,再運作一下試試。

    在實際代碼中當然不會使用這種代碼。能夠寫這樣的代碼并不是一個有用的技能(這同樣适用于寫出可讀性更差的代碼,通過更加深入地使用任何技術或者使用其他方式使代碼的實際作用模糊不清,這并不是那麼困難)。但是它有些愚蠢和搞笑,寫這些代碼對進階開發者來說是個不錯的放松方式。要了解它為什麼這樣運作,以及輸出如何生成,探索Python的一些知識,并在實際中運用才是有用的。

    是以讓我們來推敲下這段代碼。

到底發生了什麼

    上面大量的代碼和我最初建議使用的port = "{0}:{0}".format(port)做的事情完全一緻。它隻是建構字元串,但是方法調用和參數處理使用了非常迂回的方式。

隐藏方法調用

    Python的getattr()函數讓你傳遞你進去一個對象和一個屬性名(字元串),然後它傳回給你對象的屬性。比如x.y和getattr(x, 'y')的作用是一緻的。這對探查一個對象的屬性(特别是和hasattr()搭配使用時)是有用的,并且對于處理一個不存在的屬性它也提供了一個簡潔的文法:你可以向getattr()傳遞第三個參數,這樣如果在對象上不存在屬性,就會傳回這個值。

    是以"{0}:{0}".format(port)可以被重寫為getattr("{0}:{0}", "format")(port)。你永遠不會想在實際代碼中這樣做,如果你知道Python的str類型有一個format方法,你可以直接調用它,但是可讀性在實際中才考慮,不是這裡所要考慮的。

一個非常啰嗦的角色

    現在讓我們使用格式化字元串。"{0}:{0}"這樣太簡單,看一眼就能了解。是以讓我換一種方式生成它。

    我們要做的是建構

":"

 和兩個獨立的"{0}",再把他們結合起來。是以首先需要獲得一個冒号字元。如果隻是寫

":"

 ,這太簡單,是以通過更複雜的方式擷取它,甩掉馬虎的讀者。

    一種方式是通過位元組值生成它,然後将它解碼成一個字元串。在ASCII碼中,這個字元用1個位元組表示,它的值為十進制的58。是以我們需要一個方法來生成數字58。當編寫這段代碼時,這是我最後做的,缺乏新穎的想法,是以隻用了位運算。你有很多方式獲得58,但是我使用63 ^ 5(這是異或操作)。為了更有迷惑性,63用十六進制表示,5用二進制表示(Python支援整數字面值用二進制、八進制和十六進制表示)。63特别好的一點在于它是2**6 - 1(2的6次方減1),2的幂次方或者是2的幂次方減1的數字能夠很好地引起讀者的注意。

    然後将它從一個位元組值解碼轉成單字元的字元串。從ACSII碼解碼太明顯,是以我們使用 Windows-1252解碼,一種很唬人編碼方式,因為它即普通又特别,就像周末淩晨3點拉響你的尋呼機一樣。代碼如下:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    兩種擷取單字元的字元串":"方法都可以使用。哪些{0}格式串又該怎樣處理呢?

未定義浮點數準備就緒

    為了擷取到完整的"{0}:{0}"格式字元串,我們仍然需要那兩個位置占位符。一個技巧是我們可以通過其他方式來表示字元串"{0}"。Python可以做到:通過set類型,它是一種包含唯一值的集合類型(比如,一個從清單[1, 1, 1, 1]生成的集合隻包含一個元素)。盡管有一個内置的set()函數可以用來從可疊代對象建立一個集合,但是你也可以通過字面量文法,用大括号包裝内容。這非常有用,因為字面量文法也是集合預設的字元串表示方式,是以如果我們建立一個隻包含整數0的集合,它的字元串表示是"{0}",這正是我們想要的。

    如果僅僅字面上寫出集合{0},這就太明顯了。寫set([0])也是如此。那麼我們怎麼能得到整數0呢?

    這裡我們求助于Python的一些細枝末節:Python最初沒有bool類型,是以大多數程式員使用整數0和1作為替代。當布爾類型最終在Python2.3引入時,它是以與舊的整數約定相容的方式完成的:bool類型是int類型的子類,兩個執行個體False和True的整數值分别為0和1。你可以将他們插入到任何需要整數的位置,他們同樣能夠正常運作。

    是以如果我們需要一個0,我們可以通過找到一些非真值的布爾表達式,然後狡猾地将它轉換成一個普通的int類型。

    我所選擇的布爾表達式是math.nan.is_integer()。常數math.nan提供了IEEE 754浮點數規定的NaN值,它是Python的浮點數類型。NAN是某些類型程式設計的禍根,因為你可能會在浮點數運算時生成它,并導緻異常。

    但它是浮點數執行個體,這意味着它具有Python浮點數的常見方法和屬性,包括is_integer(),用于判斷浮點數是否等于某個整數。例如,如果my_float = 2.0,然後my_float.is_integer()為真值(但是如果my_float = 2.01,則為非真值)。因為NaN絕不可能是整數,是以math.nan.is_integer()将會給我們傳回一個布爾值False。

    把它轉換成一個普通的int,這就不太明顯了。内置函數sum()很适合;它會欣然地為我們将bool清單中的值加和(bool執行個體是整數,支援算術運算),傳回值為int類型。是以這樣寫sum([nan.is_integer()]) == sum([False]) == sum([0]) == 0。我們将其輸入set()(再次在清單中包裝它,因為set()需要一個可疊代的),然後使用repr()處理它(repr()類似于str(),但這樣更好調試,可用來重構列印的内容;我故意寫的含糊不清,以掩蓋使用str和repr的真實意圖)。代碼如下:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    上面表達式計算後為“{0}”,意味着我們可以使用它來代替文字字元串。

現在這裡有兩個“{0}”!

    但是在我們的格式化字元串中,我們不隻需要一個"{0}"占位符。我需要兩個,一個在冒号的另一側。我們可使用*操作符,它不隻做乘法運算。

    Python支援運算符重載,這意味着運算符及流控制協定,通過定義特殊名稱方法在任何參數類型上都可實作重載。并且Python的内建類型用到了這一特性。* 運算符重載的實作和行為取決于操作數的類型;當兩個操作數都是int類型時,表示兩者相乘。但是當一個是int類型,另一個是序列類型時,它對序列每個元素乘積生成新的序列。你試一下:在Python解釋器中輸入[1,2,3] * 2。

  (如果你在沒有實作的類型上使用操作符時,比如兩個字元串類型,會報TypeError錯誤)

    是以,我們可以使用*運算符将占位符字元串的一個執行個體轉換為任意數量的占位符字元串。我們想要兩個,我們可以用清單的形式,通過執行[“{0}”]*2,得到[“{0}”,“{0}”]。我們已經有一個表達式的計算結果是{0}。我們可以把它括在括号裡,然後附加上 * 2,但是這有什麼樂趣呢?

     為了生成2,使用異或運算符,但這次是通過operator.xor函數(标準庫中的 operator 子產品提供了Python的封裝在函數中的标準操作符;operator.xor(x, y) 會傳回x ^ y異或運算後的結果)。很多表達式都會這樣做,但是我選擇6 ^ 4,因為它讓我在代碼中嵌入了一個可疑的能夠轉移視線的東西。

     我解釋一下:程式員對“魔數”很敏感。2的幂很常見。6 ^ 4用兩個單獨的數字,聯合起來是64,這是2的幂。在這裡,我們看到實際的int 64—盡管在其八進制形式中指定為0o100—出現在代碼中。把它傳給str(),則傳回字元串“64”。

     Python的字元串是序列類型,這意味着對其他序列(如清單和元組)的操作也對字元串有效。使用這個特性對序列“64”進行操作,将字元轉換為整數,再傳給 operator.xor (我們是為了得到6 ^ 4 = 2)。

     拼湊起來:[i for i in map(int, "64")]。内置的map()接受一個函數和一個序列,傳回函數應用于序列中的每項的結果。這裡傳回[6,4],把它提供給 operator.xor() 來執行6 ^ 4,最後得到想要的2。

     為此,我們需要使用*來觸發Python的可疊代解包行為:some_function(*arg),其中的arg是一個清單或其他可疊代的對象,把 arg “解包”到一系列單獨的參數中,然後傳遞給some_function。是以,如果arg = [1, 2, 3],那麼some_function(*arg)完全等價于some_function(1,2,3)。這也意味着兩個 * 用途不同。

     但這裡還有兩個技巧。一是我們不直接使用 map(int,…) 。相反,我們提供第二個可選的int類型參數,表明傳入的數字時使用什麼進制。這裡我使用了16進制,它不會影響将“6”和“4”轉換成整數的結果,但是讓我通過指定基數用十六進制數字 0x10 轉換成八進制數字 0o100。

     另一個技巧是,在 map() 中執行此操作需要lambda表達式,它定義一次性的匿名函數,該函數将接收的字元串傳遞給 int(),額外的進制參數。當然那個匿名函數需要命名它的參數。那麼用什麼名字呢?為什麼,當然是 іn。現在,in是一個Python操作符,是以它是保留符号;在Python中,給任何東西命名都是合法的 - 函數、類、變量、參數,你都可以用in來命名。Python允許你在辨別符名稱中使用範圍廣泛的Unicode,這裡禁用的拉丁字母 i ,這個外形相似的西裡爾字母會替代保留關鍵字in,這在Python中是完全合法的,建立了一個名稱的外形看起來和内置的in操作符一模一樣,但至關重要的是它不是in操作符。

     是以我們代碼這樣編寫:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

     這是一種非常迂回的方式實作 ["{0}"] * 2,這就得到了["{0}","{0}"]。我們幾乎已經能夠編寫“{0}:{0}”.format了!

說到格式

     我們需要getattr("{0}:{0}", "format")。我們如何得到這個“format”呢?

     Python中還有一個叫做format()的内置函數。在本例中,format(x, y)與x.format(y)作用相同。這裡我們将format函數傳遞給str()。傳回的函數的字元串表示,即字元串“”。現在需要提取字元串中的“format”,分三步:

  1. 使用Split()方法用空格拆分,生成[""]。
  2. 使用索引[-1](索引号-1指向清單或元組中的最後一項)來獲得“format>”。
  3. 提取除最後一個字元之外的所有字元:[:-1]。

     最終得到字元串“format”:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

     現在建構好了getattr("{0}:{0}", "format")序列。我們需要調用它(字元串“{0}:{0}”的format()方法)并傳入參數:port。

字元組成的字元串

     我們最終将需要實際的變量port,但是我們可以通過首先生成字元串“port”來實作。再說一遍,我們不想讓它太明顯!是以,我們将以一種更複雜的方式,重複使用上面得到字元串“format”的技巧。我們将不再尋找包含精确的子字元串“port”的字元串表示,而是查找包含單個字元“p”、“o”、“r”、“t”的字元串表示,然後将它們連接配接起來。

     這就是repr(repr)、repr(repr)、repr(str)、str(repr)序列在最後所做的:repr函數的字元串表示為“”,而str函數的字元串表示為“”。是以我們要尋找那些特定字元首次出現的字元串中的索引:

  1. p在repr表示中第一個索引為21。
  2. o在repr表示中第一個索引為16。
  3. r在str表示中第一個索引為10。
  4. t在repr表示中第一個索引為5。

     我在str和repr的表示中找到一組整數索引,這将按順序(即可升也可降序)給出所要的字元。上述索引按降序排列:21、16、10、5。轉為升序5、10、16、21,然後把它插入OEIS,找到某個數學函數按順序生成這些整數。查找結果中第一個是序列A172334,它很容易計算,從0、5、10、16、21、... 開始,隻要去掉第一個值。

     這就是floor()和sqrt()函數的作用;它生成了A172334序列。然後我們将擷取序列的第2到第5個值,這些值是我們想要用來作為str和repr的字元串表示的索引,但是以相反的順序(是以将它們提供給Python的reverse()函數以獲得正确的順序)。

     它看起來又像使用一個保留名生成序列,在這個例子中保留名是for:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

     這次的訣竅也是使用西裡爾字母(這次替換拉丁字母o)。

     但是range函數調用怎麼辦?現在,您可能已經掌握了足夠的技巧,可以計算出它正在執行range(1,5)以獲得所需的值(我們需要包含數學序列的下标1-4)。但它還是使用了bool類型是int類型的子類型的技巧,這就是如何獲得取值範圍起始位置1的方法。對于5從哪裡截取,我們的做法是将31(十六進制0x1f)傳遞給bin,bin傳回它的二進制字元串(“0b11111”)。然後我們使用.count()來計算字元1在該字元串中出現的次數:它是5。為了獲得該操作的單字元字元串“1”,我們使用repr(--True),它再次使用True作為值為1的整數,并對其進行兩次取反(第一次取反後值為-1,第二次取反則将其轉換回1)。

     最後,我們需要真正的提取出正确的字元。我們在這裡生成了兩個序列:一個是整數清單(21、16、10、5),另一個是字元串清單,在這些整數給出的索引中可以找到字元串中所需的字元。這些序列具有相同的長度,是以我們可以将它們傳遞給zip(),建立一個将它們組合在一起的元組清單。生成的元組清單是這樣:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

     這表明字元串對應的索引,再用operator.itemgetter擷取字元。

     itemgetter函數是内置操作符的另一個包裝器:在本例中,itemgetter(x)(y)相當于y[x]。我們再次使用外形相似字元的技巧做出具有令人迷惑名字的循環變量(在本例中看起來像保留字 not),還有布爾類型是整型子類型的技巧(我們想要的東西在我們生成的元組中索引的位置為0和1,是以布爾類型能夠很好地處理)。代碼如下:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

     這段代碼生成的序列["p", "o", "r", "t"]。

     然後我們将在一個空字元串上使用.join():"".join(["p", "o", "r", "t"])的結果是"port"。

     為了生成空字元串,我們使用str(str)[1:1],乍一看它似乎做了一些事情(因為它使用的是非零索引),但實際上隻是取了字元串str(str)的一個長度為零的切片。

我們的進展很快

     最後,我們需要将字元串“port”轉換為對實際變量port的引用。有幾種方法可以使用,當我選擇vars()時,我完全沒有使它變的充滿迷惑性。這是一個函數,它将傳回一個本地定義變量的字典(當無參數調用時),或者一個對象的屬性/方法的字典(當使用一個對象作為參數調用時)。你還可以使用locals()(它隻能擷取局部變量的字典,不支援處理對象),甚至globals()(全局變量的字典)。vars()["port"]實際上是本地變量port。

     由于我們已經生成了字元串“port”,我們可以把它作為一個主鍵傳遞給vars()傳回的字典,然後最終完整的代碼如下:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

    它等價于:

python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

但是,為什麼要這樣做?

     再次強調,“編寫模糊代碼”作為一種通用技能,在大多數情況下并不是特别有用。但是:考慮一下Python和它的标準庫中有多少不常用和怪異的特性能實作這個功能。盡管它不是很好的代碼(實際上非常糟糕!),但它至少可以讓具有幾乎任何經驗水準的Python程式員學到一些東西。

     即使不去學習更多關于Python的知識,上面使用的一些技術對實際場景的代碼也有安全方面的影響,并且可能會讓你有點緊張,使你聯想到“如果有人對我的應用程式做了那樣的事情會怎麼樣?” 我想這就是有像國際模糊C競賽這樣的事物存在的原因。當我講web應用程式安全課程時,我經常提到JSFuck,它其中一種技術,将JS代碼轉換成隻有6個字元的字母表編寫的等價代碼。以Django為例,Web架構無法保護您不受這些技術的影響,是以了解這些技術可能是有用的知識。

     最終,我這樣做的原因是因為我覺得這很有趣。我喜歡程式設計,但這不是我通常為了娛樂而做的事情;在我的工作和開源社群中,我花了大量的時間編寫代碼。然而,每隔一段時間,我确實會為了這樣的樂趣而編寫一些無用的代碼(而不是出于憤怒編寫一些無用的代碼,這種情況偶爾也會發生)。

英文原文:https://www.b-list.org/weblog/2020/jan/20/fun/ 譯者:穆勝亮
python 01清單異或_見識一下,Python代碼可以寫的多麼亂?

繼續閱讀