pythonpython-3.xmonkeypatchingfast-ai

fastai.fastcore patch decorator vs simple monkey-patching


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?


Solution

  • 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 copy import copy
    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!"