Python 使用 environs 库定义环境变量的方法


Posted in Python onFebruary 25, 2020

Environs是解析环境变量的Python库。它的开发受envparse启发,底层使用marshmallow验证并序列化值。

在运行一个项目的时候,我们经常会遇到设置不同环境的需求,如设置是开发环境、测试环境还是生产环境,或者在某些设置里面可能还需要设置一些变量开关,如设置调试开关、日志开关、功能开关等等。

这些变量其实就是在项目运行时我们给项目设置的一些参数。这些参数一般情况来说,可以有两种设置方法,一种是通过命令行参数,一种是通过环境变量。二者的适用范围不同,在不同的场景下我们可以选用更方便的方式来实现参数的设置。

本节我们以 Python 项目为例,说说环境变量的设置。

设置和获取环境变量

首先,我们先来了解一下在 Python 项目里面怎样设置和获取变量。

首先让我们定义一个最简单的 Python 文件,命名为 main.py,内容如下:

import os
print(os.environ['VAR1'])

在这里我们导入了 os 模块,它的 environ 对象里面就包含了当前运行状态下的所有环境变量,它其实是一个 os._Environ 对象,我们可以通过类似字典取值的方式从中获取里面包含的环境变量的值,如代码所示。

好,接下来我们什么也不设置,直接运行,看下结果:

python3 main.py

结果如下:

raise KeyError(key) from None
KeyError: 'VAR1'

直接抛出来了一个错误,这很正常,我们此时并没有设置 VAR1 这个环境变量,当然会抛出键值异常的错误了。

接下来我们在命令行下进行设置,运行如下命令:

VAR1=germey python3 main.py

运行结果如下:

germey

可以看到我们在运行之前,在命令行之前通过键值对的形式对环境变量进行设置,程序就可以获取到 VAR1 这个值了,成功打印出来了 germey。

但这个环境变量是永久的吗?我们这次再运行一遍原来的命令:

python3 main.py

结果如下:

raise KeyError(key) from None
KeyError: 'VAR1'

嗯,又抛错了。

这说明了什么,在命令行的前面加上的这个环境变量声明只能对当前执行的命令生效。

好,那既然如此,我难道每次运行都要在命令行前面加上这些声明吗?那岂不麻烦死了。

当然有解决方法,我们使用 export 就可以了。

比如这里,我们执行如下命令:

export VAR1=germey

执行完这个命令之后,当前运行环境下 VAR1 就被设置成功了,下面我们运行的命令都能获取到 VAR1 这个环境变量了。

下面来试试,还是执行原来的命令:

python3 main.py

结果如下:

germey

可以,成功获取到了 VAR1 这个变量,后面我们运行的每一个命令就都会生效了。

但等一下,这个用了 export 就是永久生效了吗?

其实并不是,其实这个 export 只对当前的命令行运行环境生效,我们只要把命令行关掉再重新打开,之前用 export 设置的环境变量就都没有了。

可以试试,重新打开命令行,再次执行原来的命令,就会又抛出键值异常的错误了。

那又有同学会问了,我要在每次命令行运行时都想自动设置好环境变量怎么办呢?

这个就更好办了,只需要把 export 的这些命令加入到 ~/.bashrc 文件里面就好了,每次打开命令行的时候,系统都会自动先执行以下这个脚本里面的命令,这样环境变量就设置成功了。当然这里面还有很多不同的文件,如 ~/.bash_profile 、~/.zshrc 、~/.profile、/etc/profile 等等,其加载是有先后顺序的,大家感兴趣可以去了解下。

好了,扯远了,我们现在已经了解了如何设置环境变量和基本的环境变量获取方法了。

更安全的获取方式

但是上面的这种获取变量的方式实际上是非常不友好的,万一这个环境变量没设置好,那岂不是就报错了,这是很不安全的。

所以,下面再介绍几种比较友好的获取环境变量的方式,即使没有设置过,也不会报错。

我们可以把中括号取值的方式改成 get 方法,如下所示:

import os
print(os.environ.get('VAR1'))

这样就不会报错了,如果 VAR1 没设置,会直接返回 None,而不是直接报错。

另外我们也可以给 get 方法传入第二个参数,表示默认值,如下所示:

import os
print(os.environ.get('VAR1', 'germey'))

这样即使我们如果设置过 VAR1,他就会用 germey 这个字符串代替,这就完成了默认环境变量的设置。

下面还有几种获取环境变量的方式,总结如下:

import os
print(os.getenv('VAR1', 'germey'))

这个方式比上面的写法更简单,功能完全一致。

弊端

但其实上面的方法有一个不方便的地方,如果我们想要设置非字符串类型的环境变量怎么办呢?比如设置 int 类型、float 类型、list 类型,可能我们的写法就会变成这个样子:

import os
import json

VAR1 = int(os.getenv('VAR1', 1))
VAR2 = float(os.getenv('VAR2', 5.5))
VAR3 = json.loads(os.getenv('VAR3'))

然后设置环境变量的时候就变成这样子:

export VAR1=1
export VAR2=2.3
export VAR3='["1", "2"]'

这样才能成功获取到结果,打印出来结果如下:

1
2.3
['1', '2']

不过看下这个,写法也太奇葩了吧,又是类型转换,又是 json 解析什么的,有没有更好的方法来设置。

environs

当然有的,下面推荐一个 environs 库,利用它我们可以轻松地设置各种类型的环境变量。

这是一个第三方库,可以通过 pip 来安装:

pip3 install environs

好,安装之后,我们再来体验一下使用 environs 来设置环境变量的方式。

from environs import Env

env = Env()
VAR1 = env.int('VAR1', 1)
VAR2 = env.float('VAR2', 5.5)
VAR3 = env.list('VAR3')

这里 environs 直接提供了 int、float、list 等方法,我们就不用再去进行类型转换了。

与此同时,设置环境变量的方式也有所变化:

export VAR1=1
export VAR2=2.3
export VAR3=1,2

这里 VAR3 是列表,我们可以直接用逗号分隔开来。

打印结果如下:

1
2.3
['1', '2']

官方示例

下面我们再看一个官方示例,这里示例了一些常见的用法。

首先我们来定义一些环境变量,如下:

export GITHUB_USER=sloria
export MAX_CONNECTIONS=100
export SHIP_DATE='1984-06-25'
export TTL=42
export ENABLE_LOGIN=true
export GITHUB_REPOS=webargs,konch,ped
export COORDINATES=23.3,50.0
export LOG_LEVEL=DEBUG

这里有字符串、有日期、有日志级别、有字符串列表、有浮点数列表、有布尔。

我们来看下怎么获取,写法如下:

from environs import Env

env = Env()
env.read_env() # read .env file, if it exists
# required variables
gh_user = env("GITHUB_USER") # => 'sloria'
secret = env("SECRET") # => raises error if not set

# casting
max_connections = env.int("MAX_CONNECTIONS") # => 100
ship_date = env.date("SHIP_DATE") # => datetime.date(1984, 6, 25)
ttl = env.timedelta("TTL") # => datetime.timedelta(0, 42)
log_level = env.log_level("LOG_LEVEL") # => logging.DEBUG

# providing a default value
enable_login = env.bool("ENABLE_LOGIN", False) # => True
enable_feature_x = env.bool("ENABLE_FEATURE_X", False) # => False

# parsing lists
gh_repos = env.list("GITHUB_REPOS") # => ['webargs', 'konch', 'ped']
coords = env.list("COORDINATES", subcast=float) # => [23.3, 50.0]

通过观察代码可以发现它提供了这些功能:

  • 通过 env 可以设置必需定义的变量,如果没有定义,则会报错。
  • 通过 date、timedelta 方法可以对日期或时间进行转化,转成 datetime.date 或 timedelta 类型。
  • 通过 log_level 方法可以对日志级别进行转化,转成 logging 里的日志级别定义。
  • 通过 bool 方法可以对布尔类型变量进行转化。
  • 通过 list 方法可以对逗号分隔的内容进行 list 转化,并可以通过 subcast 方法对 list 的每个元素进行类型转化。

可以说有了这些方法,定义各种类型的变量都不再是问题了。

支持类型

总的来说,environs 支持的转化类型有这么多:

env.str
env.bool
env.int
env.float
env.decimal
env.list (accepts optional subcast keyword argument)
env.dict (accepts optional subcast keyword argument)
env.json
env.datetime
env.date
env.timedelta (assumes value is an integer in seconds)
env.url
env.uuid
env.log_level
env.path (casts to a pathlib.Path)

这里 list、dict、json、date、url、uuid、path 个人认为都还是比较有用的,另外 list、dict 方法还有一个 subcast 方法可以对元素内容进行转化。

对于 dict、url、date、uuid、path 这里我们来补充说明一下。

下面我们定义这些类型的环境变量:

export VAR_DICT=name=germey,age=25
export VAR_JSON='{"name": "germey", "age": 25}'
export VAR_URL=https://cuiqingcai.com
export VAR_UUID=762c8d53-5860-4d5d-81bc-210bf2663d0e
export VAR_PATH=/var/py/env

需要注意的是,DICT 的解析,需要传入的是逗号分隔的键值对,JSON 的解析是需要传入序列化的字符串。

解析写法如下:

from environs import Env

env = Env()
VAR_DICT = env.dict('VAR_DICT')
print(type(VAR_DICT), VAR_DICT)

VAR_JSON = env.json('VAR_JSON')
print(type(VAR_JSON), VAR_JSON)

VAR_URL = env.url('VAR_URL')
print(type(VAR_URL), VAR_URL)

VAR_UUID = env.uuid('VAR_UUID')
print(type(VAR_UUID), VAR_UUID)

VAR_PATH = env.path('VAR_PATH')
print(type(VAR_PATH), VAR_PATH)

运行结果如下:

<class 'dict'> {'name': 'germey', 'age': '25'}
<class 'dict'> {'name': 'germey', 'age': 25}
<class 'urllib.parse.ParseResult'> ParseResult(scheme='https', netloc='cuiqingcai.com', path='', params='', query='', fragment='')
<class 'uuid.UUID'> 762c8d53-5860-4d5d-81bc-210bf2663d0e
<class 'pathlib.PosixPath'> /var/py/env

可以看到,它分别给我们转化成了 dict、dict、ParseResult、UUID、PosixPath 类型了。

在代码中直接使用即可。

文件读取

如果我们的一些环境变量是定义在文件中的,environs 还可以进行读取和加载,默认会读取本地当前运行目录下的 .env 文件。

示例如下:

from environs import Env

env = Env()
env.read_env()
APP_DEBUG = env.bool('APP_DEBUG')
APP_ENV = env.str('APP_ENV')
print(APP_DEBUG)
print(APP_ENV)

下面我们在 .env 文件中写入如下内容:

APP_DEBUG=false
APP_ENV=prod

运行结果如下:

False
prod

没问题,成功读取。

当然我们也可以自定义读取的文件,如 .env.test 文件,内容如下:

APP_DEBUG=false
APP_ENV=test

代码则可以这么定义:

from environs import Env

env = Env()
env.read_env(path='.env.test')
APP_DEBUG = env.bool('APP_DEBUG')
APP_ENV = env.str('APP_ENV')

这里就通过 path 传入了定义环境变量的文件路径即可。

前缀处理

environs 还支持前缀处理,一般来说我们定义一些环境变量,如数据库的连接,可能有 host、port、password 等,但在定义环境变量的时候往往会加上对应的前缀,如 MYSQL_HOST、MYSQL_PORT、MYSQL_PASSWORD 等,但在解析时,我们可以根据前缀进行分组处理,见下面的示例:

# export MYAPP_HOST=lolcathost
# export MYAPP_PORT=3000
 
with env.prefixed("MYAPP_"):
 host = env("HOST", "localhost") # => 'lolcathost'
 port = env.int("PORT", 5000) # => 3000
 
# nested prefixes are also supported:
 
# export MYAPP_DB_HOST=lolcathost
# export MYAPP_DB_PORT=10101
 
with env.prefixed("MYAPP_"):
 with env.prefixed("DB_"):
  db_host = env("HOST", "lolcathost")
  db_port = env.int("PORT", 10101)

可以看到这里通过 with 和 priefixed 方法组合使用即可实现分区处理,这样在每个分组下再赋值到一个字典里面即可。

合法性验证

有些环境变量的传入是不可预知的,如果传入一些非法的环境变量很可能导致一些难以预料的问题。比如说一些可执行的命令,通过环境变量传进来,如果是危险命令,那么会非常危险。

所以在某些情况下我们需要验证传入的环境变量的有效性,看下面的例子:

# export TTL=-2
# export NODE_ENV='invalid'
# export EMAIL='^_^'
 
from environs import Env
from marshmallow.validate import OneOf, Length, Email
 
env = Env()
 
# simple validator
env.int("TTL", validate=lambda n: n > 0)
# => Environment variable "TTL" invalid: ['Invalid value.']
 
# using marshmallow validators
env.str(
 "NODE_ENV",
 validate=OneOf(
  ["production", "development"], error="NODE_ENV must be one of: {choices}"
 ),
)
# => Environment variable "NODE_ENV" invalid: ['NODE_ENV must be one of: production, development']
 
# multiple validators
env.str("EMAIL", validate=[Length(min=4), Email()])
# => Environment variable "EMAIL" invalid: ['Shorter than minimum length 4.', 'Not a valid email address.']

在这里,我们通过 validate 方法,并传入一些判断条件。如 NODE_ENV 只允许传入 production 和 develpment 其中之一;EMAIL 必须符合 email 的格式。

这里依赖于 marshmallow 这个库,里面有很多验证条件,大家可以了解下。

如果不符合条件的,会直接抛错,例如:

marshmallow.exceptions.ValidationError: ['Invalid value.']

关于 marshmallow 库的用法,大家可以参考:https://marshmallow.readthedocs.io/en/stable/,后面我也抽空写一下介绍下。

最后再附一点我平时定义环境变量的一些常见写法,如:

import platform
from os.path import dirname, abspath, join
from environs import Env
from loguru import logger
 
env = Env()
env.read_env()
 
# definition of flags
IS_WINDOWS = platform.system().lower() == 'windows'
 
# definition of dirs
ROOT_DIR = dirname(dirname(abspath(__file__)))
LOG_DIR = join(ROOT_DIR, env.str('LOG_DIR', 'logs'))
 
# definition of environments
DEV_MODE, TEST_MODE, PROD_MODE = 'dev', 'test', 'prod'
APP_ENV = env.str('APP_ENV', DEV_MODE).lower()
APP_DEBUG = env.bool('APP_DEBUG', True if APP_ENV == DEV_MODE else False)
APP_DEV = IS_DEV = APP_ENV == DEV_MODE
APP_PROD = IS_PROD = APP_DEV == PROD_MODE
APP_TEST = IS_TEST = APP_ENV = TEST_MODE
 
# redis host
REDIS_HOST = env.str('REDIS_HOST', '127.0.0.1')
# redis port
REDIS_PORT = env.int('REDIS_PORT', 6379)
# redis password, if no password, set it to None
REDIS_PASSWORD = env.str('REDIS_PASSWORD', None)
# redis connection string, like redis://[password]@host:port or rediss://[password]@host:port
REDIS_CONNECTION_STRING = env.str('REDIS_CONNECTION_STRING', None)
 
# definition of api
API_HOST = env.str('API_HOST', '0.0.0.0')
API_PORT = env.int('API_PORT', 5555)
API_THREADED = env.bool('API_THREADED', True)
 
# definition of flags
ENABLE_TESTER = env.bool('ENABLE_TESTER', True)
ENABLE_GETTER = env.bool('ENABLE_GETTER', True)
ENABLE_SERVER = env.bool('ENABLE_SERVER', True)
 
# logger
logger.add(env.str('LOG_RUNTIME_FILE', 'runtime.log'), level='DEBUG', rotation='1 week', retention='20 days')
logger.add(env.str('LOG_ERROR_FILE', 'error.log'), level='ERROR', rotation='1 week')

这里定义了一些开发环境、日志路径、数据库连接、API 设置、开关设置等等,是从我之前写的一个代理池项目拿来的,大家可以参考:https://github.com/Python3WebSpider/ProxyPool。

总结

到此这篇关于Python 使用 environs 库来更好地定义环境变量的文章就介绍到这了,更多相关python 使用 environs 库定义环境变量内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python高并发异步服务器核心库forkcore使用方法
Nov 26 Python
Python实现telnet服务器的方法
Jul 10 Python
Python语言描述KNN算法与Kd树
Dec 13 Python
python kmeans聚类简单介绍和实现代码
Feb 23 Python
python 常用的基础函数
Jul 10 Python
Sanic框架流式传输操作示例
Jul 18 Python
Python之NumPy(axis=0 与axis=1)区分详解
May 27 Python
python 统计文件中的字符串数目示例
Dec 24 Python
django的403/404/500错误自定义页面的配置方式
May 21 Python
python的launcher用法知识点总结
Aug 07 Python
python如何利用traceback获取详细的异常信息
Jun 05 Python
Python实现PIL图像处理库绘制国际象棋棋盘
Jul 16 Python
Python 序列化和反序列化库 MarshMallow 的用法实例代码
Feb 25 #Python
python用pip install时安装失败的一系列问题及解决方法
Feb 24 #Python
python numpy--数组的组合和分割实例
Feb 24 #Python
python梯度下降算法的实现
Feb 24 #Python
利用python实现逐步回归
Feb 24 #Python
python数据分析:关键字提取方式
Feb 24 #Python
python数据预处理 :数据共线性处理详解
Feb 24 #Python
You might like
php下获取Discuz论坛登录用户名、用户组、用户ID等信息的实现代码
2010/12/29 PHP
php对关联数组循环遍历的实现方法
2015/03/13 PHP
php实现大文件断点续传下载实例代码
2019/10/01 PHP
轻轻松松学JS调试(不下载任何工具)
2010/04/14 Javascript
利用JQuery的load函数动态加载其它页面的内容的实现代码
2010/12/14 Javascript
js的压缩及jquery压缩探讨(提高页面加载性能/保护劳动成果)
2013/01/29 Javascript
jQuery中setTimeout的几种使用方法小结
2013/04/07 Javascript
使用Jquery实现点击文字后变成文本框且可修改
2013/09/21 Javascript
表单序列化与jq中的serialize使用示例
2014/02/21 Javascript
简单谈谈node.js 版本控制 nvm和 n
2015/10/15 Javascript
MVC Ajax Helper或Jquery异步加载部分视图
2015/11/29 Javascript
JS中with的替代方法与String中的正则方法详解
2016/12/23 Javascript
jquery实现的table排序功能示例
2017/03/10 Javascript
Javascript实现数组中的元素上下移动
2017/04/28 Javascript
纯js实现的积木(div层)拖动功能示例
2017/07/19 Javascript
JavaScript简单实现合并两个Json对象的方法示例
2017/10/16 Javascript
Vue-Router2.X多种路由实现方式总结
2018/02/09 Javascript
element-ui中Table表格省市区合并单元格的方法实现
2019/08/07 Javascript
vue使用exif获取图片经纬度的示例代码
2020/12/11 Vue.js
Vue实现小购物车功能
2020/12/21 Vue.js
Python基于Tkinter的HelloWorld入门实例
2015/06/17 Python
Python数据结构与算法之图结构(Graph)实例分析
2017/09/05 Python
Tensorflow实现AlexNet卷积神经网络及运算时间评测
2018/05/24 Python
python入门:这篇文章带你直接学会python
2018/09/14 Python
对python实现合并两个排序链表的方法详解
2019/01/23 Python
python创造虚拟环境方法总结
2019/03/04 Python
解决Python数据可视化中文部分显示方块问题
2020/05/16 Python
eDreams澳大利亚:预订机票、酒店和度假产品
2017/04/19 全球购物
Ancheer官方户外和运动商店:销售电动自行车
2019/08/07 全球购物
请问如下代码执行后a和b的值分别是什么
2016/05/05 面试题
运动会邀请函范文
2014/01/31 职场文书
毕业生个人求职自荐信
2014/02/26 职场文书
经济贸易系毕业生求职信
2014/05/31 职场文书
小学国庆节活动方案策划书
2014/09/16 职场文书
财会专业大学生求职信
2014/09/26 职场文书
募捐感谢信
2015/01/22 职场文书