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 相关文章推荐
python3生成随机数实例
Oct 20 Python
python实现的文件同步服务器实例
Jun 02 Python
Python3连接MySQL(pymysql)模拟转账实现代码
May 24 Python
Python读取sqlite数据库文件的方法分析
Aug 07 Python
python利用OpenCV2实现人脸检测
Apr 16 Python
python同时遍历数组的索引和值的实例
Nov 15 Python
Python GUI编程 文本弹窗的实例
Jun 11 Python
Python Django Cookie 简单用法解析
Aug 13 Python
Python配置文件处理的方法教程
Aug 29 Python
python实现递归查找某个路径下所有文件中的中文字符
Aug 31 Python
Java多线程实现四种方式原理详解
Jun 02 Python
容易被忽略的Python内置类型
Sep 03 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中的escape函数
2013/06/29 PHP
php实现httpRequest的方法
2015/03/13 PHP
PHP的openssl加密扩展使用小结(推荐)
2016/07/18 PHP
kindeditor 加入七牛云上传的实例讲解
2017/11/12 PHP
Javascript 中的类和闭包
2010/01/08 Javascript
原生js操作checkbox用document.getElementById实现
2013/10/12 Javascript
返回上一页并自动刷新的JavaScript代码
2014/02/19 Javascript
php+js实现倒计时功能
2014/06/02 Javascript
js propertychange和oninput事件
2014/09/28 Javascript
Javascript中数组sort和reverse用法分析
2014/12/30 Javascript
7个有用的jQuery代码片段分享
2015/05/19 Javascript
JavaScript获取页面中超链接数量的方法
2015/11/09 Javascript
每天一篇javascript学习小结(面向对象编程)
2015/11/20 Javascript
JavaScript实现经典排序算法之插入排序
2016/12/28 Javascript
Angular2实现自定义双向绑定属性
2017/03/22 Javascript
微信小程序实现YDUI的ScrollNav组件
2018/02/02 Javascript
vue中element组件样式修改无效的解决方法
2018/02/03 Javascript
layui实现动态和静态分页
2018/04/28 Javascript
angularjs1.5 组件内用函数向外传值的实例
2018/09/30 Javascript
webpack结合express实现自动刷新的方法
2019/05/07 Javascript
小程序中设置缓存过期的实现方法
2020/01/14 Javascript
es6中Promise 对象基本功能与用法实例分析
2020/02/23 Javascript
关于小程序优化的一些建议(小结)
2020/12/10 Javascript
[58:25]VP vs RNG 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/17 DOTA
python读取文件名并改名字的实例
2019/01/07 Python
使用PyQtGraph绘制精美的股票行情K线图的示例代码
2019/03/14 Python
Python使用matplotlib 模块scatter方法画散点图示例
2019/09/27 Python
python turtle工具绘制四叶草的实例分享
2020/02/14 Python
Django如何实现密码错误报错提醒
2020/09/04 Python
美国宠物用品网站:Value Pet Supplies
2018/03/17 全球购物
世界顶级户外运动品牌折扣网站:LeftLane Sports
2019/06/12 全球购物
干部培训自我鉴定
2014/01/22 职场文书
安全生产检查通报
2014/01/29 职场文书
2014信息公开实施方案
2014/02/22 职场文书
党风廉政建设心得体会
2019/05/21 职场文书
python中sqllite插入numpy数组到数据库的实现方法
2021/06/21 Python