Thursday, 28 July 2022

functools Partial and __name__

I've recently come across a small issue with python functools and a simple solution. First, let's start with some theory.

You can get the name of a class by means of __name__. Maybe you think that's an attribute of the class (I mean, given a class Person, you could think that you have the __name__ attribute in Person.__dict__). That's wrong. __name__ is a descriptor in Person.__class__ (Person's metaclass), hence, a descriptor in Person.__class__.__dict__. Notice that if __name__ were directly an attribute in the class, you could do person_instance.__name__, something that is not possible


class Person:
	pass
Person.__name__
Out[52]: 'Person'

Person.__dict__["__name__"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [53], in ()
----> 1 Person.__dict__["__name__"]

KeyError: '__name__'

Person.__class__.__dict__["__name__"]
Out[54]: <attribute '__name__' of 'type' objects>

type(Person.__class__.__dict__["__name__"])
Out[55]: getset_descriptor

p1 = Person()

p1.__name__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [57], in ()
----> 1 p1.__name__

AttributeError: 'Person' object has no attribute '__name__'

You can also get the __name__ of a function. Again, this is not an attribute directly in each function dictionary, but a descriptor in the function's __class__.__dict__. Python functions are objects that are instances of the class "function", so it's interesting how instances of the function class has a __name__, by means of the __dict__[__name__] in the function class, but as we've seen above, a normal class like Person does not have such __name__ in its own __dict__.


def say_hi(who):
    print(f"hi {who}")
    

say_hi.__name__
Out[60]: 'say_hi'

say_hi.__dict__["__name__"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [61], in ()
----> 1 say_hi.__dict__["__name__"]

KeyError: '__name__'

say_hi.__class__.__dict__["__name__"]
Out[62]: <

The functools module provides us with all (or most) "functional helper" functions that we can be used to from other languages. functools.partial is used to create partially applied funtions, so it's similar to JavaScript Function.prototype.bind. Interestingly, functions.partial does not return another function (a closure trapping the parameters being bound and the original function itself), but an instance of the functools.partial class (as such class has a __call__ method, that instance is callable and is mainly like a function to us). The problem I've found with that partial object is that it lacks a __name__ attribute.


def format_message(wrap_str, msg):
    return f"{wrap_str}{msg}{wrap_str}"

format_with_x = functools.partial(format_message, "x")
print(format_with_x("Hi"))

print(type(format_message))
print(format_message.__name__)

print(type(format_with_x))
try:
    print(format_with_x.__name__)
except BaseException as ex:
    print(f"Exception: {ex}")

# xHix
# <class 'function'>
# format_message
# <class 'functools.partial'>
# Exception: 'functools.partial' object has no attribute '__name__'


I've found here that functools provides a fix for this, the functools.updatewrapper function, that "Updates a wrapper function to look like the wrapped function". This means that we can just do this:


def create_wrapped_partial(func, *args, **kwargs):
    partial_func = functools.partial(func, *args, **kwargs)
    functools.update_wrapper(partial_func, func)
    return partial_func
	
format_with_z = create_wrapped_partial(format_message, "z")
print(format_with_z("Hi"))
print(type(format_with_z))
try:
    print(format_with_z.__name__)
except BaseException as ex:
    print(f"Exception: {ex}")
	
# zHiz
# <class 'function'>
# format_message
# <class 'functools.partial'>
# format_message	

No comments:

Post a Comment