Python中配置環(huán)境變量的幾種姿勢
大家好,歡迎來到 Crossin的編程教室 !
在運行一個項目的時候,我們經(jīng)常會遇到設置不同環(huán)境的需求,如設置是開發(fā)環(huán)境、測試環(huán)境還是生產(chǎn)環(huán)境,或者在某些設置里面可能還需要設置一些變量開關(guān),如設置調(diào)試開關(guān)、日志開關(guān)、功能開關(guān)等等。
這些變量其實就是在項目運行時我們給項目設置的一些參數(shù)。這些參數(shù)一般情況來說,可以有兩種設置方法,一種是通過命令行參數(shù),一種是通過環(huán)境變量。二者的適用范圍不同,在不同的場景下我們可以選用更方便的方式來實現(xiàn)參數(shù)的設置。
本節(jié)我們以 Python 項目為例,說說環(huán)境變量的設置。
設置和獲取環(huán)境變量
首先,我們先來了解一下在 Python 項目里面怎樣設置和獲取變量。
首先讓我們定義一個最簡單的 Python 文件,命名為 main.py,內(nèi)容如下:
import os
print(os.environ['VAR1'])
在這里我們導入了 os 模塊,它的 environ 對象里面就包含了當前運行狀態(tài)下的所有環(huán)境變量,它其實是一個os._Environ對象,我們可以通過類似字典取值的方式從中獲取里面包含的環(huán)境變量的值,如代碼所示。
好,接下來我們什么也不設置,直接運行,看下結(jié)果:
python3 main.py
結(jié)果如下:
raise KeyError(key) from None
KeyError: 'VAR1'
直接拋出來了一個錯誤,這很正常,我們此時并沒有設置 VAR1 這個環(huán)境變量,當然會拋出鍵值異常的錯誤了。
接下來我們在命令行下進行設置,運行如下命令:
VAR1=germey python3 main.py
運行結(jié)果如下:
germey
可以看到我們在運行之前,在命令行之前通過鍵值對的形式對環(huán)境變量進行設置,程序就可以獲取到 VAR1 這個值了,成功打印出來了 germey。
但這個環(huán)境變量是永久的嗎?我們這次再運行一遍原來的命令:
python3 main.py
結(jié)果如下:
raise KeyError(key) from None
KeyError: 'VAR1'
嗯,又拋錯了。
這說明了什么,在命令行的前面加上的這個環(huán)境變量聲明只能對當前執(zhí)行的命令生效。
好,那既然如此,我難道每次運行都要在命令行前面加上這些聲明嗎?那豈不麻煩死了。
當然有解決方法,我們使用 export 就可以了。
比如這里,我們執(zhí)行如下命令:
export VAR1=germey
執(zhí)行完這個命令之后,當前運行環(huán)境下 VAR1 就被設置成功了,下面我們運行的命令都能獲取到 VAR1 這個環(huán)境變量了。
下面來試試,還是執(zhí)行原來的命令:
python3 main.py
結(jié)果如下:
germey
可以,成功獲取到了 VAR1 這個變量,后面我們運行的每一個命令就都會生效了。
但等一下,這個用了 export 就是永久生效了嗎?
其實并不是,其實這個 export 只對當前的命令行運行環(huán)境生效,我們只要把命令行關(guān)掉再重新打開,之前用 export 設置的環(huán)境變量就都沒有了。
可以試試,重新打開命令行,再次執(zhí)行原來的命令,就會又拋出鍵值異常的錯誤了。
那又有同學會問了,我要在每次命令行運行時都想自動設置好環(huán)境變量怎么辦呢?
這個就更好辦了,只需要把 export 的這些命令加入到~/.bashrc文件里面就好了,每次打開命令行的時候,系統(tǒng)都會自動先執(zhí)行以下這個腳本里面的命令,這樣環(huán)境變量就設置成功了。當然這里面還有很多不同的文件,如~/.bash_profile、~/.zshrc、~/.profile、/etc/profile等等,其加載是有先后順序的,大家感興趣可以去了解下。
好了,扯遠了,我們現(xiàn)在已經(jīng)了解了如何設置環(huán)境變量和基本的環(huán)境變量獲取方法了。
更安全的獲取方式
但是上面的這種獲取變量的方式實際上是非常不友好的,萬一這個環(huán)境變量沒設置好,那豈不是就報錯了,這是很不安全的。
所以,下面再介紹幾種比較友好的獲取環(huán)境變量的方式,即使沒有設置過,也不會報錯。
我們可以把中括號取值的方式改成 get 方法,如下所示:
import os
print(os.environ.get('VAR1'))
這樣就不會報錯了,如果 VAR1 沒設置,會直接返回 None,而不是直接報錯。
另外我們也可以給 get 方法傳入第二個參數(shù),表示默認值,如下所示:
import os
print(os.environ.get('VAR1', 'germey'))
這樣即使我們?nèi)绻O置過 VAR1,他就會用 germey 這個字符串代替,這就完成了默認環(huán)境變量的設置。
下面還有幾種獲取環(huán)境變量的方式,總結(jié)如下:
import os
print(os.getenv('VAR1', 'germey'))
這個方式比上面的寫法更簡單,功能完全一致。
弊端
但其實上面的方法有一個不方便的地方,如果我們想要設置非字符串類型的環(huán)境變量怎么辦呢?比如設置 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'))
然后設置環(huán)境變量的時候就變成這樣子:
export VAR1=1
export VAR2=2.3
export VAR3='["1", "2"]'
這樣才能成功獲取到結(jié)果,打印出來結(jié)果如下:
1
2.3
['1', '2']
不過看下這個,寫法也太奇葩了吧,又是類型轉(zhuǎn)換,又是 json 解析什么的,有沒有更好的方法來設置。
environs
當然有的,下面推薦一個 environs 庫,利用它我們可以輕松地設置各種類型的環(huán)境變量。
這是一個第三方庫,可以通過 pip 來安裝:
pip3 install environs
好,安裝之后,我們再來體驗一下使用 environs 來設置環(huán)境變量的方式。
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 等方法,我們就不用再去進行類型轉(zhuǎn)換了。
與此同時,設置環(huán)境變量的方式也有所變化:
export VAR1=1
export VAR2=2.3
export VAR3=1,2
這里 VAR3 是列表,我們可以直接用逗號分隔開來。
打印結(jié)果如下:
1
2.3
['1', '2']
官方示例
下面我們再看一個官方示例,這里示例了一些常見的用法。
首先我們來定義一些環(huán)境變量,如下:
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
這里有字符串、有日期、有日志級別、有字符串列表、有浮點數(shù)列表、有布爾。
我們來看下怎么獲取,寫法如下:
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]
通過觀察代碼可以發(fā)現(xiàn)它提供了這些功能:
通過 env 可以設置必需定義的變量,如果沒有定義,則會報錯。 通過 date、timedelta 方法可以對日期或時間進行轉(zhuǎn)化,轉(zhuǎn)成 datetime.date 或 timedelta 類型。 通過 log_level 方法可以對日志級別進行轉(zhuǎn)化,轉(zhuǎn)成 logging 里的日志級別定義。 通過 bool 方法可以對布爾類型變量進行轉(zhuǎn)化。 通過 list 方法可以對逗號分隔的內(nèi)容進行 list 轉(zhuǎn)化,并可以通過 subcast 方法對 list 的每個元素進行類型轉(zhuǎn)化。
可以說有了這些方法,定義各種類型的變量都不再是問題了。
支持類型
總的來說,environs 支持的轉(zhuǎn)化類型有這么多:
env.strenv.boolenv.intenv.floatenv.decimalenv.list(accepts optionalsubcastkeyword argument)env.dict(accepts optionalsubcastkeyword argument)env.jsonenv.datetimeenv.dateenv.timedelta(assumes value is an integer in seconds)env.urlenv.uuidenv.log_levelenv.path(casts to apathlib.Path)
這里 list、dict、json、date、url、uuid、path 個人認為都還是比較有用的,另外 list、dict 方法還有一個 subcast 方法可以對元素內(nèi)容進行轉(zhuǎn)化。
對于 dict、url、date、uuid、path 這里我們來補充說明一下。
下面我們定義這些類型的環(huán)境變量:
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)
運行結(jié)果如下:
<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
可以看到,它分別給我們轉(zhuǎn)化成了 dict、dict、ParseResult、UUID、PosixPath 類型了。
在代碼中直接使用即可。
文件讀取
如果我們的一些環(huán)境變量是定義在文件中的,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文件中寫入如下內(nèi)容:
APP_DEBUG=false
APP_ENV=prod
運行結(jié)果如下:
False
prod
沒問題,成功讀取。
當然我們也可以自定義讀取的文件,如.env.test文件,內(nèi)容如下:
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 傳入了定義環(huán)境變量的文件路徑即可。
前綴處理
environs 還支持前綴處理,一般來說我們定義一些環(huán)境變量,如數(shù)據(jù)庫的連接,可能有 host、port、password 等,但在定義環(huán)境變量的時候往往會加上對應的前綴,如 MYSQL_HOST、MYSQL_PORT、MYSQL_PASSWORD 等,但在解析時,我們可以根據(jù)前綴進行分組處理,見下面的示例:
# 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 方法組合使用即可實現(xiàn)分區(qū)處理,這樣在每個分組下再賦值到一個字典里面即可。
合法性驗證
有些環(huán)境變量的傳入是不可預知的,如果傳入一些非法的環(huán)境變量很可能導致一些難以預料的問題。比如說一些可執(zhí)行的命令,通過環(huán)境變量傳進來,如果是危險命令,那么會非常危險。
所以在某些情況下我們需要驗證傳入的環(huán)境變量的有效性,看下面的例子:
# 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.']
關(guān)于 marshmallow 庫的用法,大家可以參考:https://marshmallow.readthedocs.io/en/stable/,后面我也抽空寫一下介紹下。
最后再附一點我平時定義環(huán)境變量的一些常見寫法,如:
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')
這里定義了一些開發(fā)環(huán)境、日志路徑、數(shù)據(jù)庫連接、API 設置、開關(guān)設置等等,是從我之前寫的一個代理池項目拿來的,大家可以參考:https://github.com/Python3WebSpider/ProxyPool。
好了,以上就是一些環(huán)境變量的定義方法。
如果文章對你有幫助,歡迎轉(zhuǎn)發(fā)/點贊/收藏~
_往期文章推薦_
