如何在 Python 中編寫干凈的代碼
↓推薦關(guān)注↓
在這篇文章中,我將分享如何在 Python 中編寫干凈的代碼及代碼編寫規(guī)則。
對(duì)于每個(gè)原則,我將提供小的代碼片段來(lái)更好地解釋原則,并向你展示如何處理事情以及如何不處理事情。
我希望這篇文章能為所有使用 Python 的人提供價(jià)值,但特別是激勵(lì)其他數(shù)據(jù)科學(xué)家編寫干凈的代碼。
有意義的名稱
這一部分應(yīng)該是顯而易見(jiàn)的,但許多開(kāi)發(fā)人員仍然沒(méi)有遵循它。
創(chuàng)建有意義的名稱!
每個(gè)人在閱讀你的代碼時(shí)都應(yīng)該直接理解發(fā)生了什么。不應(yīng)該需要內(nèi)聯(lián)注釋來(lái)描述您的代碼在做什么以及某些變量代表什么。
如果名稱是描述性的,那么應(yīng)該非常清楚函數(shù)在做什么。
讓我們看一個(gè)典型的機(jī)器學(xué)習(xí)示例:加載數(shù)據(jù)集并將其拆分為訓(xùn)練集和測(cè)試集:
import pandas as pd
from sklearn.model_selection import train_test_split
def load_and_split(d):
df = pd.read_csv(d)
X = df.iloc[:, :-1]
y = df.iloc[:, -1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
return X_train, X_test, y_train, y_test
大多數(shù)了解數(shù)據(jù)科學(xué)的人都知道這里發(fā)生了什么,他們也知道 X 是什么,y 是什么。但是對(duì)于新手來(lái)說(shuō)呢?
將 CSV 文件的路徑僅用 d 命名,這是一個(gè)好的做法嗎?
將特征命名為 X,將目標(biāo)命名為 y,這是一個(gè)好的做法嗎?
讓我們看一個(gè)更具有意義的名稱的例子:
import pandas as pd
from sklearn.model_selection import train_test_split
def load_data_and_split_into_train_test(dataset_path):
data_frame = pd.read_csv(dataset_path)
features = data_frame.iloc[:, :-1]
target = data_frame.iloc[:, -1]
features_train, features_test, target_train, target_test = train_test_split(features, target, test_size=0.2, random_state=42)
return features_train, features_test, target_train, target_test
這樣更容易理解。現(xiàn)在,即使是對(duì)于不熟悉 pandas 和 train_test_split 約定的人,也非常清楚該函數(shù)正在從 dataset_path 中列出的路徑加載數(shù)據(jù),從數(shù)據(jù)幀中檢索特征和目標(biāo),然后計(jì)算訓(xùn)練集和測(cè)試集的特征和目標(biāo)。
這些更改使代碼更易于閱讀和理解,特別是對(duì)于可能不熟悉機(jī)器學(xué)習(xí)代碼約定的人來(lái)說(shuō),其中特征大多以 X 命名,目標(biāo)以 y 命名。
但是請(qǐng)不要過(guò)度使用不提供任何附加信息的命名。
讓我們看另一個(gè)例子代碼片段:
import pandas as pd
from sklearn.model_selection import train_test_split
def load_data_from_csv_and_split_into_training_and_testing_sets(dataset_path_csv):
data_frame_from_csv = pd.read_csv(dataset_path_csv)
features_columns_data_frame = data_frame_from_csv.iloc[:, :-1]
target_column_data_frame = data_frame_from_csv.iloc[:, -1]
features_columns_data_frame_for_training, features_columns_data_frame_for_testing, target_column_data_frame_for_training, target_column_data_frame_for_testing = train_test_split(features_columns_data_frame, target_column_data_frame, test_size=0.2, random_state=42)
return features_columns_data_frame_for_training, features_columns_data_frame_for_testing, target_column_data_frame_for_training, target_column_data_frame_for_testing
當(dāng)你看到這段代碼時(shí),你有什么感覺(jué)?
有必要包含函數(shù)加載 CSV 嗎?以及數(shù)據(jù)集路徑是指向 CSV 文件的路徑嗎?
這段代碼包含太多沒(méi)有提供任何額外信息的信息。它反而使讀者分心。
因此,添加有意義的名稱是在描述性和簡(jiǎn)潔之間取得平衡的行為。
函數(shù)
現(xiàn)在讓我們來(lái)看看函數(shù)。
函數(shù)的第一個(gè)規(guī)則是它們應(yīng)該很小。函數(shù)的第二個(gè)規(guī)則是它們應(yīng)該比那更小 [1]。
這一點(diǎn)非常重要!
函數(shù)應(yīng)該很小,不超過(guò) 20 行。如果函數(shù)中有大塊占用大量空間的代碼,請(qǐng)將它們放入新的函數(shù)中。
另一個(gè)重要原則是函數(shù)應(yīng)該做一件事。而不是更多。如果它們做了更多,請(qǐng)將第二個(gè)事情分離到新的函數(shù)中。
現(xiàn)在讓我們?cè)倏匆粋€(gè)小例子:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_clean_feature_engineer_and_split(data_path):
# Load data
df = pd.read_csv(data_path)
# Clean data
df.dropna(inplace=True)
df = df[df['Age'] > 0]
# Feature engineering
df['AgeGroup'] = pd.cut(df['Age'], bins=[0, 18, 65, 99], labels=['child', 'adult', 'senior'])
df['IsAdult'] = df['Age'] > 18
# Data preprocessing
scaler = StandardScaler()
df[['Age', 'Fare']] = scaler.fit_transform(df[['Age', 'Fare']])
# Split data
features = df.drop('Survived', axis=1)
target = df['Survived']
features_train, features_test, target_train, target_test = train_test_split(features, target, test_size=0.2, random_state=42)
return features_train, features_test, target_train, target_test
您能否已經(jīng)發(fā)現(xiàn)上述提到的規(guī)則的違規(guī)情況?
這個(gè)函數(shù)不長(zhǎng),但顯然違反了一個(gè)函數(shù)應(yīng)該做一件事的規(guī)則。
此外,注釋表明這些代碼塊可以放在一個(gè)單獨(dú)的函數(shù)中,并且可以為每個(gè)函數(shù)命名,以便更清楚地了解情況,并且不需要注釋(關(guān)于這一點(diǎn)將在下一節(jié)中討論)。
所以,讓我們看看重構(gòu)后的例子:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_data(data_path):
return pd.read_csv(data_path)
def clean_data(df):
df.dropna(inplace=True)
df = df[df['Age'] > 0]
return df
def feature_engineering(df):
df['AgeGroup'] = pd.cut(df['Age'], bins=[0, 18, 65, 99], labels=['child', 'adult', 'senior'])
df['IsAdult'] = df['Age'] > 18
return df
def preprocess_features(df):
scaler = StandardScaler()
df[['Age', 'Fare']] = scaler.fit_transform(df[['Age', 'Fare']])
return df
def split_data(df, target_name='Survived'):
features = df.drop(target_name, axis=1)
target = df[target_name]
return train_test_split(features, target, test_size=0.2, random_state=42)
if __name__ == "__main__":
data_path = 'data.csv'
df = load_data(data_path)
df = clean_data(df)
df = feature_engineering(df)
df = preprocess_features(df)
X_train, X_test, y_train, y_test = split_data(df)
在這個(gè)重構(gòu)后的代碼片段中,每個(gè)函數(shù)只做一件事,使得閱讀代碼變得更容易。測(cè)試本身現(xiàn)在也變得更容易,因?yàn)槊總€(gè)函數(shù)都可以與其他函數(shù)隔離地進(jìn)行測(cè)試。
即使注釋也不再需要,因?yàn)楝F(xiàn)在函數(shù)名稱就像是對(duì)自己的注釋一樣。
但是現(xiàn)在還缺少一個(gè)部分:文檔字符串
文檔字符串是 Python 的標(biāo)準(zhǔn),旨在提供可讀和可理解的代碼。
每個(gè)用于生產(chǎn)代碼的函數(shù)都應(yīng)包含一個(gè)文檔字符串,描述其意圖、輸入?yún)?shù)以及有關(guān)返回值的信息。
文檔字符串直接被諸如 Sphinx 這樣的工具使用,其目的是為代碼創(chuàng)建文檔。
現(xiàn)在讓我們?yōu)樯厦娴拇a片段添加文檔字符串:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_data(data_path):
"""
從CSV文件中加載數(shù)據(jù)到pandas DataFrame中。
Args:
data_path (str): 數(shù)據(jù)集的文件路徑。
Returns:
DataFrame: 加載的數(shù)據(jù)集。
"""
return pd.read_csv(data_path)
def clean_data(df):
"""
通過(guò)刪除帶有缺失值的行并過(guò)濾掉非正年齡的行來(lái)清理DataFrame。
Args:
df (DataFrame): 輸入數(shù)據(jù)集。
Returns:
DataFrame: 清理后的數(shù)據(jù)集。
"""
df.dropna(inplace=True)
df = df[df['Age'] > 0]
return df
def feature_engineering(df):
"""
對(duì)DataFrame進(jìn)行特征工程,包括年齡分組和成人標(biāo)識(shí)。
Args:
df (DataFrame): 輸入數(shù)據(jù)集。
Returns:
DataFrame: 添加了新特征的數(shù)據(jù)集。
"""
df['AgeGroup'] = pd.cut(df['Age'], bins=[0, 18, 65, 99], labels=['child', 'adult', 'senior'])
df['IsAdult'] = df['Age'] > 18
return df
def preprocess_features(df):
"""
通過(guò)使用StandardScaler對(duì)'Age'和'Fare'列進(jìn)行標(biāo)準(zhǔn)化來(lái)預(yù)處理特征。
Args:
df (DataFrame): 輸入數(shù)據(jù)集。
Returns:
DataFrame: 帶有標(biāo)準(zhǔn)化特征的數(shù)據(jù)集。
"""
scaler = StandardScaler()
df[['Age', 'Fare']] = scaler.fit_transform(df[['Age', 'Fare']])
return df
def split_data(df, target_name='Survived'):
"""
將數(shù)據(jù)集分割為訓(xùn)練集和測(cè)試集。
Args:
df (DataFrame): 輸入數(shù)據(jù)集。
target_name (str): 目標(biāo)變量列的名稱。
Returns:
tuple: 包含訓(xùn)練特征、測(cè)試特征、訓(xùn)練目標(biāo)和測(cè)試目標(biāo)數(shù)據(jù)集。
"""
features = df.drop(target_name, axis=1)
target = df[target_name]
return train_test_split(features, target, test_size=0.2, random_state=42)
if __name__ == "__main__":
data_path = 'data.csv'
df = load_data(data_path)
df = clean_data(df)
df = feature_engineering(df)
df = preprocess_features(df)
X_train, X_test, y_train, y_test = split_data(df)
集成開(kāi)發(fā)環(huán)境(IDEs)如VSCode通常提供文檔字符串的擴(kuò)展,因此只要您在函數(shù)定義下添加多行字符串,文檔字符串就會(huì)自動(dòng)添加。這有助于您快速獲得所需格式的正確文檔字符串。
格式化
代碼主要是閱讀的次數(shù)要比編寫的次數(shù)多。沒(méi)有人愿意閱讀格式混亂、難以理解的代碼。
在Python中,有PEP 8風(fēng)格指南可供遵循,以使代碼更易讀。
一些重要的格式化規(guī)則包括:
-
使用四個(gè)空格進(jìn)行代碼縮進(jìn) -
將所有行限制在最多79個(gè)字符 -
在某些情況下避免不必要的空白(即在括號(hào)內(nèi)部,尾隨逗號(hào)和右括號(hào)之間,...) 但請(qǐng)記住:格式化規(guī)則應(yīng)該使代碼更易讀。有時(shí),應(yīng)用其中一些規(guī)則是沒(méi)有意義的,因?yàn)槟菢拥拇a就不會(huì)更易讀。在這種情況下,請(qǐng)忽略其中一些規(guī)則。
您可以使用IDE中的擴(kuò)展來(lái)支持遵循您的指南。例如,VSCode提供了幾種用于此目的的擴(kuò)展。
您可以使用Pylint和autopep8等Python包來(lái)支持格式化您的Python腳本。
Pylint是一個(gè)靜態(tài)代碼分析器,它會(huì)給您的代碼打分(最高10分),而autopep8可以自動(dòng)將您的代碼格式化為符合PEP8標(biāo)準(zhǔn)的形式。
讓我們使用本文中早期代碼片段來(lái)了解一下:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_data(data_path):
return pd.read_csv(data_path)
def clean_data(df):
df.dropna(inplace=True)
df = df[df['Age'] > 0]
return df
def feature_engineering(df):
df['AgeGroup'] = pd.cut(df['Age'], bins=[0, 18, 65, 99], labels=['child', 'adult', 'senior'])
df['IsAdult'] = df['Age'] > 18
return df
def preprocess_features(df):
scaler = StandardScaler()
df[['Age', 'Fare']] = scaler.fit_transform(df[['Age', 'Fare']])
return df
def split_data(df, target_name='Survived'):
features = df.drop(target_name, axis=1)
target = df[target_name]
return train_test_split(features, target, test_size=0.2, random_state=42)
if __name__ == "__main__":
data_path = 'data.csv'
df = load_data(data_path)
df = clean_data(df)
df = feature_engineering(df)
df = preprocess_features(df)
X_train, X_test, y_train, y_test = split_data(df)
現(xiàn)在將其保存到一個(gè)名為train.py的文件中,并運(yùn)行Pylint來(lái)檢查我們的代碼段的分?jǐn)?shù):
pylint train.py
這將產(chǎn)生以下輸出:
************* Module train
train.py:29:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:30:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:31:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:32:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:33:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:34:0: C0304: Final newline missing (missing-final-newline)
train.py:34:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
train.py:1:0: C0114: Missing module docstring (missing-module-docstring)
train.py:5:0: C0116: Missing function or method docstring (missing-function-docstring)
train.py:5:14: W0621: Redefining name 'data_path' from outer scope (line 29) (redefined-outer-name)
train.py:8:0: C0116: Missing function or method docstring (missing-function-docstring)
train.py:8:15: W0621: Redefining name 'df' from outer scope (line 30) (redefined-outer-name)
train.py:13:0: C0116: Missing function or method docstring (missing-function-docstring)
train.py:13:24: W0621: Redefining name 'df' from outer scope (line 30) (redefined-outer-name)
train.py:18:0: C0116: Missing function or method docstring (missing-function-docstring)
train.py:18:24: W0621: Redefining name 'df' from outer scope (line 30) (redefined-outer-name)
train.py:23:0: C0116: Missing function or method docstring (missing-function-docstring)
train.py:23:15: W0621: Redefining name 'df' from outer scope (line 30) (redefined-outer-name)
train.py:29:2: C0103: Constant name "data_path" doesn't conform to UPPER_CASE naming style (invalid-name)
------------------------------------------------------------------
Your code has been rated at 3.21/10
哇,只有3.21分中的得分。
您現(xiàn)在可以手動(dòng)修復(fù)這些問(wèn)題,然后重新運(yùn)行它。或者您可以使用autopep8軟件包自動(dòng)解決其中一些問(wèn)題。
讓我們采取第二種方法:
autopep8 --in-place --aggressive --aggressive train.py
現(xiàn)在train.py腳本如下所示:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_data(data_path):
return pd.read_csv(data_path)
def clean
_data(df):
df.dropna(inplace=True)
df = df[df['Age'] > 0]
return df
def feature_engineering(df):
df['AgeGroup'] = pd.cut(
df['Age'], bins=[
0, 18, 65, 99], labels=[
'child', 'adult', 'senior'])
df['IsAdult'] = df['Age'] > 18
return df
def preprocess_features(df):
scaler = StandardScaler()
df[['Age', 'Fare']] = scaler.fit_transform(df[['Age', 'Fare']])
return df
def split_data(df, target_name='Survived'):
features = df.drop(target_name, axis=1)
target = df[target_name]
return train_test_split(features, target, test_size=0.2, random_state=42)
if __name__ == "__main__":
data_path = 'data.csv'
df = load_data(data_path)
df = clean_data(df)
df = feature_engineering(df)
df = preprocess_features(df)
X_train, X_test, y_train, y_test = split_data(df)
再次運(yùn)行Pylint,我們得到了10分的分?jǐn)?shù):
pylint train.py
太棒了!
這真正展示了Pylint在使您的代碼更清晰并迅速遵循PEP8標(biāo)準(zhǔn)方面的作用。
錯(cuò)誤處理
錯(cuò)誤處理確保您的代碼能夠處理意外情況,而不會(huì)崩潰或產(chǎn)生不正確的結(jié)果。
想象一下,您部署了一個(gè)模型在一個(gè)API后面,用戶可以向該部署模型發(fā)送數(shù)據(jù)。然而,用戶可能會(huì)向該模型發(fā)送錯(cuò)誤的數(shù)據(jù),因此應(yīng)用程序可能會(huì)崩潰,這對(duì)用戶體驗(yàn)來(lái)說(shuō)不是一個(gè)好印象。他們很可能會(huì)責(zé)怪您的應(yīng)用程序,并聲稱它開(kāi)發(fā)得不好。
如果用戶收到一個(gè)特定的錯(cuò)誤代碼和一個(gè)清楚告訴他們出了什么問(wèn)題的消息,那就更好了。
這就是Python異常發(fā)揮作用的地方。
假設(shè)用戶可以上傳一個(gè)CSV文件到您的應(yīng)用程序,將其加載到pandas數(shù)據(jù)框中,然后將其轉(zhuǎn)發(fā)給您的模型進(jìn)行預(yù)測(cè)。
那么您會(huì)有一個(gè)類似以下的函數(shù):
import pandas as pd
def load_data(data_path):
"""
從指定的CSV文件中加載數(shù)據(jù)集到pandas DataFrame中。
參數(shù):
data_path (str): 數(shù)據(jù)集的文件路徑。
返回:
DataFrame: 加載的數(shù)據(jù)集。
"""
return pd.read_csv(data_path)
到目前為止,一切都很順利。
但是當(dāng)用戶沒(méi)有提供CSV文件時(shí)會(huì)發(fā)生什么呢?
您的程序?qū)⒈罎ⅲ@示以下錯(cuò)誤消息:
FileNotFoundError: [Errno 2] No such file or directory: 'data.csv'
由于您正在運(yùn)行一個(gè)API,它將簡(jiǎn)單地向用戶返回一個(gè)HTTP 500代碼,告訴他有一個(gè)“內(nèi)部服務(wù)器錯(cuò)誤”。
用戶可能會(huì)因此責(zé)怪您的應(yīng)用程序,因?yàn)樗麩o(wú)法看到他對(duì)該錯(cuò)誤負(fù)責(zé)。
有什么更好的處理方法嗎?
添加一個(gè)try-except塊并捕獲FileNotFoundError來(lái)正確處理該情況:
import pandas as pd
import logging
def load_data(data_path):
"""
從指定的CSV文件中加載數(shù)據(jù)集到pandas DataFrame中。
參數(shù):
data_path (str): 數(shù)據(jù)集的文件路徑。
返回:
DataFrame: 加載的數(shù)據(jù)集。
"""
try:
return pd.read_csv(data_path)
except FileNotFoundError:
logging.error("路徑 %s 處的文件不存在。請(qǐng)確保您已正確上傳文件。", data_path)
但現(xiàn)在我們只記錄了該錯(cuò)誤消息。最好定義一個(gè)自定義異常,然后在我們的API中處理該異常,以向用戶返回特定的錯(cuò)誤代碼:
import pandas as pd
import logging
class DataLoadError(Exception):
"""當(dāng)數(shù)據(jù)無(wú)法加載時(shí)引發(fā)的異常。"""
def __init__(self, message="數(shù)據(jù)無(wú)法加載"):
self.message = message
super().__init__(self.message)
def load_data(data_path):
"""
從指定的CSV文件中加載數(shù)據(jù)集到pandas DataFrame中。
參數(shù):
data_path (str): 數(shù)據(jù)集的文件路徑。
返回:
DataFrame: 加載的數(shù)據(jù)集。
"""
try:
return pd.read_csv(data_path)
except FileNotFoundError:
logging.error("路徑 %s 處的文件不存在。請(qǐng)確保您已正確上傳文件。", data_path)
raise DataLoadError(f"路徑 {data_path} 處的文件不存在。請(qǐng)確保您已正確上傳文件。")
然后,在您的API的主要函數(shù)中:
try:
df = load_data('path/to/data.csv')
# 進(jìn)行進(jìn)一步的處理和模型預(yù)測(cè)
except DataLoadError as e:
# 向用戶返回一個(gè)包含錯(cuò)誤消息的響應(yīng)
# 例如:return Response({"error": str(e)}, status=400)
現(xiàn)在,用戶將收到一個(gè)錯(cuò)誤代碼400(錯(cuò)誤的請(qǐng)求),并帶有一個(gè)告訴他們出了什么問(wèn)題的錯(cuò)誤消息。
他現(xiàn)在知道該怎么做了,不會(huì)再責(zé)怪您的程序無(wú)法正常工作了。
面向?qū)ο缶幊?/span>
面向?qū)ο缶幊淌且环N編程范式,它提供了一種將屬性和行為捆綁到單個(gè)對(duì)象中的方法。
主要優(yōu)點(diǎn):
-
對(duì)象通過(guò)封裝隱藏?cái)?shù)據(jù)。 -
通過(guò)繼承可以重用代碼。 -
可以將復(fù)雜問(wèn)題分解為小對(duì)象,并且開(kāi)發(fā)人員可以一次專注于一個(gè)對(duì)象。 -
提高可讀性。 -
還有許多其他優(yōu)點(diǎn)。我強(qiáng)調(diào)了最重要的幾個(gè)(至少對(duì)我來(lái)說(shuō)是這樣)。
現(xiàn)在讓我們看一個(gè)小例子,其中創(chuàng)建了一個(gè)名為“TrainingPipeline”的類,并帶有一些基本函數(shù):
from abc import ABC, abstractmethod
class TrainingPipeline(ABC):
def __init__(self, data_path, target_name):
"""
初始化TrainingPipeline。
Args:
data_path (str): 數(shù)據(jù)集的文件路徑。
target_name (str): 目標(biāo)列的名稱。
"""
self.data_path = data_path
self.target_name = target_name
self.data = None
self.X_train = None
self.X_test = None
self.y_train = None
self.y_test = None
@abstractmethod
def load_data(self):
"""從數(shù)據(jù)路徑加載數(shù)據(jù)集。"""
pass
@abstractmethod
def clean_data(self):
"""清理數(shù)據(jù)。"""
pass
@abstractmethod
def feature_engineering(self):
"""執(zhí)行特征工程。"""
pass
@abstractmethod
def preprocess_features(self):
"""預(yù)處理特征。"""
pass
@abstractmethod
def split_data(self):
"""將數(shù)據(jù)拆分為訓(xùn)練集和測(cè)試集。"""
pass
def run(self):
"""運(yùn)行訓(xùn)練管道。"""
self.load_data()
self.clean_data()
self.feature_engineering()
self.preprocess_features()
self.split_data()
這是一個(gè)抽象基類,僅定義了派生自基類的類必須實(shí)現(xiàn)的抽象方法。
這在定義所有子類都必須遵循的藍(lán)圖或模板時(shí)非常有用。
然后,一個(gè)示例子類可能如下所示:
import pandas as pd
from sklearn.preprocessing import StandardScaler
class ChurnPredictionTrainPipeline(TrainingPipeline):
def load_data(self):
"""從數(shù)據(jù)路徑加載數(shù)據(jù)集。"""
self.data = pd.read_csv(self.data_path)
def clean_data(self):
"""清理數(shù)據(jù)。"""
self.data.dropna(inplace=True)
def feature_engineering(self):
"""執(zhí)行特征工程。"""
categorical_cols = self.data.select_dtypes(include=['object', 'category']).columns
self.data = pd.get_dummies(self.data, columns=categorical_cols, drop_first=True)
def preprocess_features(self):
"""預(yù)處理特征。"""
numerical_cols = self.data.select_dtypes(include=['int64', 'float64']).columns
scaler = StandardScaler()
self.data[numerical_cols] = scaler.fit_transform(self.data[numerical_cols])
def split_data(self):
"""將數(shù)據(jù)拆分為訓(xùn)練集和測(cè)試集。"""
features = self.data.drop(self.target_name, axis=1)
target = self.data[self.target_name]
self.features_train, self.features_test, self.target_train, self.target_test = train_test_split(features, target, test_size=0.2, random_state=42)
這樣做的好處是,您可以構(gòu)建一個(gè)應(yīng)用程序,該應(yīng)用程序自動(dòng)調(diào)用訓(xùn)練管道的方法,并且可以創(chuàng)建訓(xùn)練管道的不同類。它們始終兼容,并且必須遵循抽象基類中定義的藍(lán)圖。
測(cè)試
這一章是最重要的章節(jié)之一。
有了測(cè)試,可以決定整個(gè)項(xiàng)目的成功或失敗。
創(chuàng)建沒(méi)有測(cè)試的代碼更快,因?yàn)楫?dāng)您還需要為每個(gè)函數(shù)編寫單元測(cè)試時(shí),這似乎是一種“浪費(fèi)時(shí)間”的行為。編寫單元測(cè)試的代碼很快就會(huì)超過(guò)函數(shù)的代碼量。
但請(qǐng)相信我,這是值得的努力!
如果您不快速添加單元測(cè)試,就會(huì)感到痛苦。有時(shí),并不是在一開(kāi)始就會(huì)感到痛苦。
但是當(dāng)您的代碼庫(kù)增長(zhǎng)并且添加更多功能時(shí),您肯定會(huì)感到痛苦。突然之間,調(diào)整一個(gè)函數(shù)的代碼可能會(huì)導(dǎo)致其他函數(shù)失敗。新的發(fā)布需要大量的緊急修復(fù)。客戶感到惱火。團(tuán)隊(duì)中的開(kāi)發(fā)人員害怕適應(yīng)代碼庫(kù)中的任何東西,導(dǎo)致發(fā)布新功能的速度非常緩慢。
因此,無(wú)論何時(shí)您在編寫后續(xù)需要投入生產(chǎn)的代碼時(shí),請(qǐng)始終遵循測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)原則!
在Python中,可以使用類似unittest或pytest的庫(kù)來(lái)測(cè)試您的函數(shù)。
我個(gè)人更喜歡pytest。
您可以在這篇文章中了解有關(guān)Python測(cè)試的更多信息。該文章還側(cè)重于集成測(cè)試,這是測(cè)試的另一個(gè)重要方面,以確保您的系統(tǒng)端到端正常工作。
讓我們?cè)俅慰匆幌轮罢鹿?jié)中的ChurnPredictionTrainPipeline類:
import pandas as pd
from sklearn.preprocessing import StandardScaler
class ChurnPredictionTrainPipeline(TrainingPipeline):
def load_data(self):
"""Load dataset from data path."""
self.data = pd.read_csv(self.data_path)
...
現(xiàn)在,讓我們使用pytest為加載數(shù)據(jù)添加單元測(cè)試:
import os
import shutil
import logging
from unittest.mock import patch
import joblib
import pytest
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from churn_library import ChurnPredictor
@pytest.fixture
def path():
"""
Return the path to the test csv data file.
"""
return r"./data/bank_data.csv"
def test_import_data_returns_dataframe(path):
"""
Test that import data can load the CSV file into a pandas dataframe.
"""
churn_predictor = ChurnPredictionTrainPipeline(path, "Churn")
churn_predictor.load_data()
assert isinstance(churn_predictor.data, pd.DataFrame)
def test_import_data_raises_exception():
"""
Test that exception of "FileNotFoundError" gets raised in case the CSV
file does not exist.
"""
with pytest.raises(FileNotFoundError):
churn_predictor = ChurnPredictionTrainPipeline("non_existent_file.csv",
"Churn")
churn_predictor.load_data()
def test_import_data_reads_csv(path):
"""
Test that the pandas.read_csv function gets called.
"""
with patch("pandas.read_csv") as mock_csv:
churn_predictor = ChurnPredictionTrainPipeline(path, "Churn")
churn_predictor.load_data()
mock_csv.assert_called_once_with(path)
這些單元測(cè)試是:
-
測(cè)試CSV文件是否可以加載到pandas數(shù)據(jù)框中。 -
測(cè)試在CSV文件不存在的情況下是否會(huì)引發(fā)FileNotFoundError異常。 -
測(cè)試pandas的“read_csv”函數(shù)是否被調(diào)用。
這個(gè)過(guò)程并不完全是TDD,因?yàn)樵谔砑訂卧獪y(cè)試之前,我已經(jīng)開(kāi)發(fā)了代碼。但在理想情況下,您應(yīng)該在實(shí)現(xiàn)load_data函數(shù)之前甚至編寫這些單元測(cè)試。
系統(tǒng)
你會(huì)一次性建造一座城市嗎?很可能不會(huì)。
對(duì)軟件也是一樣的。
構(gòu)建一個(gè)干凈的系統(tǒng)就是將其拆分為更小的組件。每個(gè)組件都使用清晰的代碼原則構(gòu)建,并經(jīng)過(guò)良好的測(cè)試。
這一章中最重要的部分是關(guān)注關(guān)注點(diǎn)的分離:
將啟動(dòng)過(guò)程與運(yùn)行時(shí)邏輯分開(kāi),構(gòu)造依賴項(xiàng)。在主函數(shù)中初始化所有對(duì)象,并將它們插入到依賴它們的類中(依賴注入)。這種方法有助于逐步構(gòu)建系統(tǒng),使其易于擴(kuò)展和添加更多功能。
并發(fā)
并發(fā)有時(shí)對(duì)于通過(guò)巧妙地在任務(wù)之間跳轉(zhuǎn)來(lái)加快進(jìn)程速度是有幫助的。
并發(fā)也可以被看作是一種解耦策略,因?yàn)椴煌牟糠中枰?dú)立運(yùn)行,以便并發(fā)可以提高總體運(yùn)行時(shí)間。
并發(fā)也會(huì)帶來(lái)一些開(kāi)銷,并使程序更加復(fù)雜,因此明智地決定是否值得投入這樣的工作。
例如,您需要處理共享資源和同步訪問(wèn)。
在Python中,您可以利用 asyncio 模塊。在這篇文章中閱讀更多關(guān)于Python中并發(fā)的內(nèi)容。
重構(gòu)
重構(gòu)您的代碼可以提高可讀性和可維護(hù)性。
總是從簡(jiǎn)單開(kāi)始,甚至是從丑陋的代碼開(kāi)始。讓它運(yùn)行起來(lái)。然后進(jìn)行重構(gòu)。消除重復(fù),改進(jìn)命名,并降低復(fù)雜性。
但請(qǐng)記住,在開(kāi)始重構(gòu)之前一定要有您的測(cè)試。這樣可以確保在重構(gòu)時(shí)不會(huì)破壞東西。
您應(yīng)該重構(gòu)您的代碼使其更加清晰。太多的開(kāi)發(fā)人員,包括我開(kāi)始時(shí),都有這樣的想法,即我的代碼現(xiàn)在可以運(yùn)行,所以我推送它然后繼續(xù)下一個(gè)任務(wù)。
擺脫這種思維方式!否則,隨著代碼庫(kù)的增長(zhǎng),您將會(huì)遇到很多問(wèn)題,現(xiàn)在您必須處理難以維護(hù)的丑陋代碼。
結(jié)論
編寫清潔的代碼是一門藝術(shù)。它需要紀(jì)律性,并且經(jīng)常不夠。但它對(duì)于軟件項(xiàng)目的成功非常重要。
作為一名數(shù)據(jù)科學(xué)家,您往往不會(huì)編寫干凈的代碼,因?yàn)槟饕獙W⒂趯ふ液玫哪P筒⒃贘upyter Notebooks中運(yùn)行代碼以獲得您所追求的指標(biāo)。
當(dāng)我主要從事數(shù)據(jù)科學(xué)項(xiàng)目時(shí),我也從不關(guān)心編寫干凈的代碼。
但是,數(shù)據(jù)科學(xué)家編寫干凈的代碼對(duì)于確保模型更快地投入生產(chǎn)也是至關(guān)重要的。
- EOF -
作者簡(jiǎn)介
城哥,公眾號(hào)9年博主,一線互聯(lián)網(wǎng)工作10年、公司校招和社招技術(shù)面試官,主導(dǎo)多個(gè)公司級(jí)實(shí)戰(zhàn)項(xiàng)目(Python、數(shù)據(jù)分析挖掘、算法、AI平臺(tái)、大模型等)。
關(guān)注我,陪你一起成長(zhǎng),遇見(jiàn)更好的自己。
星球服務(wù)
會(huì)不定期發(fā)放知識(shí)星球優(yōu)惠券,加入星球前可以添加城哥微信:dkl88191,咨詢優(yōu)惠券問(wèn)題。
加入知識(shí)星球后,可以享受7大福利與服務(wù):免費(fèi)獲取海量技術(shù)資料、向我 1 對(duì) 1 技術(shù)咨詢、求職指導(dǎo),簡(jiǎn)歷優(yōu)化、歷史文章答疑(源碼+數(shù)據(jù))、綜合&專業(yè)技術(shù)交流社群、大模型技術(shù)分享、定制專屬學(xué)習(xí)路線,幫你快速成長(zhǎng)、告別迷茫。
原創(chuàng)不易,技術(shù)學(xué)習(xí)資料如下,星球成員可免費(fèi)獲取,非星球成員,添加城哥微信:dkl88191,請(qǐng)城哥喝杯星巴克。
