【问题标题】:Pretty-print dataclasses prettier漂亮的打印数据类更漂亮
【发布时间】:2025-12-18 16:55:01
【问题描述】:

Python Data Classes 实例还包括一个字符串表示方法,但是当类具有多个字段和/或更长的字段值时,它的结果实际上不足以用于漂亮的打印目的。

基本上,我正在寻找一种方法来自定义默认数据类字符串表示例程,或者寻找一种能够理解数据类并将它们打印得更漂亮的漂亮打印机。

所以,这只是我想到的一个小定制:在每个字段之后添加一个换行符,同时在第一个字段之后缩进。

例如,而不是

x = InventoryItem('foo', 23)
print(x) # =>
InventoryItem(name='foo', unit_price=23, quantity_on_hand=0)

我想得到这样的字符串表示:

x = InventoryItem('foo', 23)
print(x) # =>
InventoryItem(
    name='foo',
    unit_price=23,
    quantity_on_hand=0
)

或类似的。也许漂亮的打印机可以变得更漂亮,例如对齐 = 分配字符或类似的东西。

当然,它也应该以递归方式工作,例如也是数据类的字段应该缩进更多。

【问题讨论】:

标签: python pretty-print python-dataclasses


【解决方案1】:

pprintsupports pretty printing only since version 3.10(注意:Python 3.10 于 2021 年发布)。

例子:

[ins] In [1]: from dataclasses import dataclass
         ...:
         ...: @dataclass
         ...: class Point:
         ...:     x: int
         ...:     y: int
         ...:
         ...: @dataclass
         ...: class Coords:
         ...:     my_points: list
         ...:     my_dict: dict
         ...:
         ...: coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})

[ins] In [15]: pprint.pprint(coords, width=20)                                  
Coords(my_points=[Point(x=1,
                        y=2),
                  Point(x=3,
                        y=4)],
       my_dict={'a': (1,
                      2),
                (1, 2): 'a'})

当使用 Python 3.9 或更早版本时,prettyprinter 包支持数据类并提供一些漂亮的打印功能。

例子:

[ins] In [1]: from dataclasses import dataclass
         ...:
         ...: @dataclass
         ...: class Point:
         ...:     x: int
         ...:     y: int
         ...:
         ...: @dataclass
         ...: class Coords:
         ...:     my_points: list
         ...:     my_dict: dict
         ...:
         ...: coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})

[nav] In [2]: import prettyprinter as pp

[ins] In [3]: pp.pprint(coords)
Coords(my_points=[Point(x=1, y=2), Point(x=3, y=4)], my_dict={'a': (1, 2), (1, 2): 'a'})

默认情况下不启用数据类支持,因此:

[nav] In [4]: pp.install_extras()
[ins] In [5]: pp.pprint(coords)
Coords(
    my_points=[Point(x=1, y=2), Point(x=3, y=4)],
    my_dict={'a': (1, 2), (1, 2): 'a'}
)

或强制所有字段缩进:

[ins] In [6]: pp.pprint(coords, width=1)
Coords(
    my_points=[
        Point(
            x=1,
            y=2
        ),
        Point(
            x=3,
            y=4
        )
    ],
    my_dict={
        'a': (
            1,
            2
        ),
        (
            1,
            2
        ): 'a'
    }
)

Prettyprinter 甚至可以语法高亮! (参见cpprint()


注意事项:

  • prettyprinter 不是 python 标准库的一部分
  • default values aren't printed, at all 到 2021 年,这个问题已经无法解决了
  • prettyprinter 的打印速度非常慢,即比标准的 pprint 慢得多,例如用于检查一个值是否为默认值,将其与默认构造的值进行比较

【讨论】:

    【解决方案2】:

    Python 3.10+ Supports 漂亮的打印数据类:

    Python 3.10.0b2+ (heads/3.10:f807a4fad4, Sep  4 2021, 18:58:04) [GCC 11.1.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> from dataclasses import dataclass
    >>> @dataclass
    ... class Literal:
    ...     value: 'Any'
    ... 
    >>> @dataclass
    ... class Binary:
    ...     left: 'Binary | Literal'
    ...     operator: str
    ...     right: 'Binary | Literal'
    ... 
    >>> from pprint import pprint
    >>> # magic happens here
    >>> pprint(
    ... Binary(Binary(Literal(2), '*', Literal(100)), '+', Literal(50)))
    Binary(left=Binary(left=Literal(value=2),
                       operator='*',
                       right=Literal(value=100)),
           operator='+',
           right=Literal(value=50))
    

    遗憾的是,它并没有被广泛使用(......但是,截至 2021 年)

    【讨论】:

      【解决方案3】:

      我们可以使用dataclasses.fields 递归嵌套数据类并漂亮地打印它们:

      from collections.abc import Mapping, Iterable
      from dataclasses import is_dataclass, fields
      
      def pretty_print(obj, indent=4):
          """
          Pretty prints a (possibly deeply-nested) dataclass.
          Each new block will be indented by `indent` spaces (default is 4).
          """
          print(stringify(obj, indent))
      
      def stringify(obj, indent=4, _indents=0):
          if isinstance(obj, str):
              return f"'{obj}'"
      
          if not is_dataclass(obj) and not isinstance(obj, (Mapping, Iterable)):
              return str(obj)
      
          this_indent = indent * _indents * ' '
          next_indent = indent * (_indents + 1) * ' '
          start, end = f'{type(obj).__name__}(', ')'  # dicts, lists, and tuples will re-assign this
      
          if is_dataclass(obj):
              body = '\n'.join(
                  f'{next_indent}{field.name}='
                  f'{stringify(getattr(obj, field.name), indent, _indents + 1)},' for field in fields(obj)
              )
      
          elif isinstance(obj, Mapping):
              if isinstance(obj, dict):
                  start, end = '{}'
      
              body = '\n'.join(
                  f'{next_indent}{stringify(key, indent, _indents + 1)}: '
                  f'{stringify(value, indent, _indents + 1)},' for key, value in obj.items()
              )
      
          else:  # is Iterable
              if isinstance(obj, list):
                  start, end = '[]'
              elif isinstance(obj, tuple):
                  start = '('
      
              body = '\n'.join(
                  f'{next_indent}{stringify(item, indent, _indents + 1)},' for item in obj
              )
      
          return f'{start}\n{body}\n{this_indent}{end}'
      
      

      我们可以使用嵌套数据类对其进行测试:

      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: int
          y: int
      
      @dataclass
      class Coords:
          my_points: list
          my_dict: dict
      
      coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})
      
      pretty_print(coords)
      
      # Coords(
      #     my_points=[
      #         Point(
      #             x=1,
      #             y=2,
      #         ),
      #         Point(
      #             x=3,
      #             y=4,
      #         ),
      #     ],
      #     my_dict={
      #         'a': (
      #             1,
      #             2,
      #         ),
      #         (
      #             1,
      #             2,
      #         ): 'a',
      #     },
      # )
      

      这应该足以涵盖大多数情况。希望这会有所帮助!

      【讨论】:

        【解决方案4】:

        您可能应该使用prettyprinter,但如果由于某种原因无法添加依赖项,那么您可以使用它,它比 salt-die 的示例略短(因为它使用 pprint

        import dataclasses
        import pprint
        
        
        def dcppformat(x, chars=0):
            def parts():
                if dataclasses.is_dataclass(x):
                    yield type(x).__name__ + "("
        
                    def fields():
                        for field in dataclasses.fields(x):
                            nindent = chars + len(field.name) + 4
                            value = getattr(x, field.name)
                            rep_value = dcppformat(value)
                            yield " " * (chars + 3) + indent_body_chars(
                                "{}={}".format(field.name, rep_value), chars=nindent
                            )
        
                    yield ",\n".join(fields())
                    yield " " * chars + ")"
                else:
                    yield pprint.pformat(x)
        
            return "\n".join(parts())
        
        
        def indent(x, level=1):
            indent_chars(x, level * 4)
        
        
        def indent_chars(x, chars=1):
            return "\n".join(" " * chars + p for p in x.split("\n"))
        
        
        def indent_body_chars(x, chars=4):
            a, *b = x.split("\n")
            if b:
                return a + "\n" + indent_chars("\n".join(b), chars=chars,)
            else:
                return a
        
        
        def dcpprint(x):
            print(dcppformat(x))
        
        
        def test():
            @dataclasses.dataclass
            class A:
                a: object
                b: str
        
            dcpprint(A(a=A(a=None, b={"a": 1, "c": 1, "long": "a" * 100}), b=2))
        
        
        if __name__ == "__main__":
            test()
        
        

        【讨论】: