IPython 的奇特 feature

下午在 Colab 被一個奇妙的問題卡了好久,我把問題簡化成底下這個儲存格: from IPython.display import Markdown m = Markdown('sample_data') 只要一執行,就會看到錯誤訊息: --------------------------------------------------------------------------- IsADirectoryError Traceback (most recent call last) in () 1 from IPython.display import Markdown ----> 2 m = Markdown('sample_data') 1 frames /usr/local/lib/python3.11/dist-packages/IPython/core/display.py in reload(self) 660 """Reload the raw data from file or URL.""" 661 if self.filename is not None: --> 662 with open(self.filename, self._read_flags) as f: 663 self.data = f.read() 664 elif self.url is not None: IsADirectoryError: [Errno 21] Is a directory: 'sample_data' 從錯誤訊息就可以猜出來,它把我要以 Markdown 格式解譯的 "sample_data" 字串內容當成路徑名稱解譯,好死不死,Colab 預設就有一個名稱為 "sample_data" 的資料夾,正因為它是資料夾,所以當 IPython 底層的程式碼想要把它當成檔案讀取時,就會發生你看到的錯誤。 但為什麼為這樣? DisplayObject 預設會從檔案更新內容 之所以會發生剛剛看到的問題,就是因為 IPython.display 模組中的 Markdown 等格式化顯示物件都是 DisplayObject 類別的子類別,這個類別的 __init__ 是這樣的(已刪除註解): def __init__(self, data=None, url=None, filename=None, metadata=None): if isinstance(data, (Path, PurePath)): data = str(data) if data is not None and isinstance(data, str): if data.startswith('http') and url is None: url = data filename = None data = None elif _safe_exists(data) and filename is None: url = None filename = data data = None self.url = url self.filename = filename self.data = data if metadata is not None: self.metadata = metadata elif self.metadata is None: self.metadata = {} self.reload() self._check_data() 你會看到它做幾件事: 如果 data 參數是路徑類的物件,就先轉成字串。 如果 data 是字串,就會先判斷是不是網址,如果是網址且沒有傳入網址給 url(預設就為 None),就設定網址給 url;若不是網址,而且沒有傳入檔案路徑給 filename(預設就是 None),會依照底下的 safe_exists 函式結果設定給 filename: def _safe_exists(path): """Check path, but don't let exceptions raise""" try: return os.path.exists(path) except Exception: return False 這個函式很簡單,就是檢查輸入的內容是不是一個合法的路徑,不過請注意,它只檢查路徑是否存在,但並不會管這個路徑是檔案還是資料夾。 最後會叫用 self.reload 嘗試從網址或是檔案讀取更新內容: def reload(self): """Reload the raw data from file or URL.""" if self.filename is not None: encoding = None if "b" in self._read_flags else "utf-8" with open(self.filename, self._read_flags, encoding=encoding) as f: self.data = f.read() elif self.url is not None: # Deferred import from urllib.request import urlopen response = urlopen(self.url) data = response.read() ...(略) 就是這裡導致我遇到的問題,由於它會在 data 不是網址而且沒有傳入檔名給 filename 參數時把 data 指派給 filename,所以在 reload 方法中就會嘗試去讀取檔案內容,並因為 "sample_data" 是資料夾而發生錯誤。 你的 feature 是我的 bug 我推想 IPython 這樣的設計,應該是想增加彈性,只要透過 data 參數,就可以依據需要傳單純的內容,或者是傳入可以載入內容的網址或檔案路徑,因為在 IPython 的文件上,data 參數就是多用途的: data (unicode, str or bytes) – The raw data or a URL or file to load the data from 所以這應該是個 feature,可是我認為你都另外獨立有 url、filename 參數了,這個彈性只是會增加意外的驚嚇! 目前唯一的解法,就是在建立這些 DisplayObject 家族的物件時,必須自己檢查傳入的字串會不會剛好是某個檔案或是資料夾的路徑,如果是,就要自己用其他方式避開,例如變成 inline code: m = Markdown('`sample_data`') 就不會有事,或者是在字串開頭隨意加個空白字元之類的,總之,這錯誤就是要在剛好的狀況下才會發生,但突然遇到就會覺得真是怪,好特別的 feature。 對了,由於這是 IPython 的問題,所以使用 Jupyter 也會遇到類似的狀況。

Apr 24, 2025 - 14:34
 0
IPython 的奇特 feature

下午在 Colab 被一個奇妙的問題卡了好久,我把問題簡化成底下這個儲存格:

from IPython.display import Markdown
m = Markdown('sample_data')

只要一執行,就會看到錯誤訊息:

---------------------------------------------------------------------------
IsADirectoryError                         Traceback (most recent call last)
 in ()
      1 from IPython.display import Markdown
----> 2 m = Markdown('sample_data')

1 frames
/usr/local/lib/python3.11/dist-packages/IPython/core/display.py in reload(self)
    660         """Reload the raw data from file or URL."""
    661         if self.filename is not None:
--> 662             with open(self.filename, self._read_flags) as f:
    663                 self.data = f.read()
    664         elif self.url is not None:

IsADirectoryError: [Errno 21] Is a directory: 'sample_data'

從錯誤訊息就可以猜出來,它把我要以 Markdown 格式解譯的 "sample_data" 字串內容當成路徑名稱解譯,好死不死,Colab 預設就有一個名稱為 "sample_data" 的資料夾,正因為它是資料夾,所以當 IPython 底層的程式碼想要把它當成檔案讀取時,就會發生你看到的錯誤。

但為什麼為這樣?

DisplayObject 預設會從檔案更新內容

之所以會發生剛剛看到的問題,就是因為 IPython.display 模組中的 Markdown 等格式化顯示物件都是 DisplayObject 類別的子類別,這個類別的 __init__ 是這樣的(已刪除註解):

    def __init__(self, data=None, url=None, filename=None, metadata=None):
        if isinstance(data, (Path, PurePath)):
            data = str(data)

        if data is not None and isinstance(data, str):
            if data.startswith('http') and url is None:
                url = data
                filename = None
                data = None
            elif _safe_exists(data) and filename is None:
                url = None
                filename = data
                data = None

        self.url = url
        self.filename = filename
        self.data = data

        if metadata is not None:
            self.metadata = metadata
        elif self.metadata is None:
            self.metadata = {}

        self.reload()
        self._check_data()

你會看到它做幾件事:

  1. 如果 data 參數是路徑類的物件,就先轉成字串。
  2. 如果 data 是字串,就會先判斷是不是網址,如果是網址且沒有傳入網址給 url(預設就為 None),就設定網址給 url;若不是網址,而且沒有傳入檔案路徑給 filename(預設就是 None),會依照底下的 safe_exists 函式結果設定給 filename

    def _safe_exists(path):
        """Check path, but don't let exceptions raise"""
        try:
            return os.path.exists(path)
        except Exception:
            return False
    

    這個函式很簡單,就是檢查輸入的內容是不是一個合法的路徑,不過請注意,它只檢查路徑是否存在,但並不會管這個路徑是檔案還是資料夾。

  3. 最後會叫用 self.reload 嘗試從網址或是檔案讀取更新內容:

    def reload(self):
        """Reload the raw data from file or URL."""
        if self.filename is not None:
            encoding = None if "b" in self._read_flags else "utf-8"
            with open(self.filename, self._read_flags, encoding=encoding) as f:
                self.data = f.read()
        elif self.url is not None:
            # Deferred import
            from urllib.request import urlopen
            response = urlopen(self.url)
            data = response.read()
    ...
    

    就是這裡導致我遇到的問題,由於它會在 data 不是網址而且沒有傳入檔名給
    filename 參數時把 data 指派給 filename,所以在 reload 方法中就會嘗試去讀取檔案內容,並因為 "sample_data" 是資料夾而發生錯誤。

你的 feature 是我的 bug

我推想 IPython 這樣的設計,應該是想增加彈性,只要透過 data 參數,就可以依據需要傳單純的內容,或者是傳入可以載入內容的網址或檔案路徑,因為在 IPython 的文件上,data 參數就是多用途的:

data (unicode, str or bytes) – The raw data or a URL or file to load the data from

所以這應該是個 feature,可是我認為你都另外獨立有 urlfilename 參數了,這個彈性只是會增加意外的驚嚇!

目前唯一的解法,就是在建立這些 DisplayObject 家族的物件時,必須自己檢查傳入的字串會不會剛好是某個檔案或是資料夾的路徑,如果是,就要自己用其他方式避開,例如變成 inline code:

m = Markdown('`sample_data`')

就不會有事,或者是在字串開頭隨意加個空白字元之類的,總之,這錯誤就是要在剛好的狀況下才會發生,但突然遇到就會覺得真是怪,好特別的 feature。

對了,由於這是 IPython 的問題,所以使用 Jupyter 也會遇到類似的狀況。