Python程式碼閱讀(第21篇):將變數名稱轉換為蛇式命名風格

Python 程式碼閱讀合集介紹:為什麼不推薦Python初學者直接看項目源碼

本篇閱讀的程式碼實現將變數名稱轉換為蛇式命名風格(snake case)的功能。

本篇閱讀的程式碼片段來自於30-seconds-of-python

snake

from re import sub

def snake(s):
  return '_'.join(
    sub('([A-Z][a-z]+)', r' \1',
    sub('([A-Z]+)', r' \1',
    s.replace('-', ' '))).split()).lower()

# EXAMPLES
snake('camelCase') # 'camel_case'
snake('some text') # 'some_text'
snake('some-mixed_string With spaces_underscores-and-hyphens') # 'some_mixed_string_with_spaces_underscores_and_hyphens'
snake('AllThe-small Things') # "all_the_small_things"

snake函數使用正則表達式將字元串變形、分解成單詞,並加上_作為分隔符組合起來。函數主要使用了re模組的substr.replacestr.splitstr.lowerstr.join。在正式分析snake函數的邏輯之前,先介紹下其中使用到的其他函數的作用。

str.replace(old, new[, count])

返回字元串的副本,其中出現的所有子字元串old都將被替換為new 如果給出了可選參數count,則只替換前count次出現。

str.split(sep=None, maxsplit=-1)

返回一個由字元串內單片語成的列表,使用sep作為分隔字元串。 如果給出了maxsplit,則最多進行maxsplit次拆分(因此,列表最多會有maxsplit+1個元素)。 如果maxsplit未指定或為-1,則不限制拆分次數(進行所有可能的拆分)。

如果sep未指定或為None,則會應用另一種拆分演算法:連續的空格會被視為單個分隔符,開頭和結尾如果包含空格的話,將不會拆分出空字元串。 因此,使用None拆分空字元串或僅包含空格的字元串將返回 []

>>> '1 2 3'.split()
['1', '2', '3']
>>> '1 2 3'.split(maxsplit=1)
['1', '2 3']
>>> '   1   2   3   '.split()
['1', '2', '3']

str.join(iterable)

返回一個由iterable中的字元串拼接而成的字元串。

str.lower()

返回原字元串的副本,其所有區分大小寫的字元均轉換為小寫。

re.sub(pattern, repl, string, count=0, flags=0)

返回通過使用repl替換在string最左邊非重疊出現的pattern而獲得的字元串。 如果樣式沒有找到,則不加改變地返回stringrepl可以是字元串或函數。 向後引用像是\6會用樣式中第6組所匹配到的子字元串來替換。 例如下面的例子中第一組匹配到的是myfun,所以在替換的時候,\1使用myfun替換,所以在結果中\npy_後面接著的是myfun

帶有'r'前綴的字元串是原始字元串,反斜杠不必做任何特殊處理。 因此r」\n」表示包含'\''n'兩個字元的字元串,而"\n"則表示只包含一個換行符的字元串。

>>> re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
...        r'static PyObject*\npy_\1(void)\n{',
...        'def myfunc():')
'static PyObject*\npy_myfunc(void)\n{'

snake執行邏輯

首先分析一下snake函數最裡面的sub函數。先看下輸入參數。

strings.replace('-', ' ')將待轉換的字元串中的'-'使用' '替換。

pattern'([A-Z]+)',其中(...)表示他是一個組合,匹配括弧內的正則表達式,並在匹配完成之後,組合的內容可以被獲取,並可以在之後用\number轉義序列進行再次匹配或使用,例如上個例子中的\1'([A-Z]+)'的組合表示要匹配一個或多個大寫字母,並儘可能匹配出最長的子字元串。

replr' \1',代表使用組合匹配出來的字元串前增加一個空格,替換匹配出來的字元串。例如'abcDEF'經過匹配和替換將變成'abc DEF'sub('([A-Z]+)', r' \1', 'abcDEF') # 'abc DEF'

因此,snake函數最裡面的sub函數的輸出是將原始字元串中的'-'使用' '替換,再匹配字元串中的一個或多個連續的大些字母,在前面增加一個空格。例如原始字元串是'abc-abcDEF-ABc'經過第一個sub函數轉換後變成'abc abc DEF ABc'(注意'ABc'前面有兩個空格)。

接下來再分析一下第二層的sub函數。還是先看一下輸入參數。

string是上個sub的輸出,在前面的例子中,是'abc abc DEF ABc'(注意'ABc'前面有兩個空格)。

pattern'([A-Z][a-z]+)'。它也是一個組合,表示要匹配一個大寫字母后面跟著一個或多個小寫字母的形式,並儘可能匹配出最長的子字元串。

repl還是r' \1',代表使用組合匹配出來的字元串前增加一個空格,替換匹配出來的字元串。

因此,第二層sub的輸出是簡單的匹配一個大寫字母后面跟著一個或多個小寫字母的形式,在前面加一個空格。繼續使用前面的例子,這層的輸入字元串是'abc abc DEF ABc'(注意'ABc'前面有兩個空格),輸出是'abc abc DEF A Bc'(注意'A'前面有兩個空格)。

然後snake函數將第二層sub輸出的字元串使用str.split函數分成字元串列表。再將得到的字元串列表使用'-'作為分隔符組合起來。最後使用str.lower將組合後的字元串轉換成小寫。延續上面的例子,最終輸出的字元串為:'abc_abc_def_a_bc'