Python優秀開源項目Rich源碼解析


這篇文章對優秀的開源項目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)dataclassesPython 3.7 版本的新引入的模塊, field函數可提供更加靈活的初始化方式,並且該模塊中的 @dataclass注解可以為類自動添加 __init__等方法,比較方便
  • extend = self._buffer.extend這種寫法將 listextent函數存到了臨時變量里,后續直接通過 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變量,取值為World 它就是標准輸出流能夠識別的格式。

回到_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里用到的代碼確實挺新的,能學到很多東西,比直接看書來的快,有興趣的朋友可以自行閱讀。歡迎關注公眾號**渡碼**不斷分享優秀開源項目源碼分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM