Are these `.` attribute bindings necessary in the implementation of `functools.partial`?



docs.python.org says that functools.partial is roughly equivalent to:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

(Note: / is used to denote func as a positional-only argument of partial. See [1].)

If I understand correctly, when a variable is referenced within a nested function, such as newfunc, Python first looks for the variable definition within the nested function. If the definition is not found there, Python will next look for the definition in the enclosing scope (i.e. the outer function; partial in this case). So, are the explicit .func, .args, and .keywords attribute bindings to newfunc above really necessary? I tried an example without said bindings and it partial worked just fine? Is there a case where they might be necessary?

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    # newfunc.func = func
    # newfunc.args = args
    # newfunc.keywords = keywords
    return newfunc

# p0, p1, p2 are positional arguments
# kw0, kw1, kw2 are keyword-only arguments
def foo3(p0, p1, p2, *, kw0, kw1, kw2):
    return 100*p2 + 10*p1 + 1*p0, kw0 + kw1 + kw3

foo2 = partial(foo3, 1, kw0=1+1j)

print(foo2(2,3,kw1=2+2j, kw2=3+3j)) # (321, (6+6j))

Are the . bindings necessary if the keywords or fkeywords dictionaries includes an item with func, args, or keywords as the keyword? What would be an example where these are necessary? As far as I can tell, that’s not a reason because the following works:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    # newfunc.func = func
    # newfunc.args = args
    # newfunc.keywords = keywords
    return newfunc

# p0, p1, p2 are positional arguments
# kw0, kw1, kw2 are keyword-only arguments
def foo3(p0, p1, p2, kw0, kw1, kw2, **kwargs):
    return 100*p2 + 10*p1 + 1*p0, kw0 + kw1 + kw2 + sum(kwargs.values())

foo2 = partial(foo3, 1, kw0=1+1J, func=10, args=10j, keywords=100+100j)

print(foo2(2,3,kw1=2+2J, kw2=3+3J, func=20, args=20j, keywords=200+200j)) # (321, (226+6j))

[1] https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters

Answer

I think you can look at the partial class implementation to help you understand better.

The following (Python 3.9.5)

class partial:
    """New function with partial application of the given arguments
    and keywords.
    """

    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(partial, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)
    
    ...

When you replace self with newfunc, they’re pretty much the same.



Source: stackoverflow