Jupyter – Split Classes in multiple Cells

Question:

I wonder if there is a possibility to split jupyter classes into different cells? Lets say:


#first cell:
class foo(object):
    def __init__(self, var):
        self.var = var

#second cell
    def print_var(self):
       print(self.var)

For more complex classes its really annoying to write them into one cell.
I would like to put each method in a different cell.

Someone made this this last year but i wonder if there is something build in so i dont need external scripts/imports.

And if not, i would like to know if there is a reason to not give the opportunity to split your code and document / debug it way easier.

Thanks in advance

Asked By: Florian H

||

Answers:

I don’t feel like that whole stuff to be a issue or a good idea… But maybe the following will work for you:


# First cell
class Foo(object):
    pass

# Other cell
def __init__(self, var):
    self.var = var

Foo.__init__ = __init__

# Yet another cell
def print_var(self):
   print(self.var)
Foo.print_var = print_var

I don’t expect it to be extremely robust, but… it should work for regular classes.

EDIT: I believe that there are a couple of situations where this may break. I am not sure if that will resist code inspection, given that the method lives “far” from the class. But you are using a notebook, so code inspection should not be an issue (?), although keep that in mind if debugging.

Another possible issue can be related to use of metaclasses. If you try to use metaclasses (or derive from some class which uses a metaclass) that may broke it, because metaclasses typically expect to be able to know all the methods of the class, and by dynamically adding methods to a class, we are bending the rules on the flow of class creation.

Without metaclasses or some “quite-strange” use cases, the approach should be safe-ish.

For “simple” classes, it is a perfectly valid approach. But… it is not exactly an expected feature, so (ab)using it may give some additional problems which I may not

Answered By: MariusSiuram

There is no way to split a single class,
You could however, add methods dynamically to an instance of it

CELL #1

import types
class A:
    def __init__(self, var):
        self.var = var

a = A()

And in a different cell:

CELL #2

def print_var(self):
    print (self.var)
a.print_var = types.MethodType( print_var, a )

Now, this should work:

CELL #3

a.print_var()
Answered By: Uri Goren

Two solutions were provided to this problem on Github issue “Define a Python class across multiple cells #1243” which can be found here: https://github.com/jupyter/notebook/issues/1243

One solution is using a magic function from a package developed for this specific case called jdc – or Jupyter dynamic classes. The documentation on how to install it and how to use can be found on package url at https://alexhagen.github.io/jdc/

The second solution was provided by Doug Blank and which just work in regular Python, without resorting to any extra magic as follows:

Cell 1:

class MyClass():
    def method1(self):
        print("method1")

Cell 2:

class MyClass(MyClass):
    def method2(self):
        print("method2")

Cell 3:

instance = MyClass()
instance.method1()
instance.method2()

I tested the second solution myself in both Jupyter Notebook and VS Code, and it worked fine in both environments, except that I got a pylint error [pylint] E0102:class already defined line 5 in VS Code, which is kind of expected but still runs fine. Moreover, VS Code was not meant to be the target environment anyway.

Answered By: Medhat Omr

Medhat Omr’s answer provides some good options; another one I found that I thought someone might find useful is to dynamically assign methods to a class using a decorator function. For example, we can create a higher-order function like the one below, which takes some arbitrary function, gets its name as a string, and assigns it as a class method.

def classMethod(func):
    setattr(MyClass, func.__name__, func)
    return func

We can then use the syntactic sugar for a decorator above each method that should be bound to the class;

@classMethod
def get_numpy(self):
    return np.array(self.data)

This way, each method can be stored in a different Jupyter notebook cell and the class will be updated with the new function each time the cell is run.

I should also note that since this initializes the methods as functions in the global scope, it might be a good idea to prefix them with an underscore or letter to avoid name conflicts (then replace func.__name__ with func.__name__[1:] or however characters at the beginning of each name you want to omit. The method will still have the "mangled" name since it is the same object, so be wary of this if you need to programmatically access the method name somewhere else in your program.

Here’s a decorator which lets you add members to a class:

import functools
def update_class(
    main_class=None, exclude=("__module__", "__name__", "__dict__", "__weakref__")
):
    """Class decorator. Adds all methods and members from the wrapped class to main_class

    Args:
    - main_class: class to which to append members. Defaults to the class with the same name as the wrapped class
    - exclude: black-list of members which should not be copied
    """

    def decorates(main_class, exclude, appended_class):
        if main_class is None:
            main_class = globals()[appended_class.__name__]
        for k, v in appended_class.__dict__.items():
            if k not in exclude:
                setattr(main_class, k, v)
        return main_class

    return functools.partial(decorates, main_class, exclude)

Use it like this:

#%% Cell 1
class MyClass:
    def method1(self):
        print("method1")
me = MyClass()

#%% Cell 2
@update_class()
class MyClass:
    def method2(self):
        print("method2")
me.method1()
me.method2()

This solution has the following benefits:

  • pure python
  • Doesn’t change the inheritance order
  • Effects existing instances
Answered By: Quantum7

thanks@Medhat Omr, it works for me for the @classmethod as well.

Base class in the first cell

class Employee:
    # define two class variables
    num_empl = 0
    raise_amt = 1.05

    def __init__(self, first, last, pay):

        self.first = first 
        self.last = last
        self.pay = pay
        ...
        ...

@classmethod in an another cell:

class Employee(Employee):

    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amt = amount


empl = Employee("Jahn", "Smith", 65000)
Employee.set_raise_amt(1.04)
print(empl.full_name() + " is getting " + str(empl.apply_raise()))
Answered By: aVral
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.