Search code examples
pythonscopefunctools

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


Solution

  • 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.