fastai.fastcore patch decorator vs simpe monkey-patching

Question:

I’m trying to understand the value-added of using fastai‘s fastcore.basics.patch_to decorator. Here’s the fastcore way:

from fastcore.basics import patch_to


class _T3(int):
    pass


@patch_to(_T3)
def func1(self, a):
    return self + a

And here’s the simple monkey-patching approach:

class simple_T3(int):
    pass


def func1(self, a):
    return self + a


simple_T3.func1 = func1

Inspecting the two classes does not reveal any differences. I understand that simple monkey-patching might cause problems in more complex cases, so it would be great to know what such cases are? In other words, what’s the value-added of fastcore.basics.patch_to?

Asked By: SultanOrazbayev

||

Answers:

TL;DR

More informative debugging messages, better IDE support.


Answer

patch and patch_to are decorators in the fastcore basics module that are helpful to make the monkey_patched method to look more like as if it was a method originally placed inside the Class, the classical way (pun intended).

If you create a function outside a class and then monkey-patch it, the outsider method typically has different attributes, such as its name, module, and documentation, compared to the original function. This can be confusing and unhelpful when debugging or working with the "outsider" function.

Source: Official documentation: https://github.com/fastai/fastcore/blob/master/nbs/01_basics.ipynb


Usage suggestion

Consider using patch instead of patch_to, because this way you can add type annotations.

from fastcore.basics import patch

class _T3(int):
    pass

@patch
def func1(self: _T3, a):
    return self + a

What if I don’t want to use the library?

Credits: Kai Lichtenberg

fastcore itself is extremely low weight: The only external library used is numpy (and dataclasses if your python is < 3.7).

But if you really want to not use it, here’s an implementation with only two built-in dependencies:

import functools
from types import FunctionType

def copy_func(f):
    "Copy a non-builtin function (NB `copy.copy` does not work for this)"
    if not isinstance(f,FunctionType): return copy(f)
    fn = FunctionType(f.__code__, f.__globals__, f.__name__, f.__defaults__, f.__closure__)
    fn.__dict__.update(f.__dict__)
    return fn

def patch_to(cls, as_prop=False):
    "Decorator: add `f` to `cls`"
    if not isinstance(cls, (tuple,list)): cls=(cls,)
    def _inner(f):
        for c_ in cls:
            nf = copy_func(f)
            # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually
            for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o))
            nf.__qualname__ = f"{c_.__name__}.{f.__name__}"
            setattr(c_, f.__name__, property(nf) if as_prop else nf)
        return f
    return _inner

def patch(f):
    "Decorator: add `f` to the first parameter's class (based on f's type annotations)"
    cls = next(iter(f.__annotations__.values()))
    return patch_to(cls)(f)
class MyClass():
    def __init__(self):
        pass
    
@patch
def new_fun(self:MyClass):
    print("I'm a patched function!")
    
MyInstance = MyClass()
MyInstance.new_fun()
"I'm a patched function!"