这篇文章对优秀的开源项目 Rich 的源码进行解析, OMG, 盘他. 为什么建议阅读源码, 有两个原因, 第一, 单纯学语言很难在实践中灵活应用, 通过阅读源码可以看到每个知识点的运用场景, 印象会更深, 以后写代码的时候就能应用起来; 第二, 通过阅读优秀的开源代码, 可以学习比人的代码规范, 设计思路; 第三, 参与到开源社区, 获得更广阔的的发展前景; 第四, 面试加分项. 所以, 有时间的话还是建议大家多读读优秀开源项目的源码.
下面进入今天的主题, 这个开源项目的名字叫 Rich, 地址: https://github.com/willmcgugan/rich (可以点击文末阅读原文查看). 这个项目是个英国老铁开发的, 比较友好的是有中文文档. 它的作用是可以在控制台输出富文本和精美的可视化格式(如: 表格, 进度条和 Markdown). 截图感受一下
各种格式
进度条
效果看起来很酷炫, 我忍不住看了一些代码, 发现作者用的是 Python 3.8 版本实现的, 好多新特性我也不了解, 所以在看源码过程中还补了一下语法基础. 下面以一个例子来简单看看 Rich 的源码, 源码的讲解我尽量言简意赅, 重点讲解源码中涉及的一些关键的知识点.
先捡个软柿子捏, 如下:
- from rich import print
- print('Hello, [bold yellow]World[/bold yellow]!')
输出效果:
可以看到对单词 World 显示为粗体, 红颜色.
先通过一张图来看看大致流程
简单来说就是将文本的格式转化成标准输出能够识别的格式, 然后输出即可. 下面来讲解源码, 当我们调用 print 函数时, 最终程序会跳转到 console.py 文件的 print 函数中, 执行以下代码
调用 self._collect_renderables 函数处理输入的字符串, 将需要格式化的部分标出来, 返回的 renderables 变量是一个 Text 列表, 因为输入只有 1 个字符串, 所以列表的大小为 1, 变量结果如下
Span(7, 12, 'bold red')便是框出来需要格式化的内容.
上述代码还有一个 with self, 它的作用我们一会儿再说. 接着 print 函数往下看
这里会遍历刚刚提到的 renderables 变量, 先调用 render 函数渲染输入的文本, 然后调用 extend 函数将 render 返回的结果添加到 self._buffer 列表里. 这里有几个知识点简单说一下
self._buffer 是函数调用, 由于它加了 @property 注解, 所以调用是可以不用加小括号, 它返回的是
self._thread_locals.buffer
变量, 该变量是 List[Segment]类型的
self._thread_locals.buffer
变量用到 dataclasses 模块的 field 函数初始化, 初始化代码为
buffer: List[Segment] = field(default_factory=list)
,dataclasses 是 Python 3.7 版本的新引入的模块, field 函数可提供更加灵活的初始化方式, 并且该模块中的 @dataclass 注解可以为类自动添加__init__等方法, 比较方便
extend = self._buffer.extend
这种写法将 list 的 extent 函数存到了临时变量里, 后续直接通过 extend 调用该函数, 比对象名. extend 的方式更简洁.
下面我们来看 render(renderable, render_options)函数的渲染逻辑, 该函数里会调用下面的代码
render_iterable = renderable.__rich_console__(self, options)
在函数声明里 renderable 对象是 RenderableType 类型的, 但实际上 Text 类型的, 并且这两种类型没有继承关系, 这里没太想明白作者为什么这样搞. 所以, 这里的__rich_console__函数我们要到 text.py 文件中去找.__rich_console__函数最终会调用 Text 对象的 render 函数, 核心代码如下:
- def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
- style_map = {index: get_style(span.style) for index, span in enumerated_spans}
- _Segment = Segment
- for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
- yield _Segment(text[offset:next_offset], get_current_style())
调用 get_style 函数, 将格式转为 Style 对象, 如:'bold red'转成 Style 对象, 然后按照不同的显示格式进行'分片', 每个'片段'构造一个 Segment 对象存储文本及其对应的格式.
get_style 函数会调用 Style.parse(name)生成 Style 对象, 核心代码如下
- @lru_cache(maxsize=1024)
- def parse(cls, style_definition: str) -> "Style":
- words = iter(style_definition.split())
- for original_word in words:
- Word = original_word.lower()
- if Word == "on":
- # ... 省略
- elif Word in style_attributes:
- attributes[style_attributes[Word]] = True
- else:
- color = Word
- style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
- return style
参数 style_definition 取值为 bold red, 分割后生成 ['bold', 'red'] 列表, 当 Word 变量等于'bold'时, 会执行 attributes[style_attributes[Word]] = True 语句, 执行后 attributes 等于{'bold': true}, 它是一个字典. 当 Word 变量等于 red 时, 执行 color=Word 语句. 最终调用导数第二行构造 Style 对象, Style 对象最核心的两个数据形式_attributes 和_color, 前者是 int 类型, 在我们例子中取值是 1, 代表'bold', 即: 粗体. 后者代表颜色, 即:'red', 它是 Color 类型的, 该类中有个属性 number 也是我们后续要用到的.
下面来看下__rich_console__函数返回了哪些 Segment 对象
可以看到有 4 个, 每一个都有文本及其 Style 对象.
回到 render(renderable, render_options)函数, 刚刚介绍了__rich_console__部分, 下面还有返回的代码, 一起来看看
- iter_render = iter(render_iterable)
- for render_output in iter_render:
- if isinstance(render_output, Segment):
- yield render_output
render_iterable 变量是__rich_console__的返回值, 即: 4 个 Segment 对象. 遍历后通过 yield 方式返回. 该关键字用来返回一个迭代器, 也可以理解为一个列表. 并且 yield 返回有个特点, 函数返回值只有真正被使用的时候才会执行调用函数.
这样, render(renderable, render_options)函数就讲解完了, 返回上一层 extend(render(renderable, render_options)), 通过 extend 函数将 4 个 Segment 对象保存到 buffer 中, 结果如下
然后 print 方法就执行完了. 看起来已经结束了, 然而控制台打印的代码貌似没有看到. 答案就在刚刚的 with self 中, with 关键字使得执行完代码体后, 会自动调用 self 的__exit__函数.__exit__函数中调用_render_buffer 函数进行最终的输出, 核心代码如下
- output: List[str] = []
- append = output.append
- for line in Segment.split_and_crop_lines(buffer, self.width, pad=False):
- for text, style, is_control in line:
- if style and not is_control:
- append(
- style.render(
- text,
- color_system=color_system,
- legacy_windows=legacy_windows,
- )
- )
- rendered = "".join(output)
- return rendered
split_and_crop_lines 函数是为了适应控制台的宽度, 暂时忽略它. line 变量仍然是刚刚提到的 4 个 Segment 对象, 通过 for text, style, is_control in line 直接将每个 Segment 对象的属性解出来并赋给 text, style, is_control 变量, 最终每个 style 对象都会调用 render 方法完成最后的渲染.
render 方法核心代码如下
- attrs = self._make_ansi_codes(color_system)
- rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
_make_ansi_codes 函数就不展开了, 其实就是利用上面提到的_attributes 和 number 属性生成标准输出的能够识别的格式, 返回值 attrs 的结果为 1;31,1 取自_attributes 代表粗体, 31 中的 1 取自 number 代表颜色, 其他颜色取值是不同的, 比如黄色是 33, 紫色是 35. 最后通过 f-string 格式 (新特性) 生成 rendered 变量, 取值为[1;31mWorld[0m 它就是标准输出流能够识别的格式.
回到_render_buffer 函数中, 调用 rendered = "".join(output)将 4 个渲染后的片段拼在一起, 返回. 返回后执行的代码如下:
- text = self._render_buffer()
- if text:
- self.file.write(text)
self.file 变量的赋值语句为 self.file = file or sys.stdout, 由于我们没有定义 file 变量, 所以 self.file 取值为 sys.stdout. 最终的输出为 sys.stdout.write(text), 至此整个流程就讲解完了. 如果你理解了上述逻辑, 应该可以通过下面代码输出同样的效果
sys.stdout.write('Hello, \033[1;31mWorld\033[0m!')
所以 Rich 做的就是把文字格式准成标准输出流能识别的格式.
Rich 里用到的代码确实挺新的, 能学到很多东西, 比直接看书来的快, 有兴趣的朋友可以自行阅读. 欢迎关注公众号 ** 渡码 ** 不断分享优秀开源项目源码分析
来源: https://www.cnblogs.com/duma/p/13252142.html