[译]Python与TOML:新朋友的最佳实践 (2) Python操作TOML指南

文章目录

  • Python 和 TOML:新最好的朋友
  • 使用Python加载TOML
  • 使用tomli 或 tomllib 读取TOML文档
  • 比较TOML类型和Python类型
  • 在项目中使用配置文件
  • 将Python对象转换为TOML
  • 将字典转换为 TOML
  • 通过tomli_w 写TOML文档
  • 创建新的TOML文件
  • 格式和样式
  • 用`tomlkit` 从头开始创建 TOML
  • 更新现有的TOML文件
  • 将 TOML 表示为 tomlkit 对象
  • 无损读取和写入 TOML
  • 总结

  • Python 和 TOML:新最好的朋友

    原文:《Python and TOML: New Best Friends》

    使用Python加载TOML

    使用tomlitomlib加载TOML文档。

    使用tomli 或 tomllib 读取TOML文档

    在python3.11中,TOML支持已经内置在标准库中,tomllib模块可以读取和解析TOML文档。
    如果你使用的是Python3.11及以后的版本,可以直接使用tomllib模块 替代tomli

    我们先创建一个TOML文件tic_tac_toe.toml

    # tic_tac_toe.toml
    
    [user]
    player_x.color = "blue"
    player_o.color = "green"
    
    [constant]
    board_size = 3
    
    [server]
    url = "https://tictactoe.example.com"
    

    用 pip 安装 tomli

    python -m pip install tomli
    

    tomli 模块仅公开两个函数:

  • load() :从文件对象加载 TOML
  • loads() :从字符串加载 TOML 文档。
  • 首先用load()函数加载TOML文件:

    import tomli
    with open("tic_tac_toe.toml", mode="rb") as fp:
        config = tomli.load(fp)
    

    然后可以查看config的内容:

    >>> config
    {'user': {'player_x': {'color': 'blue'}, 'player_o': {'color': 'green'}},
     'constant': {'board_size': 3},
     'server': {'url': 'https://tictactoe.example.com'}}
    
    >>> config["user"]["player_o"]
    {'color': 'green'}
    
    >>> config["server"]["url"]
    'https://tictactoe.example.com'
    

    TOML 文档在 Python 中表示为字典。TOML 文件中的所有表和子表都显示为嵌套字典

    如果已经将 TOML 文档表示为字符串,则可以使用 loads()代替 load()

    >>> import tomli
    >>> toml_str = """
    ... offset_date-time_utc = 2021-01-12 00:23:45Z
    ... potpourri = ["flower", 1749, { symbol = "X", color = "blue" }, 1994-02-14]
    ... """
    
    >>> tomli.loads(toml_str)
    {'offset_date-time_utc': datetime.datetime(2021, 1, 12, 0, 23, 45,
                                               tzinfo=datetime.timezone.utc),
     'potpourri': ['flower',
                   1749,
                   {'symbol': 'X', 'color': 'blue'},
                   datetime.date(1994, 2, 14)]}
    

    注意,TOML 时间和日期类型由 Python datetime 的类型表示

    load()loads()之间的一个区别是,当您使用loads()时,您使用的是常规字符串而不是字节。在这种情况下, tomli 假设您已正确处理编码。

    如果你希望自动设置tomli和tomllib。如果Python版本是3.11及以后的版本,则直接使用tomllib模块,否则安装并使用tomli模块。
    requirements.txt写入:

    tomli >= 1.1.0 ; python_version < "3.11"
    

    在导入模块时:

    try:
        import tomllib
    except ModuleNotFoundError:
        import tomli as tomllib
    

    比较TOML类型和Python类型

  • TOML 文件必须是有效的 UTF-8 编码的 Unicode 文档。
  • 任意 64 位有符号整数(从 −2^63 到 2^63−1)应被接受并无损处理。
  • 浮点数应实现为 IEEE 754 binary64 值。
  • 一般来说,TOML的需求与Python对相应类型的实现非常匹配。Python 在处理文件时通常默认使用 UTF-8,而 Python float 遵循 IEEE 754。Python 的 int 类实现了任意精度的整数,这些整数可以处理所需的范围和更大的数字。

    对于像 和 tomllib 这样的 tomli 基本库,TOML 的数据类型和 Python 的数据类型之间的映射是很自然的。您可以在tomllib 文档中找到以下转换表:

    TOML Python
    string str
    integer int
    float float
    boolean bool
    table dict
    offset date-time datetime.datetime (.tzinfo是datetime.timezone是实例)
    local date-time datetime.datetime(.tzinfo是None)
    local date datetime.date
    local time datetime.time
    array list

    load()loads() 函数有一个参数 parse_float ,指定应如何分析浮点数。

    默认实现满足使用 64 位浮点数的要求,通常精确到大约 16 位有效数字。大部分情况下这个精度足够了,但是如果您需要更高的精度,您可以使用 Decimal 类型。

    >>> import tomli
    >>> from decimal import Decimal
    >>> ts = tomli.loads(
    ...     "ts = 2_459_772.084027777777778",
    ...     parse_float=Decimal,
    ... )["ts"]
    >>> ts
    Decimal('2459772.084027777777778')
    
    >>> seconds = (ts - int(ts)) * 86_400
    >>> seconds
    Decimal('7260.000000000019200')
    
    >>> seconds - 7260
    Decimal('1.9200E-11')
    

    在项目中使用配置文件

    我们希望配置文件只读取一次,然后各个地方的代码都可以使用这个配置。

    实际上,Python的import机制可以帮助我们实现这一点。import模块时,会缓存模块的内容,所以只有第一次import时会读取配置文件。

    下面看一个具体的例子。
    我们的配置文件为tic_tac_toe.toml

    # tic_tac_toe.toml
    
    [user]
    player_x.color = "blue"
    player_o.color = "green"
    
    [constant]
    board_size = 3
    
    [server]
    url = "https://tictactoe.example.com"
    

    创建config/目录:

    config/
    ├── __init__.py
    └── tic_tac_toe.toml
    

    名为 __init__.py 的文件在 Python 中扮演着特殊的角色。它们将包含目录标记为包。此外, __init__.py 定义的名称通过包公开。

    下面我们在__init__.py读取配置文件:

    # __init__.py
    
    import pathlib
    import tomli
    
    path = pathlib.Path(__file__).parent / "tic_tac_toe.toml"
    with path.open(mode="rb") as fp:
        tic_tac_toe = tomli.load(fp)
    

    在config目录下启动解释器:

    >>> import config
    >>> config.path
    PosixPath('/home/realpython/config/tic_tac_toe.toml')
    
    >>> config.tic_tac_toe
    {'user': {'player_x': {'color': 'blue'}, 'player_o': {'color': 'green'}},
     'constant': {'board_size': 3},
     'server': {'url': 'https://tictactoe.example.com'}}
    

    现在,您可以通过将 config/ 目录复制到项目中并将井字游戏配置替换为您自己的设置,将配置集成到现有项目中。
    在代码文件中,您可能希望为配置导入添加别名,以便更方便地访问您的设置:

    >>> from config import tic_tac_toe as CFG
    
    >>> CFG["user"]["player_x"]["color"]
    'blue'
    

    将Python对象转换为TOML

    TOML 文档通常是手写的,因为它们主要用作配置。不过,有时您可能需要将嵌套字典转换为 TOML 文档。

    将字典转换为 TOML

    {
        "user": {
            "player_x": {"symbol": "X", "color": "blue", "ai": True},
            "player_o": {"symbol": "O", "color": "green", "ai": False},
            "ai_skill": 0.85,
        },
        "board_size": 3,
        "server": {"url": "https://tictactoe.example.com"},
    }
    

    在本小节中,您将编写一个简化的 TOML 编写器,该编写器能够将此字典编写为 TOML 文档。

    创建一个新文件to_toml.py
    首先,编写一个名为_dumps_value() 的辅助函数。此函数将获取某个值,并根据值类型返回其 TOML 表示形式。

    # to_toml.py
    
    def _dumps_value(value):
        if isinstance(value, bool):
            return "true" if value else "false"
        elif isinstance(value, (int, float)):
            return str(value)
        elif isinstance(value, str):
            return f'"{value}"'
        elif isinstance(value, list):
            return f"[{', '.join(_dumps_value(v) for v in value)}]"
        else:
            raise TypeError(f"{type(value).__name__} {value!r} is not supported")
    

    接下来,您将添加处理表的代码。main 函数遍历字典,并将每个项目转换为键值对。如果该值恰好是字典,则您将添加一个表头并递归填写该表:

    # to_toml.py
    
    # ...
    
    def dumps(toml_dict, table=""):
        toml = []
        for key, value in toml_dict.items():
            if isinstance(value, dict):
                table_key = f"{table}.{key}" if table else key
                toml.append(f"\n[{table_key}]\n{dumps(value, table_key)}")
            else:
                toml.append(f"{key} = {_dumps_value(value)}")
        return "\n".join(toml)
    

    这在处理嵌套时会出现顺序问题,我们进行排序来修复这个问题。

    # to_toml.py
    
    # ...
    
    def dumps(toml_dict, table=""):
        def tables_at_end(item):
            _, value = item
            return isinstance(value, dict)
    
        toml = []
        for key, value in sorted(toml_dict.items(), key=tables_at_end):
            if isinstance(value, dict):
                table_key = f"{table}.{key}" if table else key
                toml.append(f"\n[{table_key}]\n{dumps(value, table_key)}")
            else:
                toml.append(f"{key} = {_dumps_value(value)}")
        return "\n".join(toml)
    

    通过tomli_w 写TOML文档

    tomli_w有两个函数: dump()dumps()

    类似于tomliload()loads()dump()写入文件,dumps()写入字符串。

    Python 3.11 中的新 tomllib 库不包括 dump() 和 dumps()

    需要安装tomli_w

    python -m pip install tomli_w
    

    现在我们可以容易地将字典导出为TOML文档。

    >>> import tomli_w
    >>> config = {
    ...     "user": {
    ...         "player_x": {"symbol": "X", "color": "blue", "ai": True},
    ...         "player_o": {"symbol": "O", "color": "green", "ai": False},
    ...         "ai_skill": 0.85,
    ...     },
    ...     "board_size": 3,
    ...     "server": {"url": "https://tictactoe.example.com"},
    ... }
    
    >>> print(tomli_w.dumps(config))
    board_size = 3
    
    [user]
    ai_skill = 0.85
    
    [user.player_x]
    symbol = "X"
    color = "blue"
    ai = true
    
    [user.player_o]
    symbol = "O"
    color = "green"
    ai = false
    
    [server]
    url = "https://tictactoe.example.com"
    

    可以使用 dump() 将配置写入文件:

    >>> with open("tic-tac-toe-config.toml", mode="wb") as fp:
    ...     tomli_w.dump(config, fp)
    ...
    

    创建新的TOML文件

    在本节中,您将首先探索如何设置 TOML 文档的格式,以使其更易于用户使用。然后,您将尝试另一个名为 tomlkit 的库,您可以使用它来完全控制 TOML 文档。

    格式和样式

    通常,TOML 文件中会忽略空格。您可以利用这一点使配置文件井井有条、可读且直观。
    此外, # 符号将该行的其余部分标记为注释。

    从某种意义上说,TOML 文档没有样式指南,因为 PEP 8 是 Python 代码的样式指南。但是,该规范确实包含一些建议,同时保留了一些样式方面供您选择。

    为了保持一致性,您可以在项目中使用像 Taplo 这样的格式化程序,并将其配置文件包含在版本控制中。您也可以将其集成到编辑器中。

    tomlkit 从头开始创建 TOML

    TOML Kit 最初是为 Poetry 项目构建的。作为依赖项管理的一部分,Poetry 会操作 pyproject.toml 该文件。但是,由于此文件用于多种用途,因此 Poetry 必须保留文件中的样式和注释。

    首先通过pip安装tomlkit:
    python -m pip install tomlkit

    然后,我们确认tomlkit可以保留样式和注释:

    >>> import tomlkit
    >>> toml_data = """
    ... [nested]  # Not necessary
    ...
    ...     [nested.table]
    ...     string       = "Hello, TOML!"
    ...     weird_string = '''Literal
    ...         Multiline'''
    ... """
    >>> print(tomlkit.dumps(tomlkit.loads(toml_data)))
    
    [nested]  # Not necessary
    
        [nested.table]
        string       = "Hello, TOML!"
        weird_string = '''Literal
            Multiline'''
    
    >>> tomlkit.dumps(tomlkit.loads(toml_data)) == toml_data
    True
    

    下面我们使用tomlkit创建一个新的TOML文件:

    >>> from tomlkit import comment, document, nl, table
    
    >>> toml = document()
    >>> toml.add(comment("Written by TOML Kit"))
    >>> toml.add(nl())
    >>> toml.add("board_size", 3)
    

    调用 document()创建一个 TOML 文档实例。然后,您可以使用 .add() 向此文档添加不同的对象,例如注释、换行符、键值对和表。

    您可以使用 dump()dumps() 如上所述 toml 转换为实际的 TOML 文档,也可以使用.as_string() 方法:

    >>> print(toml.as_string())
    # Written by TOML Kit
    
    board_size = 3
    

    通过添加几个表来继续您的示例:

    >>> player_x = table()
    >>> player_x.add("symbol", "X")
    >>> player_x.add("color", "blue")
    >>> player_x.comment("Start player")
    >>> toml.add("player_x", player_x)
    
    >>> player_o = table()
    >>> player_o.update({"symbol": "O", "color": "green"})
    >>> toml["player_o"] = player_o
    

    您可以通过调用 table()创建表并向其添加内容。
    可以通过.add()添加键和值,也可以使用 .update() 直接从字典添加键和值。

    更新现有的TOML文件

    将 TOML 表示为 tomlkit 对象

    下面仔细研究一下如何用ttomlki表示 TOML 文档。

    首先,创建tic-tac-toe-config.toml :

    # tic-tac-toe-config.toml
    
    board_size = 3
    
    [user]
    ai_skill = 0.85  # A number between 0 (random) and 1 (expert)
    
        [user.player_x]
        symbol = "X"
        color = "blue"
        ai = true
    
        [user.player_o]
        symbol = "O"
        color = "green"
        ai = false
    
    # Settings used when deploying the application
    [server]
    url = "https://tictactoe.example.com"
    

    打开 REPL 会话并使用以下命令 tomlkit 加载此文档:

    >>> import tomlkit
    >>> with open("tic-tac-toe-config.toml", mode="rt", encoding="utf-8") as fp:
    ...     config = tomlkit.load(fp)
    ...
    >>> config
    {'board_size': 3, 'user': {'ai_skill': 0.85, 'player_x': { ... }}}
    
    >>> type(config)
    <class 'tomlkit.toml_document.TOMLDocument'>
    

    load() 用于从文件加载 TOML 文档。 config 看起来像一个字典。然而,深入挖掘,你会发现它是TOMLDocument 类型。

    我们可以用[]访问:

    >>> config["user"]["player_o"]["color"]
    'green'
    
    >>> type(config["user"]["player_o"]["color"])
    <class 'tomlkit.items.String'>
    
    >>> config["user"]["player_o"]["color"].upper()
    'GREEN'
    

    发现其中的值也是特殊的tomlkit数据类型。不过我们可以像普通的Python对象一样使用它们。例如,可以使用 .upper() 字符串方法。

    特殊数据类型的一个优点是,它们允许您访问有关文档的元信息,包括注释和缩进:

    >>> config["user"]["ai_skill"]
    0.85
    
    >>> config["user"]["ai_skill"].trivia.comment
    '# A number between 0 (random) and 1 (expert)'
    
    >>> config["user"]["player_x"].trivia.indent
    '    '
    

    如上所述,您通常可以将这些特殊对象视为原生 Python 对象。事实上,它们继承自相应的Python对象。但是,如果你真的需要,你可以用 .unwrap() 它们转换为普通的 Python:

    >>> config["board_size"] ** 2
    9
    
    >>> isinstance(config["board_size"], int)
    True
    
    >>> config["board_size"].unwrap()
    3
    
    >>> type(config["board_size"].unwrap())
    <class 'int'>
    

    无损读取和写入 TOML

    在本小节中,您将加载现有的 TOML 文件,并在将其写回磁盘之前对其进行一些更改。

    首先加载您在上一小节中使用的相同 TOML 文件:

    >>> import tomlkit
    >>> with open("tic-tac-toe-config.toml", mode="rt", encoding="utf-8") as fp:
    ...     config = tomlkit.load(fp)
    ...
    

    现在,你可以用.add()添加新元素,但无法用.add()更新现有key的值。

    >>> config.add("app_name", "Tic-Tac-Toe")
    {'board_size': 3, 'app_name': 'Tic-Tac-Toe', 'user': { ... }}
    
    >>> config["user"].add("ai_skill", 0.6)
    Traceback (most recent call last):
      ...
    KeyAlreadyPresent: Key "ai_skill" already exists.
    

    不过,我们可以像字典一样:

    >>> config["user"]["ai_skill"] = 0.6
    >>> print(config["user"].as_string())
    ai_skill = 0.6  # A number between 0 (random) and 1 (expert)
    
        [user.player_x]
        symbol = "X"
        color = "blue"
        ai = true
    
        [user.player_o]
        symbol = "O"
        color = "green"
        ai = false
    

    当您更新这样的值时, tomlkit 仍然会保留样式和注释。如您所见,关于 ai_skill 的注释保持不变。

    部分 tomlkit 支持所谓的 Fluent 接口(链式调用)。在实践中,这意味着返回更新的对象

    >>> from tomlkit import aot, comment, inline_table, nl, table
    >>> player_data = [
    ...     {"user": "gah", "first_name": "Geir Arne", "last_name": "Hjelle"},
    ...     {"user": "tompw", "first_name": "Tom", "last_name": "Preston-Werner"},
    ... ]
    
    >>> players = aot()
    >>> for player in player_data:
    ...     players.append(
    ...         table()
    ...         .add("username", player["user"])
    ...         .add("name",
    ...             inline_table()
    ...             .add("first", player["first_name"])
    ...             .add("last", player["last_name"])
    ...         )
    ...     )
    ...
    >>> config.add(nl()).add(comment("Players")).add("players", players)
    

    完成对配置的更新后,现在可以将其写回同一文件:

    >>> with open("tic-tac-toe-config.toml", mode="wt", encoding="utf-8") as fp:
    ...     tomlkit.dump(config, fp)
    

    打开 tic-tac-toe-config.toml 并注意您的更新已包含在内。同时,保留了原有的风格。

    总结

    现在你已经了解TOML语法以及在Python中使用它的方式。当你需要一个配置文件时,推荐考虑使用TOML

    作者:一只大鸽子

    物联沃分享整理
    物联沃-IOTWORD物联网 » [译]Python与TOML:新朋友的最佳实践 (2) Python操作TOML指南

    发表回复