首頁 > 軟體

Python實現自定義Jupyter魔法命令

2022-08-19 14:02:30

相信大家都用過 jupyter,也用過裡面的魔法命令,這些魔法命令都以 % 或者 %% 開頭,我們舉個例子。

用法還是比較簡單的,但是我們能不能自定義魔法命令呢?毫無疑問是可以的,因為上面的 %%cython 就是 Cython 模組自定義的。

所以命令可以是 jupyter 內建的,比如 %time,直接拿來就能用;還可以是第三方模組裡面的,在 jupyter 通過 %load_ext 載入之後,再嵌入進來。下面就來看看如何自定義魔法命令。

from IPython.core.magic import (
    magics_class,
    Magics,
    line_magic,
    cell_magic
)


@magics_class
class MagicOrder(Magics):
    """
    自定義一個類,類名叫什麼無所謂
    但要繼承 Magics,並且要被 magics_class 裝飾
    """

    @line_magic
    def hello(self, line):
        """
        在 jupyter 中就可以使用如下命令,比如:
        %hello <Your Code>,然後就會呼叫這個 hello 方法
        引數 line 就是 %hello 後面的程式碼
        """
        print(f"line: {line}")

    @cell_magic
    def world(self, line, cell):
        """
        在 jupyter 中就可以使用如下命令,比如:
        %%world
        <Your Code>
        <Your Code>
        ...

        然後就會呼叫這個 world 方法
        引數 cell 就是 %%world 下面整個單元格的程式碼

        然後還有一個引數 line,它表示 %%world 所在行後面的程式碼
        但對於 %% 開頭的命令來說,我們一般都會新起一行,然後寫程式碼
        所以 line 這個引數暫時用不到
        """
        print(f"line: n{line}")
        print("-----------------")
        print(f"cell: n{cell}")


# 必須定義 load_ipython_extension 函數
# %load_ext 本質上也是載入一個模組
# 但它會自動呼叫該函數
def load_ipython_extension(ip):
    # 在函數內部,我們將類 MagicOrder 註冊進去
    # 然後就可以使用它內部的魔法命令了
    ip.register_magics(MagicOrder)

# 如果不定義此函數,那麼使用 %load_ext 載入時會報錯
# The xxx module is not an IPython extension.

當前模組叫 main.py,我們來測試一下:

結果沒有問題,但說實話對於 %% 開頭的命令來說,我們很少會在它後面寫程式碼,基本都是新起一行,就像下面這個樣子。

自定義命令我們已經實現了,並且也知道怎麼獲取輸入的程式碼了,下面要做的就是執行它。而將字串當成程式碼執行,我們可以使用內建函數 exec。

@magics_class
class MagicOrder(Magics):

    @line_magic
    def hello(self, line):
        exec(line)

    @cell_magic
    def world(self, line, cell):
        exec(cell)

程式碼的其它部分不變,然後你覺得接下來呼叫魔法命令會執行成功嗎?我們測試一下:

神奇的地方出現了,雖然命令執行成功了,但執行完之後,告訴我們變數未定義。其實原因很好想,我們呼叫 exec 的時候沒有指定名稱空間,那麼預設會影響 exec 函數所在的名稱空間,即 hello 和 world 函數的名稱空間。

當開啟一個 jupyter 的時候,內部相當於啟動了一個 shell,所以在呼叫 exec 的時候,應該將整個 shell 的名稱空間傳進去。

from IPython.core.magic import (
    magics_class,
    Magics,
    line_magic,
    cell_magic,
    needs_local_scope
)


@magics_class
class MagicOrder(Magics):

    @line_magic
    def hello(self, line):
        # 通過 self.shell.user_ns
        # 可以拿到當前 shell 的名稱空間
        # 注意:包含所有的單元格
        local_ns = self.shell.user_ns
        # 在 local_ns 當中執行程式碼
        exec(line, local_ns, local_ns)

    @needs_local_scope
    @cell_magic
    def world(self, line, cell, local_ns):
        # 或者通過 needs_local_scope 裝飾器
        # 這樣在呼叫函數的時候,會額外傳遞一個 local_ns 引數
        # 該引數和 self.shell.user_ns 等價
        exec(cell, local_ns, local_ns)

def load_ipython_extension(ip):
    ip.register_magics(MagicOrder)

然後再來測試一下:

此時就沒有任何問題了。

下面我們模仿 jupyter 的 %time 命令,實現一個 %my_time,來加深一遍印象。

@magics_class
class MagicOrder(Magics):

    @needs_local_scope
    @line_magic
    def my_time(self, line, local_ns):
        start = time.perf_counter()
        exec(line, local_ns, local_ns)
        end = time.perf_counter()
        print(f"總耗時: {round(end - start, 3)}")

測試一下:

結果沒有問題,是我們想要的結果。

最後再來看看如何設定可選引數,舉一個 Cython 的例子:

我們說對於以 %% 開頭的命令,應該新起一行,在它的下面寫程式碼。而之所以新起一行,是因為命令所在的行,要用於設定可選引數。那麼問題來了,如何設定指定的可選引數呢?

from IPython.core.magic import (
    magics_class,
    Magics,
    cell_magic,
    needs_local_scope
)
from IPython.core import magic_arguments


@magics_class
class MagicOrder(Magics):
    @magic_arguments.magic_arguments()
    # 在 jupyter 中可以通過 -n=xxx 或者 --name=xxx
    # 然後是 dest="name",用於指定引數的名字
    # 後續便可以通過 name 欄位來獲取該引數的值
    @magic_arguments.argument(
        "-n", "--name", dest="name", default="satori"
    )
    # "-" 和 "--" 可以只出現一個,並且預設解析得到的是字串
    # 而 age 我們希望是整數,所以指定 type 為 int
    # 解析完引數之後,會自動呼叫 int 進行轉化
    # 如果不指定該引數,則使用 default
    # 而這裡沒有 default,那麼結果就是 None
    @magic_arguments.argument(
        "--age", dest="age", type=int
    )
    @magic_arguments.argument(
        "-h", "--hobby", dest="hobby", default=[],
        action="append"
    )
    @needs_local_scope
    @cell_magic
    def order(self, line, cell, local_ns):
        # 顯然 line 就是可選引數,cell 就是程式碼塊
        exec(cell, local_ns, local_ns)
        # 解析引數
        args = magic_arguments.parse_argstring(
            self.order, line)
        # 列印
        print(args)


def load_ipython_extension(ip):
    ip.register_magics(MagicOrder)

我們測試一下:

還是很簡單的,而且這裡的引數解析和 argparse 模組非常類似,可以自己看一下。

到此這篇關於Python實現自定義Jupyter魔法命令的文章就介紹到這了,更多相關Python Jupyter魔法命令內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com