1. 程式人生 > 程式設計 >Python 使用 environs 庫定義環境變數的方法

Python 使用 environs 庫定義環境變數的方法

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 庫定義環境變數內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!