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