Notebooks importieren

Um modularer entwickeln zu können, ist der Import von Notebooks erforderlich. Da Notebooks jedoch keine Python-Dateien sind, lassen sie sich auch nicht so einfach importieren. Glücklicherweise stellt Python einige Hooks für den Import bereit, sodass IPython-Notebooks schließlich doch importiert werden können.

[1]:
import os, sys, types
[2]:
import nbformat

from IPython import get_ipython
from IPython.core.interactiveshell import InteractiveShell

Import-Hooks haben normalerweise zwei Objekte:

  • Module Loader, der einen Modulnamen (z.B. IPython.display) annimmt und ein Modul zurückgibt

  • Module Finder, der herausfindet, ob ein Modul vorhanden ist, und Python mitteilt, welcher Loader verwendet werden soll

Zunächst jedoch schreiben wir eine Methode, die ein Notebook anhand des vollständig qualifizierten Namen und des optionalen Pfades findet. So wird z.B. aus mypackage.foo mypackage/foo.ipynb und ersetzt Foo_Bar durch Foo Bar, wenn Foo_Bar nicht existiert.

[3]:
def find_notebook(fullname, path=None):
    name = fullname.rsplit(".", 1)[-1]
    if not path:
        path = [""]
    for d in path:
        nb_path = os.path.join(d, name + ".ipynb")
        if os.path.isfile(nb_path):
            return nb_path
        # let import Foo_Bar find "Foo Bar.ipynb"
        nb_path = nb_path.replace("_", " ")
        if os.path.isfile(nb_path):
            return nb_path

Notebook Loader

Der Notebook Loader führt die folgenden drei Schritte aus:

  1. Laden des Notebook-Dokuments in den Speicher

  2. Erstellen eines leeren Moduls

  3. Ausführen jeder Zelle im Modul-Namensraum

    Da IPython-Zellen eine erweiterte Syntax haben können, wird mit transform_cell jede Zelle in reinen Python-Code umgewandelt, bevor er ausgeführt wird.

[4]:
class NotebookLoader(object):
    """Module Loader for IPython Notebooks"""

    def __init__(self, path=None):
        self.shell = InteractiveShell.instance()
        self.path = path

    def load_module(self, fullname):
        """import a notebook as a module"""
        path = find_notebook(fullname, self.path)

        print("importing notebook from %s" % path)

        # load the notebook object
        nb = nbformat.read(path, as_version=4)

        # create the module and add it to sys.modules
        # if name in sys.modules:
        #    return sys.modules[name]
        mod = types.ModuleType(fullname)
        mod.__file__ = path
        mod.__loader__ = self
        mod.__dict__["get_ipython"] = get_ipython
        sys.modules[fullname] = mod

        # extra work to ensure that magics that would affect the user_ns
        # magics that would affect the user_ns actually affect the
        # notebook module’s ns
        save_user_ns = self.shell.user_ns
        self.shell.user_ns = mod.__dict__

        try:
            for cell in nb.cells:
                if cell.cell_type == "code":
                    # transform the input to executable Python
                    code = self.shell.input_transformer_manager.transform_cell(
                        cell.source
                    )
                    # run the code in the module
                    exec(code, mod.__dict__)
        finally:
            self.shell.user_ns = save_user_ns
        return mod

Notebook Finder

Der Finder ist ein einfaches Objekt, das angibt, ob ein Notebook anhand seines Dateinamens importiert werden kann, und das den entsprechenden Loader zurückgibt.

[5]:
class NotebookFinder(object):
    """Module Finder finds the transformed IPython Notebook"""

    def __init__(self):
        self.loaders = {}

    def find_module(self, fullname, path=None):
        nb_path = find_notebook(fullname, path)
        if not nb_path:
            return

        key = path
        if path:
            # lists aren’t hashable
            key = os.path.sep.join(path)

        if key not in self.loaders:
            self.loaders[key] = NotebookLoader(path)
        return self.loaders[key]

Hook registrieren

Jetzt registrieren wir NotebookFinder mit sys.meta_path:

[6]:
sys.meta_path.append(NotebookFinder())

Überprüfen

Nun sollte unser Notebook mypackage/foo.ipynb importierbar sein mit

[7]:
from mypackage import foo
importing notebook from /Users/veit/cusy/trn/Python4DataScience-de/docs/workspace/ipython/mypackage/foo.ipynb

Wird die Python-Methode bar ausgeführt?

[8]:
foo.bar()
[8]:
'bar'

… und die IPython-Syntax?

[9]:
foo.has_ip_syntax()
[9]:
['debugging.ipynb',
 'display.ipynb',
 'examples.ipynb',
 'extensions.rst',
 'importing.ipynb',
 'index.rst',
 'magics.ipynb',
 '\x1b[34mmypackage\x1b[m\x1b[m',
 'myscript.py',
 'shell.ipynb',
 'start.rst',
 '\x1b[31mtab-completion-for-anything.png\x1b[m\x1b[m',
 '\x1b[31mtab-completion-for-modules.png\x1b[m\x1b[m',
 '\x1b[31mtab-completion-for-objects.png\x1b[m\x1b[m',
 '\x1b[34munix-shell\x1b[m\x1b[m']

Wiederverwendbarer Import-Hook

Der Import-Hook kann auch einfach in anderen Notebooks ausgeführt werden mit

[10]:
%run importing.ipynb
importing notebook from /Users/veit/cusy/trn/Python4DataScience-de/docs/workspace/ipython/mypackage/foo.ipynb