16.3 集成命令行选项和环境变量
环境变量的一般规则是,它们和命令行选项及参数类似,属于配置输入。在大多数情况下,我们将很少更改的设置保存为环境变量。通常,我们会通过.bashrc或者.bash _ profile文件设置它们,这样每次我们登录时,对应的值都会被更新。我们可以考虑将环境变量设置在更全局的/etc/bashrc文件中,这样它们就会应用于所有的用户。我们也可以用命令行设置环境变量,但是这些设置只会保存到会话结束之前。
在某些情况下,我们所有的配置都可以在命令行中提供。在本例中,环境变量可以作为很少需要被更改的变量的一种选择。
在其他情况下,我们提供的配置值可能会被隔离到环境变量提供的设置中,这些设置不同于命令行选项提供的。我们可能需要从环境变量中获取一些值,然后将这些值与来自于命令行的一些值合并。
我们可以用环境变量来为配置对象设置默认值。在解析命令行参数前,我们希望可以先收集这些值。在这种情况下,命令行参数可以覆盖环境变量,有两种常用的实现方式。
- 在定义命令行参数时,显式设置值:这样做的优点是可以在帮助消息中显示默认值。它只针对环境变量和命令行选项重合的情况。我们可能希望用SIM _ SAMPLES环境变量提供一个可以被覆盖的默认值。
parser.add_argument( "—samples", action="store",
default=int(os.environ.get("SIM_SAMPLES",100)),
type=int, help="Samples to generate" )
- 在解析过程中隐式设置值:这种方式让我们可以很容易地将环境变量和命令行选项合并到一个单独的配置中。我们可以用默认值先生成一个命名空间,然后用从命令行解析生成的值来覆盖它。这为我们提供了3个级别的选项值:定义在解析器中的默认值、写入命名空间中的覆盖值和最后由命令行提供的覆盖值。
config4= argparse.Namespace()
config4.samples= int(os.environ.get("SIM_SAMPLES",100))
config4a= parser.parse_args( namespace=config4 )
| 参数解析器可以为非简单字符串的值提供类型转换。但是,收集环境变量不会自动触发类型转换。对于包含非字符串值的选项,我们必须在应用程序中执行类型转换。 |
16.3.1 提供更多的可配置默认值
我们可以将配置文件与环境变量和命令行选项进行合并,这为我们提供了3种为应用程序提供配置的方法。
- 配置文件的层次结构可以提供默认值。有关如何做到这一点的示例,可参见第13章“配置文件和持久化”。
- 环境变量可以提供覆盖配置文件的方法,这可能意味着需要将一个环境变量的命名空间翻译为配置文件的命名空间。
- 用命令行选项定义最后的覆盖操作。
使用全部3种方法可能不是一个好选择。如果有太多的地方需要搜索,追踪一个设置会变得麻烦。最终决定使用哪种方式提供配置通常需要与应用程序和框架的总体结构保持一致。我们应该努力使我们的程序和其他模块无缝结合。
我们会介绍本主题的两个小变化。第1个例子向我们展示了如何用环境变量覆盖配置文件设置。第2个例子向我们展示了如何用配置文件覆盖全局的环境变量配置。
16.3.2 用环境变量覆盖配置文件设置
我们会用3个阶段的处理过程来合并环境变量,并赋予它们比配置文件设置更高的优先级。首先,我们会从环境变量中创建一些默认设置。
env_values= [
("attribute_name", os.environ.get( "SOMEAPP_VARNAME", None )),
("another_name", os.environ.get( "SOMEAPP_OTHER", None )),
etc.
]
创建类似这样的映射可以将外部的环境变量名称(SOMEAPP VARNAME)改写为与我们的应用程序配置属性匹配的内部配置名称(attribute name)。对于没有定义的环境变量,会将None作为它们的默认值。稍后我们会单独介绍这部分。
接下来,我们会解析配置文件层次结构来获取后台配置信息。
configname= "someapp.yaml"
configlocations = (
os.path.curdir,
os.path.expanduser("~/"),
"/etc",
os.path.expanduser("~thisapp/"), # or thisapp.__file,
)
candidates = ( os.path.join(dir,config_name)
for dir in config_locations )
config_names = ( name for name in candidates if os.path.exists(name) )
files_values = [yaml.load(file) for file in config_names]
我们按照重要性顺序从高(用户所有的)到低(安装文件的一部分)创建了一个路径列表。对于每个确实存在的文件,解析文件的内容,然后创建从名称到值的映射。我们依赖于YAML标记,因为它很灵活并且容易理解。
我们可以用这些资源建立一个ChainMap对象的实例。
defaults= ChainMap( dict( (k,v) for k,v in env_values if v is not None
), *files_values )
我们将多个映射合并到一个ChainMap中。程序会首先搜索环境变量。当值存在于环境变量中时,程序会先在用户的配置文件中搜索该值,如果用户配置文件没有提供值,那么会接着在其他配置文件中搜索。
我们可以用下面的代码解析命令行参数并且更新这些默认值。
config= parser.parse_args( namespace=argparse.Namespace( **defaults ) )
我们将ChainMap配置文件的设置转换为一个argparse.Namespace对象。然后,我们解析命令行选项并更新这个命名空间对象。由于在ChainMap中环境变量最先出现,因此它们会覆盖所有的配置文件。
16.3.3 用配置文件覆盖环境变量
一些应用程序会将环境变量作为可以被配置文件覆盖的基础默认值。在本例中,我们会改变创建ChainMap的顺序。在上面的例子中,我们将环境变量放在第1个。我们可以将env _ config放在defaults.maps的最后,这样它就作为最后的选择。
defaults= ChainMap( *files_values )
defaults.maps.append( dict( (k,v) for k,v in env_values if v is not
None ) )
终于,我们可以使用下面的代码来解析命令行参数并更新这些默认值。
config= parser.parse_args( namespace=argparse.Namespace( **defaults )
)
我们将配置文件设置的ChainMap转换为了一个argparse.Namespace对象。然后,我们解析命令行参数来更新这个命名空间对象。由于环境变量位于ChainMap的最后,它们会提供任何配置文件中所缺少的值。
16.3.4 让配置文件理解None
这个三阶段设置环境变量的过程包含许多常见的参数和配置项的设置。我们并非总是需要环境变量、配置文件和命令行参数。一些应用程序可能只需要使用这些技术中的一小部分。
我们会经常需要保留None值的类型转换。保留None值可以确保我们能知道还没有设置环境变量。下面是一个更完整的类型转换方式,它是None-aware的。
def nint( x ):
if x is None: return x
return int(x)
我们可以在下面的上下文中使用这个nint()转换方法来获取环境变量。
env_values= [
('samples', nint(os.environ.get("SIM_SAMPLES", None)) ),
('stake', nint(os.environ.get( "SIM_STAKE", None )) ),
('rounds', nint(os.environ.get( "SIM_ROUNDS", None )) ),
]
如果某个环境变量没有被设置,就会使用None作为默认值。如果环境变量已经设置,那么这个值会被转换为一个整数。在后面的处理步骤中,我们可以基于None值用非None的正确的值来创建字典。
