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 也會遇到類似的狀況。

下午在 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 也會遇到類似的狀況。