HOWTO · Python

在 Python 中將字串轉換為布林值

在 Python 中使用 bool()、明確的真/假規則、列表推導、map()、strtobool() 和 json.loads() 將字串轉換為布林值,並了解為什麼 bool("False") 會返回 True 以及 eval() 不安全。

本頁內容

來自檔案、設定、環境變數或使用者輸入的文字,會把布林值寫成 "true" 和 "false" 這樣的字串,而把這些文字轉換回真正的 True 或 False,取決於你所處的情形。有時你只需要真值性(truthiness)——即是否存在任何文字。有時文字來自一組可以預先列出的固定標記。有時文字是帶有自身嚴格寫法的 JSON 字面值。選錯轉換方法是經典的 bug 來源,因為最順手的工具 bool() 從不讀取文字:bool("False") 返回 True。

本教學按這個順序介紹各種轉換方法:用於真值性的 bool()、用於已知標記的明確規則、用推導式或 map() 把同樣的規則應用於列表、帶有確切可用範圍和返回值界定的舊有 strtobool()、用於 JSON 文字的 json.loads(),以及最後關於 eval() 的警告。

使用 bool() 將字串轉換為布林值

bool() 函式將 Python 的真值性規則應用於字串。每個非空字串都是 True,空字串是 False。字串的內容不起任何作用:

print(bool("False"))
print(bool("True"))
print(bool(""))
print(bool("hello"))

輸出:

True
True
False
True

只有當問題是「是否存在」時才使用 bool()——例如某個可選設定是否已提供。一旦文字本身承載含義,如上面的 "False",bool() 就會悄悄給出錯誤答案,此時你需要改用明確規則。

使用明確規則轉換已知的文字值

當你知道輸入可能包含哪些標記時,就把文字與恰好這些標記進行比較。先統一大小寫並去除兩端空白,對任何無法識別的內容拋出錯誤而不是猜測,這樣設定檔案中的拼寫錯誤會立即暴露,而不是悄悄變成 False:

def parse_bool(value):
    text = value.strip().lower()
    if text in ("true", "yes", "on", "1"):
        return True
    if text in ("false", "no", "off", "0"):
        return False
    raise ValueError(f"unrecognized boolean string: {value!r}")


print(parse_bool("True"))
print(parse_bool(" off "))
print(parse_bool("YES"))

輸出:

True
False
True

超出接受集合的輸入會拋出錯誤,呼叫方可以捕獲並報告它:

try:
    parse_bool("maybe")
except ValueError as error:
    print(error)

輸出:

unrecognized boolean string: 'maybe'

按你的資料來源調整這兩個標記集合:CSV 匯出可能只有 "true" 和 "false",而命令列旗標還可能允許 "on" 和 "off"。集合是明確的,這正是這種方法比 bool() 更安全的原因。

使用列表推導轉換字串列表

比較本身已經產生布林值,因此列表推導可以對每個元素應用比較來轉換整個列表。這是本教學原始版本範例的直接延續,使用相同的資料:

words = ["False", "True", "False", "False", "True"]
flags = [word == "True" for word in words]
print(flags)

輸出:

[False, True, False, False, True]

當列表可能包含多種標記寫法時,重用上一節的明確轉換函式,而不是單一比較:

flags = [parse_bool(word) for word in ["true", "False", "yes", "off"]]
print(flags)

輸出:

[True, False, True, False]

使用 parse_bool 時,無法識別的元素會拋出 ValueError 並指出錯誤的值,而不是悄悄以 False 落入結果。

使用 map() 作為列表轉換的替代方案

map() 將函式應用於可迭代物件的每個元素,因此當你傳入一個單參數函式時,它執行相同的轉換。使用 lambda 時,它與推導式互為鏡像:

words = ["False", "True", "False", "False", "True"]
flags = list(map(lambda word: word == "True", words))
print(flags)

輸出:

[False, True, False, False, True]

結果與列表推導完全相同。當轉換像這裡一樣內聯書寫時,優先使用推導式;當轉換函式已經作為命名函式存在時,map() 更易讀,例如 list(map(parse_bool, words)),它無需 lambda 即可把明確規則應用於每個元素。

將 strtobool() 作為舊有或依賴特定的選項處理

舊程式碼經常使用標準庫 distutils.util 模組中的 strtobool()。它的界定是精確的,在依賴它之前很重要。該函式在 Python 3.11 及之前屬於標準庫;distutils 在 Python 3.10 中根據 PEP 632 被棄用,並在 Python 3.12 中被移除,因此在目前的直譯器上,除非 setuptools 提供它自己的副本(見下文),否則匯入會失敗。Python 3.11 文件精確地定義了其行為:輸入會被轉為小寫,真值為 'y'、'yes'、't'、'true'、'on' 和 '1',假值為 'n'、'no'、'f'、'false'、'off' 和 '0',任何其他值都會拋出 ValueError。標準庫版本返回整數 1 和 0,而不是布林值,因此當你需要真正的布林值時,用 bool() 包裹結果:

from distutils.util import strtobool

print(strtobool("Yes"))
print(bool(strtobool("Yes")))
print(strtobool("off"))

輸出:

1
True
0

該函式還以 vendored 副本的形式存留在 setuptools 中,許多專案已經依賴它。它接受相同的值並拋出相同的 ValueError(例如 invalid truth value 'abc'),但目前的 setuptools 版本直接返回 True 和 False——已使用 setuptools 84.2.1 驗證:

from setuptools._distutils.util import strtobool

print(strtobool("Yes"))

輸出:

True

由於返回類型、甚至模組的來源在標準庫副本、setuptools 副本,以及 setuptools 在其存在時安裝的 distutils 墊片之間各不相同,應將 strtobool() 視為舊有或依賴特定的選項:在有界的執行時上維護已經使用它的程式碼時可以接受,但對於新程式碼,上面的明確規則函式沒有任何依賴,並且在每個 Python 版本上行為一致。

使用 json.loads() 解析 JSON 布林字面值

當字串來自 JSON 資料時,讓 JSON 解析器來轉換它。JSON 只用小寫書寫它的布林字面值,而 json.loads() 會為它們返回真正的布林值:

import json

print(json.loads("true"))
print(json.loads("false"))

輸出:

True
False

Python 風格的大寫寫法不是有效的 JSON,因此對 "True" 的相同呼叫會失敗而不是轉換:

import json

json.loads("True")

輸出:

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

請注意,json.loads() 解析任何 JSON 值,而不僅僅是布林值;如果輸入不可信,在將其用作布林值之前,檢查結果是否確實是布林值。

為什麼 eval() 對字串轉換不安全

eval() 看起來能轉換字串,因為它將字串作為 Python 運算式求值,而 True 和 False 是有效的運算式:

print(eval("False"))

輸出:

False

這正是問題所在:eval() 不是轉換字串,而是執行它。像 "__import__('os').getcwd()" 這樣的值不會被拒絕——它會以你的程式的權限被執行,而惡意值可以執行任何程式碼。切勿將來自檔案、網路或使用者的文字傳遞給 eval()。如果你的字串保證是 Python 字面值 "True" 和 "False",ast.literal_eval() 會在不執行任何內容的情況下解析它們,並拒絕運算式:

import ast

print(ast.literal_eval("True"))
print(ast.literal_eval("False"))
try:
    ast.literal_eval("__import__('os').getcwd()")
except ValueError:
    print("ValueError")

輸出:

True
False
ValueError

在實踐中,對標記文字使用明確規則,對 JSON 文字使用 json.loads(),僅對保證是 Python 字面值的文字使用 ast.literal_eval()——並將 eval() 完全排除在轉換程式碼之外。