修改變量名,簡單有效地提高代碼質(zhì)量!
請快速說出以下代碼的功能:
- for i in range(n):
- for j in range(m):
- for k in range(l):
- temp_value = X[i][j][k] *12.5
- new_array[i][j][k] = temp_value+ 15
很難,對吧?要想對這段代碼進行修改或調(diào)試,除非知道作者在想什么,否則將難以實現(xiàn)。即使是作者本人,在編寫這段代碼幾天后也會忘記其用途,因為變量名和“魔數(shù)”(magic numbers)并不能幫助記憶代碼的功能。
使用數(shù)據(jù)科學代碼時,類似于上面(或者更糟)的示例很常見:代碼中含有如x、y、xs、x1、x2、tp、tn、clf、reg、xi、yi、ii這樣的變量名和許多未命名的常量值。坦率地說,數(shù)據(jù)科學家(包括本人)并不擅長于命名變量。
很多人經(jīng)歷過從為一次性分析編寫研究導向的數(shù)據(jù)科學代碼到編寫生產(chǎn)水平代碼的過程,所以不得不摒棄從數(shù)據(jù)科學書籍、課程和實驗室中獲得的實踐來改進編程方式。可實際應用的機器學習代碼與數(shù)據(jù)科學家的編程方法有許多不同之處,但本文將從兩個影響力較大的常見問題開始:
- 無用的/混淆的/不明確的變量名
- 未命名的“魔幻”常數(shù)
這兩個問題都導致了數(shù)據(jù)科學研究(或Kaggle項目)和生產(chǎn)機器學習系統(tǒng)之間的脫節(jié)。是的,你可以在只運行一次代碼的Jupyter Notebook中僥幸逃脫這些問題,但是當任務(wù)中關(guān)鍵的機器學習管道需要每天準確無誤地運行數(shù)百次時,編寫可讀以及可理解的代碼就十分必要了。幸運的是,數(shù)據(jù)科學家可以采用軟件工程中的優(yōu)秀實踐,本文也將對其進行介紹。
注意:本文主要討論Python,因為它是目前為止工業(yè)數(shù)據(jù)科學中應用最廣泛的語言。在Python中:
- 變量名/函數(shù)名小寫并且用下劃線隔開
- 命名常數(shù)的名稱全部大寫
- 類的名稱采用駝峰式大小寫命名規(guī)則
命名變量
命名變量時要記住三個基本原則:
- 變量名必須描述變量所表示的信息。變量名應該用詞明確,來體現(xiàn)變量代表的內(nèi)容。
- 代碼讀取的次數(shù)將多于編寫的次數(shù)。所以優(yōu)先考慮代碼的可讀性而不是編寫速度。
- 采用標準的命名規(guī)范,才可以做出一個全局決策,而不是做出多個局部決策。
在實踐中又如何呢?下面對變量名進行一些改進:
- x和y。如果多次閱讀了解的話會知道它們是特性和目標,但是對于閱讀代碼的其他開發(fā)人員來說,這可能并不明確。相反,要使用可以描述這些變量含義的名稱,如house_features和house_prices。
- value。value代表什么?它可以是velocity_mph, customers_served, efficiency, revenue_total。像value這樣的名稱并不能體現(xiàn)變量的用途,而且容易混淆。
- temp。即使只將變量用作臨時值存儲,也要給它一個有意義的名稱。這可能是用于轉(zhuǎn)換單位的值,因此在這種情況下,請明確說明:
- # Don't do this
- temp = get_house_price_in_usd(house_sqft, house_room_count)
- final_value = temp * usd_to_aud_conversion_rate
- # Do this instead
- house_price_in_usd = get_house_price_in_usd(house_sqft,
- house_room_count)
- house_price_in_aud = house_price_in_usd * usd_to_aud_conversion_rate
造成不良變量名的原因
命名變量的問題大多數(shù)來源于:
- 試圖縮短變量名
- 直接將公式轉(zhuǎn)寫為代碼
關(guān)于第一點,雖然像Fortran這樣的語言確實限制了變量名的長度(6個字符以內(nèi)),但現(xiàn)代編程語言沒有限制,所以不要強迫自己使用縮寫。也不要使用過長的變量名,但是如果一定要做出選擇,要力求可讀性。
關(guān)于第二點,當編寫方程或使用模型時——這是學校忘記強調(diào)的一點——記住字母或輸入代表實際值!
下列是一個同時犯了兩種錯誤的例子及其改正方式。假設(shè)有一個從模型中得出的多項式能夠求出一所房子的價格。開發(fā)者可能會想直接用代碼編寫數(shù)學公式:
- temp = m1 * x1 + m2 * (x2 ** 2)
- final = temp + b
這段代碼看起來像是由機器為機器編寫的。雖然計算機最終會運行此代碼,但人類進行讀取的次數(shù)更多,所以編寫能讓人類理解的代碼!
要做到這一點,并不需要考慮公式本身——怎么做——而需要考慮建模的真實對象——是什么。下面是完整的等式(這能很好地測試讀者是否理解了模型):
- house_price = price_per_room * rooms + \
- price_per_floor_squared *(floors ** 2)
- house_pricehouse_price = house_price + expected_mean_house_price
如果命名變量時遇到困難,意味著對模型或代碼的不了解。編寫代碼是為了解決實際問題,所以需要理解模型采集的目標。描述性變量名有助于在比公式更高的抽象級別工作,以及幫助開發(fā)者關(guān)注問題本身。
其他注意事項
命名變量時重要的一點是一致性計數(shù)。使用一致的變量名可以減少命名時間,增加解決問題的時間,尤其是添加復合性變量名時。
1. 變量名中的聚合
讀者已經(jīng)了解使用描述性名稱的基本思想,將 xs更改為distances,將e 更改為efficiency,將v更改為 velocity。那么求平均速度該使用什么樣的變量名?是average_velocity, velocity_mean 還是velocity_average?下列步驟可以解決這個問題:
- 首先,確定常用縮寫:avg表示平均值,max表示最大值,std表示標準差等等。確保團隊的所有成員達成一致,并把這些記錄下來。
- 把縮寫放在變量名的末尾。將最具相關(guān)性的信息,即變量所描述的實體,放在開頭。
按照這些規(guī)則,聚合變量可能被命名為為velocity_avg, distance_avg, velocity_min,以及 distance_max。第2條可以依據(jù)個人情況酌情選擇。
當一個變量表示一個項目的數(shù)量時,就會出現(xiàn)一個棘手的問題。如要使用building_num,那么它是指建筑物的總數(shù),還是特定建筑物的某個索引值?為了避免歧義,使用building_count表示建筑物總數(shù),使用building_index表示特定建筑物。這也適于其他問題,如item_count和item_index。item_count也可用item_total代替。這種方法解決了歧義,并保持了將復合名稱添加在名稱末尾的一致性。
2. 循環(huán)索引
非常不幸,典型的循環(huán)變量已經(jīng)變成i、j和k。這可能是造成數(shù)據(jù)科學中最多錯誤和困擾的原因。將無說明性的變量名與嵌套循環(huán)結(jié)合起來(筆者見過使用ii、jj甚至iii的嵌套循環(huán)),就會產(chǎn)生不可讀、容易出錯的代碼。這一點可能有些爭議,但筆者從不使用i或任何其他單個字母作為循環(huán)變量,而是選擇描述迭代的內(nèi)容,例如
- for building_index in range(building_count):
- ....
或
- for row_index in range(row_count):
- for column_index inrange(column_count):
- ....
這格外適用于嵌套循環(huán),此時無需記憶i是代表行還是列,或者與j和k混淆。應該花更多腦力來思考如何創(chuàng)建最佳模型,而不是數(shù)組索引的具體順序。
(在Python中,如果不使用循環(huán)變量,則應使用下劃線“_”作為占位符。這樣就不會對是否使用了索引感到困惑。)
3. 其他需要避免的命名方式
- 避免在變量名中使用數(shù)字
- 避免易拼錯的單詞
- 避免使用不明確的字符
- 避免使用含義相似的變量名
- 避免使用縮寫
- 避免發(fā)音相似的變量名
應堅持優(yōu)先考慮可讀性而不是方便性這一原則。編程主要是為了與其他程序員進行溝通,因此請適當考慮團隊成員。
不使用魔數(shù)
魔數(shù)是指未命名的常量。它常被用于單位轉(zhuǎn)換,改變時間間隔或增加下標時:
- final_value = unconverted_value * 1.61
- final_quantity = quantity / 60
- valuevalue_with_offset = value + 150
- (這些變量名都很糟糕?。?nbsp;
魔數(shù)會導致大量的錯誤和混亂,因為:
- 只有作者本人知道魔數(shù)的含義
- 改變魔數(shù)的值需要查找它出現(xiàn)的所有位置,然后人工輸入新值
可以定義一個用于轉(zhuǎn)換的函數(shù)來代替魔數(shù)。這個函數(shù)接受一個未經(jīng)轉(zhuǎn)換的值以及一個轉(zhuǎn)換率作為參數(shù)。
- defconvert_usd_to_aud(price_in_usd,
- aud_to_usd_conversion_rate):
- price_in_aus = price_in_usd *usd_to_aud_conversion_rate
如果要在一個項目的許多函數(shù)中使用同一個轉(zhuǎn)換率,可以在某處定義一個命名常量。
- USD_TO_AUD_CONVERSION_RATE = 1.61
- price_in_aud = price_in_usd * USD_TO_AUD_CONVERSION_RATE
(在開始編寫這個項目之前,需要和其他組員約定usd代表美元,aud代表澳元。記住標準!)
下面是另一個例子:
- # Conversion function approach
- def get_revolution_count(minutes_elapsed,
- revolutions_per_minute):
- revolution_count = minutes_elapsed *revolutions_per_minute
- # Named constant approach
- REVOLUTIONS_PER_MINUTE = 60
- revolution_count = minutes_elapsed *REVOLUTIONS_PER_MINUT
使用在某處定義的命名常量使得改寫數(shù)值更加容易和一致。如果轉(zhuǎn)換率發(fā)生改變,無需搜索整個代碼庫來改變它每次出現(xiàn)時的值,因為它只在一處被定義。這也把常數(shù)的含義告訴了代碼的讀者。如果參數(shù)名能夠體現(xiàn)參數(shù)的內(nèi)容,函數(shù)參數(shù)也是一個可行的解決方式。
一個魔數(shù)缺陷的實例來自于筆者在大學時從事的某個研究項目。這個項目需要獲取每15分鐘更新的能量數(shù)據(jù)。沒有人想到這個數(shù)字可能會變化,于是團隊編寫了大量使用魔數(shù)15的函數(shù)(或者96,即每日觀測次數(shù))。這些函數(shù)運行得很好,直到開始以5分鐘和1分鐘的間隔獲取數(shù)據(jù)。整個團隊花費了幾周的時間來修改函數(shù),使它們能夠接受一個時間間隔作為參數(shù)。即使這樣,還是遇到了很多由于幾個月以來使用魔數(shù)而導致的錯誤。
真實的數(shù)據(jù)經(jīng)常改變,比如匯率每分鐘都在變化。強行使用具體的數(shù)值來編程意味著可能不得不花費大量時間重寫代碼和修復錯誤。編程中沒有“魔法”的位置,即使是在數(shù)據(jù)科學中也是如此。
標準和約定的重要性
使用標準的好處在于它們幫助開發(fā)者簡單地做出全局決策而不是許多的局部決策。無需在每次命名變量時選擇聲明的位置,而是在項目開始的時候做好決定,然后在整個項目中前后一致地使用這些變量。這樣做的目的是花費更少的時間在命名、格式和風格這類數(shù)據(jù)科學的非核心問題上,用更多的時間解決重要的問題(比如使用機器學習研究環(huán)境變化)。
習慣獨自工作的開發(fā)者可能很難意識到采用標準的優(yōu)越性。然而即使是獨自工作,也可以練習定義自己的規(guī)則并且一貫地使用它們。開發(fā)者將能夠少做一些瑣碎的決定,并且這也是為將來進行團隊開發(fā)工作做準備。在任何需要一人以上的項目中,標準都是必要的。
讀者可能會質(zhì)疑本文中的某些命名選擇,這無關(guān)緊要。更重要的是采用一組一貫的標準,而不是命名時具體使用的空間或者變量名的最大長度。關(guān)鍵是不再在偶然的難題中花費大量時間,而是專注于解決必然的問題。
結(jié)論
記住剛剛學到的,現(xiàn)在回到文章開頭的代碼:
- for i in range(n):
- for j in range(m):
- for k in range(l):
- temp_value = X[i][j][k] *12.5
- new_array[i][j][k] =temp_value + 150
然后使用描述性的變量名和符號常量對它進行修改。
- PIXEL_NORMALIZATION_FACTOR = 12.5
- PIXEL_OFFSET_FACTOR = 150
- for row_index in range(row_count):
- for column_index in range(column_count):
- for color_channel_index in range(color_channel_count):
- normalized_pixel_value = (
- original_pixel_array[row_index][column_index][color_channel_index]
- * PIXEL_NORMALIZATION_FACTOR
- )
- transformed_pixel_array[row_index][column_index][color_channel_index] =(
- normalized_pixel_value + PIXEL_OFFSET_FACTOR
- )
現(xiàn)在可以看出這段代碼是在規(guī)格化數(shù)組中的像素值,并且加上一個偏移量來建立一個新的數(shù)組(忽略這種實現(xiàn)的低效性!)。當這段代碼被交付給同事,他們將能夠讀懂和修改它。而且,當開發(fā)者回頭看這段代碼,試圖測試和修正錯誤,他們也將清楚地知道自己在做什么。
這個話題無聊嗎?或許有一點枯燥,但如果花費時間閱讀有關(guān)軟件工程的書目,會發(fā)現(xiàn)優(yōu)秀程序員和普通程序員的區(qū)別就在于重復使用這些無趣的技巧,比如好的變量名、短暫的工作周期、測試每一行代碼、重構(gòu)等等。這就是把代碼從實驗室級別提升到工業(yè)生產(chǎn)級別所需要的。并且一旦做到這一點,就能發(fā)現(xiàn)使用模型來改變現(xiàn)實生活中的決策非常有趣。
本文討論了一些改進變量名的方式。
需要牢記的所有重點:
- 變量名應該描述它所代表的對象
- 比起易編寫性更重視易讀性
- 在項目中使用一貫的標準,從而盡可能減輕瑣碎決定的識別難題。
特別要點:
- 使用描述性的變量名
- 使用函數(shù)參數(shù)或者命名常量而不是“魔數(shù)”
- 不要使用特定的機器學習縮寫
- 用變量名描述算式或模型的含義
- 把組合而成的變量名放在末尾
- 使用item_count而不是num
- 使用描述性的循環(huán)索引而不是i、j、k
- 在整個項目中采用統(tǒng)一的命名和格式規(guī)則