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_toclass _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):passdef func1(self, a):return self + asimple_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
?
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 patchclass _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 FunctionTypedef 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 fndef 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 manuallyfor 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 freturn _innerdef 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!"