在Python应用程序中使用配置的最佳实践
off999 2024-10-01 14:04 64 浏览 0 评论
大多数计算机应用程序都可以使用配置来指定行为,无论是通过命令行标志、环境变量还是配置文件。作为一名软件开发人员,处理配置时会遇到一些挑战,例如解析不合法的输入、验证它以及在程序的任意位置访问它。以Python为例,在这篇博客中,我想分享一些可以帮助您安全有效地处理配置的最佳实践,并且我希望和您达成共识:这些是在您自己的代码中应该遵循的合理原则。
介绍
除了最简单的程序外,所有的程序都有一组参数来控制它们的行为。作为具体的例子,考虑ls工具的输出格式、nginx监听的端口或git在提交消息中使用的电子邮件地址。根据应用程序的大小和复杂性,可能有许多这样的参数,它们可能只影响一个小的执行细节或者整个程序行为。
当您处理配置时,有很多方面需要考虑:首先,它是如何从外部传递到您的程序中的,如何解析和验证?其次,如何在程序内部处理、访问和在组件之间传递?根据应用程序的类型,您必须考虑在程序运行时用户如何检查和更新它。从操作的角度来看,您可能必须考虑如何管理、测试多个配置并将其部署到生产环境中。
每一个主题都可能变得相当复杂,值得深入探讨。不过,在这篇博文中,我只想关注第二个方面。我将介绍一些处理程序内部配置的指导原则,这些原则是经过时间检验的,我想推荐给任何开发中小型应用程序的人。
在过去,我用各种编程语言(如Go、Scala和Python)构建和维护应用程序。在这篇博文中,我想以Python为例,因为它的动态特性允许使用很多机制用以提高开发速度和灵活性(例如,在运行时修改类),但从长远来看可能会使维护和重构更加困难。
一个简单的例子
当谈到软件应该如何工作以及组件应该如何交互的重要思想时,有时很难与实际编码联系起来。为了避免出现这种情况,让我们跳到下面的代码示例中,看看我想在本文中解决的一些问题:
在评论中,我已经给出了一些关于该代码可能存在的缺点,但是让我们现在更详细地探讨一下。
指导原则
编程是一项在智力上具有挑战性的任务,因此我认为作为软件工程师,我们应该将尽可能多的复杂任务委托给我们的工具,如ide、linter、格式化程序、编译器或类型检查程序。如果可以使用一个工具来发现错误和提高代码质量,那么我认为这就证明了用这种工具来编写代码是合理的。
另外,如果尽管我们仔细检查和使用了工具,但代码中仍有错误,那么应该在应用程序启动时尽快报告,这会产生一个重要的警告消息,并且在许多情况下,程序会立即退出。最糟糕的事情莫过于在一次看似成功的部署的几个小时后,半夜里发现某个配置密钥丢失。
基于这些基础,我认为处理应用程序内部配置的数据结构应该遵循以下四个原则:
它应该使用标识符而不是字符串键来访问配置值。
它的值应该是静态类型的。
应该尽早验证。
它应该声明在它使用的地方。
让我在下面解释这些原则及其作用。 一.使用标识符而不是字符串键值
可能与近年来文件交换和序列化格式的某种“JSONification”有关,以PEP 484为标准的字符串键词典Dict[str,Any]似乎已经成为许多Python开发人员的一站式数据结构。很简单,只需使用json.loads处理一个json格式的字符串后放入Python字典,然后使用像config[“port”]或config[“user”][“email”]一样的代码随意访问它,就像我在介绍性示例中所做的那样。(这种方法不是Python独有的,例如Scala的Lightbend配置库也有一个类似conf.getInt(“foo.bar”)的API。)如果需要新的配置条目,只需将其添加到JSON文件中,并在整个代码中立即使用它。
但是,这种方法有许多缺点:
无法检测不一致的拼写,例如,密钥是“user”还是“users”。
如果存在不一致,不能明确哪里发生了错误。
只有和字典中的值相同才是正确的。
在实际访问数据之前,不会发现丢失的数据。
无法使用IDE/工具来重命名密钥,需要找到并替换字符串的所有匹配项。
不能使用检查变量名格式一致性的工具。
重复的字符串解析和字典查找相当费力。
因此,我建议使用标识符,而不是使用字符串键(在字典中或作为某些get方法的参数)。直接的方法是使用类成员,然后编写config.user.email,而不是config[“user”][“email”]。请注意,Python的数据类(在3.7版中引入,但在3.6版中通过dataclasses模块提供)对于保存此类数据非常方便。
这样做可以解决上面列出的问题:
在编译语言中,编译器显然会立即告诉您是否存在拼写错误,但对于Python,一个足够现代的IDE通常会指出是否使用了未声明的变量或类成员。
类中定义中的名称才是唯一确定的正确名称。
即使在Python中,声明的变量也可能没有初始化(参见PEP 526),但在许多情况下,IDE或linter会告诉您这一点。
使用IDE可以轻松完成重命名。
可以应用普通格式化程序或样式检查程序。
二.静态类型
在上一节中,我们看到了Dict[str,Any]的str部分是如何导致问题的,现在让我们来看看Any部分。我不想在这里讨论静态类型编程语言和动态类型编程语言的所有方面,但就程序正确性而言,有一些证据表明,静态类型检查减少了修复错误时的工作量并且效果更好。在Python中,mypy可以对使用类型注释的代码执行此类检查。我想鼓励您在代码中使用这些注释,而不仅仅是在使用配置时。
从上面的一个例子来看,start_server(port=os.environ.get(“port”,80)),对于需要整数值端口的函数,如果设置了环境变量port,则此代码将失败,因为os.environ的条目始终是字符串类型。您可能知道这一点,但如果start_server函数声明为类似start_server(port:int),那么使用mypy进行的检查将显示出问题:
py:6:错误:“start_server”的参数1具有不兼容的类型“Union[str,int];应为“int”
除了这些基本检查之外,静态类型还提供了一种优雅的方法来限制代码可能接受的输入集。例如,当您有一个引用文件的配置项时,请使用pathlib.Path而不是str,并避免处理字符串格式的无效文件名。如果有固定数量的可选值,请使用enum.enum来表示它。如果只能指定一个或另一个值,请使用Union。如果值是可选的,则通过使用optional显式表达。通过使用类型系统正式指定允许或禁止的值,您可以使用工具来发现您没有覆盖的代码路径,或者那些实际上永远不会覆盖的代码路径。
还有一件事要考虑,特别是在处理诸如持续时间、重量、距离、速度等物理维度时,要抽象出维度,而不是具体的单位。例如,与其像check_interval_s:float或check_interval_ms:int那样声明配置项,不如像check_interval:datetime.timedelta那样声明它。然后,您可以根据这些维度编写大部分代码,在抽象级别上使用它们进行计算,并且只在使用外部库时(例如调用time.sleep(check_interval.total_seconds)将它们转换为具体值。
最后要注意:在Python中,类型注释在运行时没有验证效果。即使所有代码都被注释并通过类型检查,如果变量a:int在运行时是一个字符串,那么意外的事情也会发生。下一节的主题是确保实际数据看起来符合您的预期。
三.早期验证
对于大多数配置值,拥有一个特定的格式、类型或数据范围才是有意义的。如前一节所述使用静态类型已经是限定值必须要有某种格式的示例。可能还有其他约束,如最小值和最大值,与某个正则表达式匹配,或指向配置的另一个(已存在)部分。
一种简单的方法是在使用配置的位置执行验证。例如,你可以写
在使用这些值时也类似。
然而,这会导致一些问题:
必须在使用该值的每个位置验证该值,从而导致代码重复。或者,您在使用它时需要记住是否已经验证了它。
如果有问题,那么只有在第一次访问配置值时才会出现问题。这使得发现错误更加困难,并且需要更多的力气来检查新的配置值是否实际有效。
如上所述,在Python中,即使在类声明中声明port:int,config.port在运行时也可以是一个字符串。你绝对不想在每次使用该值时都去检查。
因此,我建议在程序启动后尽快验证配置,如果发现配置无效,请立即退出。注意,如果您选择使用上一节中建议的适当类型来表示配置条目,那么在许多情况下,只要能成功地解析配置就能保证配置有效(参见解析,不要验证)。
在操作方面,早期验证确保程序在启动后的一段时间内不会因为配置无效而退出。从开发的角度看,它使工作变得更容易,因为您可以在任何地方假设配置数据只包含有效值,并且可以像使用程序中的任何其他对象一样安全地使用配置。
四.在使用配置的地方声明它
最后一个原则是,配置项应该声明在它们使用的地方附近,例如,作为使用它的代码所在模块中的一个类。
此规则不能直接从上述基础派生,因为它不一定有助于更有效地使用工具,也不一定有助于及早预防或报告错误。但是,与在一个地方声明所有配置条目相比,它在软件工程方面有两个优点:
物理封闭性有助于导航,例如,更容易找到使用某个配置项的位置。此外,如果您使用的数据结构还定义了配置值的有效边界,那么在接近依赖这些边界的代码旁定义配置是有意义的。
它有助于避免在不同的、不相关的组件中使用相同的配置项。假设您有一个条目,例如timeout,它定义在一个公共位置,并且可以从所有模块访问,那么很容易会去在不同的不相关位置重用同一个timeout条目,而不是添加一个新条目并适当地命名和记录它。和在模块中本地定义配置对比下,则更容易看出这样做不好,例如,您很可能不会在db.backend模块中导入web.http.config.client.timeout以将其用作数据库连接池的设置。
在测试以配置为参数的组件时,只需要为组件使用的条目创建模拟配置对象,而不需要为整个应用程序模拟完整的配置。
每个模块的子配置可以通过组合或继承组装成一个更大的类。一般来说,我建议组合,因为从多个小配置类继承可能在某个点上导致命名冲突。
把碎片拼在一起
所以让我们看看如何将这些原则组合成一个小的代码示例。这个例子深受Alexandru Nedelcu的Scala最佳实践集合第3.5节所述方法的启发。
我们有三个模块,每个模块都定义了类型良好的配置类。(为了简洁起见,我省略了import语句。)
例如,app.user模块中的类可以在构造函数中获取其本地配置类的实例并使用它,而不必担心类型不匹配或缺少值。用户模块中的单元测试不必模拟整个应用程序配置。
注意,数据类特别适合这个应用程序,因为它们不能声明成员而不初始化,这与普通的Python类相反。如果在dataclass声明中添加了一个成员,那么mypy将报告代码中在没有为新成员提供值的情况下构造实例的所有位置。
然后,位于不同模块中的主程序可以定义一个应用程序范围的配置类,如下所示:
到目前为止,我还没有讨论如何实际创建实例并对这个全局配置类执行验证。对于类似这样的简单情况,将字典转换为数据类的dacite库非常有用。请考虑以下代码:
如果执行此代码时没有异常,那么我们就有了一个有效的配置对象,如
我希望可以和您达成共识,上述都是传递配置数据的更好方法,而不仅仅是一个包含已解析的JSON内容的字典。
英文原文:https://tech.preferred.jp/en/blog/working-with-configuration-in-python/
译者:阿布铥
相关推荐
- 鸿蒙系统适配第三方机型(鸿蒙 第三方适配)
-
最新华为官方公布了鸿蒙系统3.0支持的机型名单,具体如下。鸿蒙系统3.0升级名单:1.Mate系列:MateXs2、MateX2、MateXs、Mate40、Mate40Pro、Mate...
- imei怎么下载(imei changer apk)
-
如果您的steam序列号激活了,可以尝试以下方法下载:1.使用steam自带的下载工具,如“下载工具”,在软件的“下载”选项卡中选择“序列号下载”。2.在下载页面中,选择要下载的游戏,然后点击“下...
- 电脑系统优化软件哪个好(系统优化软件排行榜)
-
有必要用,非常好用,WINDOWS优化大师是一个网络上下载率极高的系统维护软件。多年未曾清理过系统和硬盘的电脑,系统内部将产生大量的垃圾文件、临时文件、废旧程序等等win10系统不需要经常更新,关闭...
- 重装系统后硬盘不见了(重装系统后磁盘不见了)
-
硬盘不见可能是因为重装系统时未正确安装驱动程序或未对硬件进行正确设置。你可以按以下步骤排查问题:进入BIOS检查硬盘是否被识别,尝试重新连接数据线和电源线,更新或安装适当的硬件驱动程序,或者使用硬件故...
- 冰封u盘装win7系统教程图解(冰封u盘启动装机教程)
-
1.查找激活工具:通常来说,Win7冰封系统已经包含了必要的驱动,所以如果你的电脑上并没有出现设备错误,那你就可以正常使用。如果你需要添加任何驱动,请尝试从厂商下载相应的驱动并执行自动安装程序。如果...
- uefi模式下找不到硬盘(uefi引导找不到硬盘)
-
首先你的安装盘必须是从UEFI启动的,然后它才能安装为UEFI启动。(条件:Fat32文件系统,efi文件夹)其次你MBR+BIOS的系统想换成GPT+EFI的,分区得做一点改动,腾出来100M的空...
- win7怎么安装蓝牙驱动程序(win7电脑安装蓝牙驱动教程)
-
方法如下: 1、再开始里点击控制版面,点击【硬件和声音】找到【添加设备】 2、之后再选择你要添加的蓝牙耳机。 3、系统就会提示正在与蓝牙适配器连接,然后提示添加成功。 4、点击“开始”-“...
- 怎么装系统win7旗舰版(电脑怎么装win7旗舰版)
-
1、目前支持64位的Wincc版本有:WinccV7Sp3、WinccV11Sp2、WinccV12。2、Wincc的V11与V12两个版本不能共存,即不能同时安装在同一台电脑上。上述这两...
- 怎样恢复桌面图标原样(怎样恢复桌面图标原样显示)
-
桌面图标恢复原样步骤方法如下:1.右键点击图标、并点击‘属性’两字,再点击‘打开文件位置‘2.然后找到文件所在的位置,然后暂时将文件在一旁放着3.然后再去点击‘更换图标’这四个字,找到刚刚打开的文件...
- 消除u盘写保护(怎么解除u盘写保护)
-
如果您的U盘启用了写保护功能,可以尝试以下几种方法来解除写保护:1.检查U盘上的物理写保护开关:部分U盘有一个物理的写保护开关,可以在U盘的外壳上找到。请确保开关处于非写保护状态。2.使用CMD命...
- microsoft国际版(微软国际版bing)
-
win10系统自带的edge浏览器不分国内版和国际版。搜索引擎Bing分为国内版和国际版,区别如下一、迎合不同人群的需求。国际版显得更加个性,他迎合了不同人群的需求,在收缩上更加方便化和智能化,后期可...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
宝塔面板如何添加免费waf防火墙?(宝塔面板开启https)
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
慕ke 前端工程师2024「完整」
-
失业程序员复习python笔记——条件与循环
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
