thorwhalen / umpyre

Materials for python coaching
MIT License
1 stars 0 forks source link

partial_plus (code review) #21

Open thorwhalen opened 3 years ago

thorwhalen commented 3 years ago
from functools import partial

writable_function_dunders = {
    '__annotations__',
    '__call__',
    '__defaults__',
    '__dict__',
    '__doc__',
    '__globals__',
    '__kwdefaults__',
    '__name__',
    '__qualname__'}

def partial_plus(func, *args, **kwargs):
    """Like partial, but with the ability to add 'normal function' stuff (name, doc) to the curried function.

    Note: if no writable_function_dunders is specified will just act as the builtin partial (which it calls first).

    >>> def foo(a, b): return a + b
    >>> f = partial_plus(foo, b=2, __name__='bar', __doc__='foo, but with b=2')
    >>> f.__name__
    'bar'
    >>> f.__doc__
    'foo, but with b=2'
    """
    dunders_in_kwargs = writable_function_dunders.intersection(kwargs)

    def gen():
        for dunder in dunders_in_kwargs:
            dunder_val = kwargs.pop(dunder)
            yield dunder, dunder_val

    dunders_to_write = dict(gen())  # will remove dunders from kwargs
    partial_func = partial(func, *args, **kwargs)
    for dunder, dunder_val in dunders_to_write.items():
        setattr(partial_func, dunder, dunder_val)
    return partial_func

There's a more general context here: what's the "right" way to make callables behave like full fledge functions. For example, in the following, I needed to add a __name__ to avoid running into problems when processing expects callables to have a __name__. I can of course encapsulate the get_name need into a more careful function (e.g. if doesn't exist, use type(o).__name__ as a fallback). But that will work only when I need a name. How do I define my objects to be robust if third-party code that expects this __name__?

@dataclass
class Segmenter:
    buffer: BufferStats
    stats_buffer_callback: Callable[[Stats, Iterable], Any] = return_buffer_on_stats_condition
    __name__ = 'Segmenter'

    def __call__(self, new_val):
        stats = self.buffer(new_val)
        return self.stats_buffer_callback(stats, self.buffer)

Note: And if the class is not a dataclass, it's even less clean: I "need" to do a self.__name__ = 'the_name' in the __init__!

Perhaps a decorator would be cleaner. But what to decorate? The following won't work on a class (or rather, won't have the effect wanted on the (callable) instance:

def add_name(obj, name=None):
    if name is None:
        name = type(obj).__name__
    obj.__name__ = name
    return obj