原文:
www.kdnuggets.com/2021/08/data-scientist-guide-efficient-coding-python.html
评论
由Dr. Varshita Sher,数据科学家
在这篇文章中,我想分享一些我在过去一年中从配对编程中吸收的编写更清晰代码的技巧。一般来说,将它们作为我日常编码例程的一部分,帮助我生成了高质量的 Python 脚本,这些脚本随着时间的推移容易维护和扩展。
有没有想过为什么高级开发者的代码看起来比初级开发者的代码好得多?继续阅读,来弥合这个差距……
我将提供实际的编码场景来说明如何使用这些技巧,而不是给出通用的示例!这是一个Jupyter Colab Notebook,如果你想跟着一起操作!
想象一下遍历一个大型的可迭代对象(列表、字典、元组、集合),却不知道代码是否已经运行完成!真糟糕,对吧!在这种情况下,请确保使用tqdm
构造来显示进度条。
例如,在我读取存在于 44 个不同目录中的所有文件时(这些路径已经存储在名为fpaths
的列表中),以显示进度:
from tqdm import tqdmfiles = list()
fpaths = ["dir1/subdir1", "dir2/subdir3", ......]
for fpath in tqdm(fpaths, desc="Looping over fpaths")):
files.extend(os.listdir(fpath))
使用 tqdm 与 “for” 循环
注意:使用*desc*
参数来为循环指定一个简短的描述。
简单来说,就是在 Python 函数定义中明确声明所有参数的类型。
我希望有具体的用例来强调何时使用类型提示,但事实是,我经常使用它们。
这是一个假设的函数update_df()
示例。它通过附加一行包含来自模拟运行的有用信息(例如使用的分类器、得分的准确率、训练-测试分割大小以及该特定运行的额外备注)的数据帧来更新给定的数据帧。
def update_df(**df: pd.DataFrame**,
**clf: str**,
**acc: float**,
**remarks: List[str] = []**
**split:float** = 0.5) -> **pd.DataFrame**:
new_row = {'Classifier':clf,
'Accuracy':acc,
'split_size':split,
'Remarks':remarks}
df = df.append(new_row, ignore_index=True)
return df
几点需要注意:
-
函数定义中的
->
符号后面的数据类型(def update_df(.......) **->** pd.DataFrame
)表示函数返回值的类型,即在这种情况下是 Pandas 的数据框。 -
如果有默认值,可以像往常一样以
param:type = value
的形式指定。(例如:split: float = 0.5
) -
如果一个函数没有返回任何内容,可以自由使用
None
。例如:def func(a: str, b: int) -> None: print(a,b)
-
要返回混合类型的值,例如,假设一个函数可以在标志
option
设置时打印语句,或者在标志未设置时返回一个int
:
from typing import Union
def dummy_args(*args: list[int], option = True) -> Union[None, int]:
if option:
print(args)
else:
return 10
注意:从 Python 3.10 开始,*Union*
* 不再是必需的,因此你可以直接这样做:*
def dummy_args(*args: list[int], option = True) -> None | int:
if option:
print(args)
else:
return 10
-
在定义参数类型时,你可以尽可能具体,就像我们对
remarks: List[str]
所做的那样。我们不仅指定它应该是一个List
,而且它应该仅仅是str
类型的列表。为了好玩,尝试在调用函数时传递一个整数列表到
remarks
。你会看到没有错误返回!为什么会这样? 因为 Python 解释器不会根据你的类型提示执行任何类型检查。
尽管如此,包含它仍然是一个好的实践!我觉得它在编写函数时能带来更多的清晰度。此外,当有人调用这样的函数时,他们会看到输入参数的清晰提示。
带类型提示的函数调用提示
想象一下:你想写一个函数,接收 一些 目录路径,并打印每个目录中的文件数量。问题是,我们不知道用户会输入 多少 个路径!可能是 2 个,也可能是 20 个!所以我们不确定应该在函数定义中定义多少个参数。显然,写一个像 def count_files(file1, file2, file3, …..file20)
这样的函数会很傻。在这种情况下,args
和(有时 kwargs
)非常有用!
Args 用于指定未知数量的 位置 参数。
Kwargs 用于指定未知数量的 关键字 参数。
这是一个函数 count_files_in_dir()
的示例,它接收 project_root_dir
和一个任意数量的文件夹路径(在函数定义中使用 *fpaths
)。作为输出,它会打印每个这些文件夹中的文件数量。
def count_files_in_dir(project_root_dir, *fpaths: str):
for path in fpaths:
rel_path = os.path.join(project_root_dir, path)
print(path, ":", len(os.listdir(rel_path)))
计算 Google Colab 目录中的文件数量
在函数调用中,我们传入了 5 个参数。由于函数定义期望一个 必需 的位置参数,即 project_root_dir
,它会自动知道 "../usr"
必须是这个参数。其余的参数(在这个例子中是四个)都被 *fpaths
吸收,用于计算文件数量。
注意:这种吸收技术的正确术语是“参数打包”,即剩余参数被打包成 **fpaths*
。
让我们来看一下必须接收未知数量的 关键字 参数的函数。在这种情况下,我们必须使用 kwargs
而不是 args
。以下是一个简短的(无用的)示例:
def print_results(**results):
for key, val in results.items():
print(key, val)
使用方式与*args
非常相似,但现在我们能够将任意数量的关键字参数传递给函数。这些参数作为键值对存储在**results
字典中。从这里开始,可以使用.items()
轻松访问字典中的项。
我在工作中发现了kwargs
的两个主要应用:
- 合并字典(有用但较少有趣)
dict1 = {'a':2 , 'b': 20}
dict2 = {'c':15 , 'd': 40}
merged_dict = {**dict1, **dict2}
*************************
{'a': 2, 'b': 20, 'c': 15, 'd': 40}
- 扩展现有方法(更有趣)
def myfunc(a, b, flag, **kwargs):
if flag:
a, b = do_some_computation(a,b)
actual_function(a,b, **kwargs)
注意:查看matplotlib 的绘图函数使用[*kwargs*](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html#matplotlib-pyplot-plot)
* 来指定图表的可选修饰,如线宽和标签。*
这里有一个实际使用**kwargs
扩展方法的案例,来自我最近的一个项目:
我们通常使用 Sklearn 的train_test_split()
来拆分X
和y
。在处理其中一个 GAN 项目时,我需要将生成的合成图像拆分为与拆分真实图像及其相应标签所用的相同的训练测试集。此外,我还希望能够传递任何其他通常传递给train_test_split()
的参数。最后,stratify
必须始终传递,因为我在处理人脸识别问题(并希望所有标签都在训练集和测试集中存在)。
为此,我们创建了一个名为custom_train_test_split()
的函数。我包含了一些打印语句来展示内部发生的情况(并省略了一些片段以简化说明)。
def custom_train_test_split(clf, y, *X, stratify, **split_args): *print("Classifier used: ", classifier)
print("Keys:", split_args.keys())
print("Values: ", split_args.values())
print(X)
print(y)
print("Length of passed keyword arguments: ", len(split_args))*
trainx,testx,*synthetic,trainy,testy = train_test_split(
*X,
y,
stratify=stratify,
**split_args
) *######### OMITTED CODE SNIPPET #############
# Train classifier on train and synthetic ims
# Calculate accuracy on testx, testy
############################################*
*print("trainx: ", trainx, "trainy: ",trainy, '\n', "testx: ",
testx, "testy:", testy)* *print("synthetic: ", *synthetic)*
注意:在调用此函数时,为了便于理解,我使用了虚拟数据替换了实际的图像向量和标签(见下图)。不过,代码同样适用于真实图像!
图 A 使用函数定义中的 kwargs 调用函数
注意事项:
-
在函数调用语句中使用的所有关键字参数(除了
stratify
),将作为键值对存储在**split_args
字典中。(要验证,请查看蓝色输出。)你可能会问为什么不使用
stratify
?这是因为根据函数定义,它是一个必需的仅限关键字参数,而不是一个可选的参数。 -
所有非关键字(即位置)参数(如
"SVM"
、labels
等)在函数调用中会存储在函数定义中的前三个参数,即clf
、y
和*X
(是的,传递的顺序很重要)。然而,在函数调用中我们有四个参数,即"SVM"
、labels
、ims
和synthetic_ims
。那第四个参数该存储在哪里?记住我们在函数定义中使用了
*X
作为第三个参数,因此传递给函数的所有参数在前两个参数之后都被打包(浸泡)到*X
中。(要验证,请检查绿色输出)。 -
当在我们的函数中调用
train_test_split()
方法时,我们实际上是在使用*
运算符解包X
和split_args
参数(*X
和**split_args
),以便将所有元素作为不同的参数传递。
也就是说,
train_test_split(*X, y, stratify = stratify, **split_args)
相当于写
train_test_split(ims, synthetic_ims, y, stratify = stratify, train_size = 0.6, random_state = 50)
- 当存储
train_test_split()
方法的结果时,我们再次打包synthetic_train
和synthetic_test
集合到一个单独的*synthetic
变量中。
要检查里面有什么,我们可以使用*
运算符再次解包它(见粉色输出)。
注意:如果你想深入了解使用***
运算符进行打包和解包,请查看这篇文章。
我们编写的代码通常很凌乱,缺乏适当的格式,比如尾随空格、尾随逗号、未排序的导入语句、缩进中的空格等。
虽然可以手动修复所有这些问题,但使用pre-commit hooks可以节省你大量的时间。简单来说,这些 hooks 可以通过一行命令进行自动格式化——pre-commit run
。
这里有一些来自官方文档的简单步骤来开始并创建一个[.pre-commit-config.yaml](https://pre-commit.com/index.html#2-add-a-pre-commit-configuration)
文件。它将包含你关心的所有格式化问题的hooks!
作为纯个人偏好,我倾向于保持我的.pre-commit-config.yaml
配置文件简单,并使用Black 的预提交配置。
注意:需要记住的一点是,文件必须被暂存,即在执行*pre-commit run*
之前使用*git add .*
,否则你会看到所有文件都会被跳过:
如果你的项目包含大量配置变量,例如数据库主机名、密码、AWS 凭证等,请使用.yml
文件来跟踪所有这些变量。你可以在任何 Jupyter Notebook 或你希望的脚本中使用这个文件。
由于我大部分工作是为客户提供模型框架,以便他们可以在自己的数据集上重新训练它,我通常使用配置文件来存储文件夹和文件的路径。这也是确保客户在运行你的脚本时只需更改一个文件的好方法。
让我们在项目目录中创建一个fpaths.yml
文件。我们将存储需要存放图像的根目录。此外,还会存储文件名、标签、属性等的路径。最后,我们还存储合成图像的路径。
image_data_dir: path/to/img/dir *# the following paths are relative to images_data_dir*
fnames:
fnames_fname: fnames.txt
fnames_label: labels.txt
fnames_attr: attr.txt
synthetic:
edit_method: interface_edits
expression: smile.pkl
pose: pose.pkl
你可以像这样阅读这个文件:
# open the yml file
with open(CONFIG_FPATH) as f:
dictionary = yaml.safe_load(f)
# print elements in dictionary
for key, value in dictionary.items():
print(key + " : " + str(value))
print()
注意:如果你想深入了解,这里有一个精彩的 教程 来帮助你入门 yaml。
虽然确实有很多不错的 Python 编辑器,但我必须说 VSCode 是我见过的最好的 (对不起,Pycharm)。为了更好地利用它,考虑从市场中安装这些扩展:
- 括号配对着色器——允许用颜色识别匹配的括号。
- 路径智能感知——允许自动补全文件名。
- Python Docstring 生成器——允许为 Python 函数生成 docstring。
使用 VSCode 扩展生成 docstring
技巧:使用*"""*
* 在你编写了函数并使用了类型提示之后生成 docstring。这样生成的 docstring 将包含更多信息,如默认值、参数类型等(见上图右侧)。*
- Python Indent——(我最喜欢的;由 Kevin Rose 发布) 允许对多行代码/括号进行正确的缩进。
来源:VSCode 扩展市场
- Python 类型提示——允许在编写函数时自动补全类型提示。
- TODO tree:(第二喜欢) 追踪在编写脚本时插入的所有
TODO
。
追踪项目中插入的所有 TODO 注释
- Pylance——允许代码自动补全、参数建议(还有很多其他功能,能更快地编写代码)。
恭喜你离成为专业 Python 开发者更近了一步。我打算在学习到更多有趣的技巧时更新这篇文章。如有更简单的方法完成本文提到的某些任务,请随时告知我。
下次再见 :)
简介:Varshita Sher 博士 是艾伦·图灵研究所的数据科学家,同时也是牛津大学和 SFU 校友。
原文. 已获得授权转载。
相关:
-
编写干净 R 代码的 5 个技巧
-
Python 数据结构比较
-
GitHub Copilot 开源替代品
1. Google 网络安全证书 - 快速进入网络安全职业生涯。
2. Google 数据分析专业证书 - 提升你的数据分析能力
3. Google IT 支持专业证书 - 支持你的组织的 IT 工作