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