龙空技术网

在Python应用程序中使用配置的最佳实践

Python部落 156

前言:

目前兄弟们对“winds引导配置数据文件包含的os项目无效”大概比较注意,大家都想要学习一些“winds引导配置数据文件包含的os项目无效”的相关文章。那么小编也在网上收集了一些对于“winds引导配置数据文件包含的os项目无效””的相关知识,希望你们能喜欢,小伙伴们一起来学习一下吧!

大多数计算机应用程序都可以使用配置来指定行为,无论是通过命令行标志、环境变量还是配置文件。作为一名软件开发人员,处理配置时会遇到一些挑战,例如解析不合法的输入、验证它以及在程序的任意位置访问它。以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内容的字典。

英文原文:

译者:阿布铥

标签: #winds引导配置数据文件包含的os项目无效