Python 使用 environs 库定义环境变量的方法
Environs是解析环境变量的Python库。它的开发受envparse启发,底层使用marshmallow验证并序列化值。
在运行一个项目的时候,我们经常会遇到设置不同环境的需求,如设置是开发环境、测试环境还是生产环境,或者在某些设置里面可能还需要设置一些变量开关,如设置调试开关、日志开关、功能开关等等。
这些变量其实就是在项目运行时我们给项目设置的一些参数。这些参数一般情况来说,可以有两种设置方法,一种是通过命令行参数,一种是通过环境变量。二者的适用范围不同,在不同的场景下我们可以选用更方便的方式来实现参数的设置。
本节我们以Python项目为例,说说环境变量的设置。
设置和获取环境变量
首先,我们先来了解一下在Python项目里面怎样设置和获取变量。
首先让我们定义一个最简单的Python文件,命名为main.py,内容如下:
importos print(os.environ['VAR1'])
在这里我们导入了os模块,它的environ对象里面就包含了当前运行状态下的所有环境变量,它其实是一个os._Environ对象,我们可以通过类似字典取值的方式从中获取里面包含的环境变量的值,如代码所示。
好,接下来我们什么也不设置,直接运行,看下结果:
python3main.py
结果如下:
raiseKeyError(key)fromNone
KeyError:'VAR1'
直接抛出来了一个错误,这很正常,我们此时并没有设置VAR1这个环境变量,当然会抛出键值异常的错误了。
接下来我们在命令行下进行设置,运行如下命令:
VAR1=germeypython3main.py
运行结果如下:
germey
可以看到我们在运行之前,在命令行之前通过键值对的形式对环境变量进行设置,程序就可以获取到VAR1这个值了,成功打印出来了germey。
但这个环境变量是永久的吗?我们这次再运行一遍原来的命令:
python3main.py
结果如下:
raiseKeyError(key)fromNone
KeyError:'VAR1'
嗯,又抛错了。
这说明了什么,在命令行的前面加上的这个环境变量声明只能对当前执行的命令生效。
好,那既然如此,我难道每次运行都要在命令行前面加上这些声明吗?那岂不麻烦死了。
当然有解决方法,我们使用export就可以了。
比如这里,我们执行如下命令:
exportVAR1=germey
执行完这个命令之后,当前运行环境下VAR1就被设置成功了,下面我们运行的命令都能获取到VAR1这个环境变量了。
下面来试试,还是执行原来的命令:
python3main.py
结果如下:
germey
可以,成功获取到了VAR1这个变量,后面我们运行的每一个命令就都会生效了。
但等一下,这个用了export就是永久生效了吗?
其实并不是,其实这个export只对当前的命令行运行环境生效,我们只要把命令行关掉再重新打开,之前用export设置的环境变量就都没有了。
可以试试,重新打开命令行,再次执行原来的命令,就会又抛出键值异常的错误了。
那又有同学会问了,我要在每次命令行运行时都想自动设置好环境变量怎么办呢?
这个就更好办了,只需要把export的这些命令加入到~/.bashrc文件里面就好了,每次打开命令行的时候,系统都会自动先执行以下这个脚本里面的命令,这样环境变量就设置成功了。当然这里面还有很多不同的文件,如~/.bash_profile、~/.zshrc、~/.profile、/etc/profile等等,其加载是有先后顺序的,大家感兴趣可以去了解下。
好了,扯远了,我们现在已经了解了如何设置环境变量和基本的环境变量获取方法了。
更安全的获取方式
但是上面的这种获取变量的方式实际上是非常不友好的,万一这个环境变量没设置好,那岂不是就报错了,这是很不安全的。
所以,下面再介绍几种比较友好的获取环境变量的方式,即使没有设置过,也不会报错。
我们可以把中括号取值的方式改成get方法,如下所示:
importos print(os.environ.get('VAR1'))
这样就不会报错了,如果VAR1没设置,会直接返回None,而不是直接报错。
另外我们也可以给get方法传入第二个参数,表示默认值,如下所示:
importos print(os.environ.get('VAR1','germey'))
这样即使我们如果设置过VAR1,他就会用germey这个字符串代替,这就完成了默认环境变量的设置。
下面还有几种获取环境变量的方式,总结如下:
importos print(os.getenv('VAR1','germey'))
这个方式比上面的写法更简单,功能完全一致。
弊端
但其实上面的方法有一个不方便的地方,如果我们想要设置非字符串类型的环境变量怎么办呢?比如设置int类型、float类型、list类型,可能我们的写法就会变成这个样子:
importos importjson VAR1=int(os.getenv('VAR1',1)) VAR2=float(os.getenv('VAR2',5.5)) VAR3=json.loads(os.getenv('VAR3'))
然后设置环境变量的时候就变成这样子:
exportVAR1=1 exportVAR2=2.3 exportVAR3='["1","2"]'
这样才能成功获取到结果,打印出来结果如下:
1
2.3
['1','2']
不过看下这个,写法也太奇葩了吧,又是类型转换,又是json解析什么的,有没有更好的方法来设置。
environs
当然有的,下面推荐一个environs库,利用它我们可以轻松地设置各种类型的环境变量。
这是一个第三方库,可以通过pip来安装:
pip3installenvirons
好,安装之后,我们再来体验一下使用environs来设置环境变量的方式。
fromenvironsimportEnv env=Env() VAR1=env.int('VAR1',1) VAR2=env.float('VAR2',5.5) VAR3=env.list('VAR3')
这里environs直接提供了int、float、list等方法,我们就不用再去进行类型转换了。
与此同时,设置环境变量的方式也有所变化:
exportVAR1=1 exportVAR2=2.3 exportVAR3=1,2
这里VAR3是列表,我们可以直接用逗号分隔开来。
打印结果如下:
1
2.3
['1','2']
官方示例
下面我们再看一个官方示例,这里示例了一些常见的用法。
首先我们来定义一些环境变量,如下:
exportGITHUB_USER=sloria exportMAX_CONNECTIONS=100 exportSHIP_DATE='1984-06-25' exportTTL=42 exportENABLE_LOGIN=true exportGITHUB_REPOS=webargs,konch,ped exportCOORDINATES=23.3,50.0 exportLOG_LEVEL=DEBUG
这里有字符串、有日期、有日志级别、有字符串列表、有浮点数列表、有布尔。
我们来看下怎么获取,写法如下:
fromenvironsimportEnv env=Env() env.read_env()#read.envfile,ifitexists #requiredvariables gh_user=env("GITHUB_USER")#=>'sloria' secret=env("SECRET")#=>raiseserrorifnotset #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 #providingadefaultvalue enable_login=env.bool("ENABLE_LOGIN",False)#=>True enable_feature_x=env.bool("ENABLE_FEATURE_X",False)#=>False #parsinglists 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(acceptsoptionalsubcastkeywordargument)
env.dict(acceptsoptionalsubcastkeywordargument)
env.json
env.datetime
env.date
env.timedelta(assumesvalueisanintegerinseconds)
env.url
env.uuid
env.log_level
env.path(caststoapathlib.Path)
这里list、dict、json、date、url、uuid、path个人认为都还是比较有用的,另外list、dict方法还有一个subcast方法可以对元素内容进行转化。
对于dict、url、date、uuid、path这里我们来补充说明一下。
下面我们定义这些类型的环境变量:
exportVAR_DICT=name=germey,age=25 exportVAR_JSON='{"name":"germey","age":25}' exportVAR_URL=https://cuiqingcai.com exportVAR_UUID=762c8d53-5860-4d5d-81bc-210bf2663d0e exportVAR_PATH=/var/py/env
需要注意的是,DICT的解析,需要传入的是逗号分隔的键值对,JSON的解析是需要传入序列化的字符串。
解析写法如下:
fromenvironsimportEnv 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)
运行结果如下:
{'name':'germey','age':'25'}
{'name':'germey','age':25}
ParseResult(scheme='https',netloc='cuiqingcai.com',path='',params='',query='',fragment='')
762c8d53-5860-4d5d-81bc-210bf2663d0e
/var/py/env
可以看到,它分别给我们转化成了dict、dict、ParseResult、UUID、PosixPath类型了。
在代码中直接使用即可。
文件读取
如果我们的一些环境变量是定义在文件中的,environs还可以进行读取和加载,默认会读取本地当前运行目录下的.env文件。
示例如下:
fromenvironsimportEnv 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
代码则可以这么定义:
fromenvironsimportEnv 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等,但在解析时,我们可以根据前缀进行分组处理,见下面的示例:
#exportMYAPP_HOST=lolcathost #exportMYAPP_PORT=3000 withenv.prefixed("MYAPP_"): host=env("HOST","localhost")#=>'lolcathost' port=env.int("PORT",5000)#=>3000 #nestedprefixesarealsosupported: #exportMYAPP_DB_HOST=lolcathost #exportMYAPP_DB_PORT=10101 withenv.prefixed("MYAPP_"): withenv.prefixed("DB_"): db_host=env("HOST","lolcathost") db_port=env.int("PORT",10101)
可以看到这里通过with和priefixed方法组合使用即可实现分区处理,这样在每个分组下再赋值到一个字典里面即可。
合法性验证
有些环境变量的传入是不可预知的,如果传入一些非法的环境变量很可能导致一些难以预料的问题。比如说一些可执行的命令,通过环境变量传进来,如果是危险命令,那么会非常危险。
所以在某些情况下我们需要验证传入的环境变量的有效性,看下面的例子:
#exportTTL=-2 #exportNODE_ENV='invalid' #exportEMAIL='^_^' fromenvironsimportEnv frommarshmallow.validateimportOneOf,Length,Email env=Env() #simplevalidator env.int("TTL",validate=lambdan:n>0) #=>Environmentvariable"TTL"invalid:['Invalidvalue.'] #usingmarshmallowvalidators env.str( "NODE_ENV", validate=OneOf( ["production","development"],error="NODE_ENVmustbeoneof:{choices}" ), ) #=>Environmentvariable"NODE_ENV"invalid:['NODE_ENVmustbeoneof:production,development'] #multiplevalidators env.str("EMAIL",validate=[Length(min=4),Email()]) #=>Environmentvariable"EMAIL"invalid:['Shorterthanminimumlength4.','Notavalidemailaddress.']
在这里,我们通过validate方法,并传入一些判断条件。如NODE_ENV只允许传入production和develpment其中之一;EMAIL必须符合email的格式。
这里依赖于marshmallow这个库,里面有很多验证条件,大家可以了解下。
如果不符合条件的,会直接抛错,例如:
marshmallow.exceptions.ValidationError:['Invalidvalue.']
关于marshmallow库的用法,大家可以参考:https://marshmallow.readthedocs.io/en/stable/,后面我也抽空写一下介绍下。
最后再附一点我平时定义环境变量的一些常见写法,如:
importplatform fromos.pathimportdirname,abspath,join fromenvironsimportEnv fromloguruimportlogger env=Env() env.read_env() #definitionofflags IS_WINDOWS=platform.system().lower()=='windows' #definitionofdirs ROOT_DIR=dirname(dirname(abspath(__file__))) LOG_DIR=join(ROOT_DIR,env.str('LOG_DIR','logs')) #definitionofenvironments DEV_MODE,TEST_MODE,PROD_MODE='dev','test','prod' APP_ENV=env.str('APP_ENV',DEV_MODE).lower() APP_DEBUG=env.bool('APP_DEBUG',TrueifAPP_ENV==DEV_MODEelseFalse) APP_DEV=IS_DEV=APP_ENV==DEV_MODE APP_PROD=IS_PROD=APP_DEV==PROD_MODE APP_TEST=IS_TEST=APP_ENV=TEST_MODE #redishost REDIS_HOST=env.str('REDIS_HOST','127.0.0.1') #redisport REDIS_PORT=env.int('REDIS_PORT',6379) #redispassword,ifnopassword,setittoNone REDIS_PASSWORD=env.str('REDIS_PASSWORD',None) #redisconnectionstring,likeredis://[password]@host:portorrediss://[password]@host:port REDIS_CONNECTION_STRING=env.str('REDIS_CONNECTION_STRING',None) #definitionofapi API_HOST=env.str('API_HOST','0.0.0.0') API_PORT=env.int('API_PORT',5555) API_THREADED=env.bool('API_THREADED',True) #definitionofflags 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='1week',retention='20days') logger.add(env.str('LOG_ERROR_FILE','error.log'),level='ERROR',rotation='1week')
这里定义了一些开发环境、日志路径、数据库连接、API设置、开关设置等等,是从我之前写的一个代理池项目拿来的,大家可以参考:https://github.com/Python3WebSpider/ProxyPool。
总结
到此这篇关于Python使用environs库来更好地定义环境变量的文章就介绍到这了,更多相关python使用environs库定义环境变量内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。