:point_up_2:  Python貓 ” ,一個值得加星標的 公衆號

劇照|《三國機密之潛龍在淵》

來源:渡碼@渡碼公衆號

這篇文章對優秀的開源項目 Rich 的源碼進行解析, OMG,盤他 。爲什麼建議閱讀源碼,有兩個原因,第一,單純學語言很難在實踐中靈活應用,通過閱讀源碼可以看到每個知識點的運用場景,印象會更深,以後寫代碼的時候就能應用起來;第二,通過閱讀優秀的開源代碼,可以學習比人的代碼規範、設計思路;第三,參與到開源社區,獲得更廣闊的的發展前景;第四,面試加分項。所以,有時間的話還是建議大家多讀讀優秀開源項目的源碼。

下面進入今天的主題,這個開源項目的名字叫 Rich ,將近8k star,地址: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
    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 函數就不展開了, 其實就是利用上面提到的 _attributesnumber 屬性生成標準輸出的能夠識別的格式,返回值 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 裏用到的代碼確實挺新的,能學到很多東西,比直接看書來的快,有興趣的朋友可以自行閱讀。經常讀我文章的朋友知道,我一直在尋找新的內容、新方向,這次源碼解析也是一次新的嘗試,不知道是不是一件有價值的事情,先持續更新幾篇看看。如果你覺得有用也想看更多的源碼解析的文章,希望點個贊或者在看鼓勵一下,不勝感激。

相關文章