functools.wrapper - AttributeError: attribute '__doc__' of 'type' objects is not writable
Asked Answered
D

3

6

While executing the code below, I'm getting AttributeError: attribute '__doc__' of 'type' objects is not writable.

from functools import wraps

def memoize(f):
    """ Memoization decorator for functions taking one or more arguments.
        Saves repeated api calls for a given value, by caching it.
    """
    @wraps(f)
    class memodict(dict):
       """memodict"""
       def __init__(self, f):
           self.f = f
       def __call__(self, *args):
           return self[args]
       def __missing__(self, key):
           ret = self[key] = self.f(*key)
           return ret
     return memodict(f)

@memoize
def a():
    """blah"""
    pass

Traceback:

AttributeError Traceback (most recent call last)
<ipython-input-37-2afb130b1dd6> in <module>()
     17             return ret
     18     return memodict(f)
---> 19 @memoize
     20 def a():
     21     """blah"""

<ipython-input-37-2afb130b1dd6> in memoize(f)
      7     """
      8     @wraps(f)
----> 9     class memodict(dict):
     10         """memodict"""
     11         def __init__(self, f):

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/functools.pyc in update_wrapper(wrapper, wrapped, assigned, updated)
     31     """
     32     for attr in assigned:
---> 33         setattr(wrapper, attr, getattr(wrapped, attr))
     34     for attr in updated:
     35         getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

AttributeError: attribute '__doc__' of 'type' objects is not writable

Even though the doc string is provided, I don't know what's wrong with this.

It's works fine if not wrapped, but I need to do this.

Dodds answered 18/8, 2016 at 5:6 Comment(1)
It's not the cause of this particular problem, but you're attempting to apply the decorator to a function that takes zero arguments, not one or more—so the doc-string of memoize() appears to be wrong.Grangerize
A
1

@wraps(f) is primarily designed to be used as a function decorator, rather than as a class decorator, so using it as the latter may lead to the occasional odd quirk.

The specific error message you're receiving relates to a limitation of builtin types on Python 2:

>>> class C(object): pass
... 
>>> C.__doc__ = "Not allowed"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute '__doc__' of 'type' objects is not writable

If you use Python 3, switch to a classic class in Python 2 (by inheriting from UserDict.UserDict rather than the dict builtin), or use a closure to manage the result cache rather than a class instance, the decorator will be able to copy the docstring over from the underlying function.

Adeline answered 18/8, 2016 at 6:14 Comment(1)
Could you please, exemplify 'switch to a classic class in Python 2 or use a closure to manage the result cache'!Confessedly
G
4

functools.wraps() was designed to wrap function, not class objects. One of the things it does is attempt to assign the __doc__ string of the wrapped (original) function to the wrapper function, which, as you've discovered, isn't allowed in Python 2. It also does the same for the __name__ and __module__ attributes.

A simple way to work around this restriction is by manually doing it when the MemoDict class is defined. Here's what I mean. (Note for increased readability I always use CamelCase class names as per the PEP 8 - Style Guide for Python Code.)

def memoize(f):
    """ Memoization decorator for functions taking one or more arguments.
        Saves repeated api calls for a given value, by caching it.
    """
    class MemoDict(dict):
        __doc__ = f.__doc__
        __name__ = f.__name__
        __module__ = f.__module__

        def __init__(self, f):
            self.f = f
        def __call__(self, *args):
            return self[args]
        def __missing__(self, key):
            ret = self[key] = self.f(*key)
            return ret

    return MemoDict(f)

@memoize
def a():
    """blah"""
    print('Hello world!')

print(a.__doc__)     # -> blah
print(a.__name__)    # -> a
print(a.__module__)  # -> __main__
a()                  # -> Hello world!

In fact, if you wished, you could create your own wrapper / class-decorating function to do it:

def wrap(f):
    """ Convenience function to copy function attributes to derived class. """
    def class_decorator(cls):
        class Derived(cls):
            __doc__ = f.__doc__
            __name__ = f.__name__
            __module__ = f.__module__
        return Derived

    return class_decorator

def memoize(f):
    """ Memoization decorator for functions taking one or more arguments.
        Saves repeated api calls for a given value, by caching it.
    """
    @wrap(f)
    class MemoDict(dict):
        def __init__(self, f):
            self.f = f
        def __call__(self, *args):
            return self[args]
        def __missing__(self, key):
            ret = self[key] = self.f(*key)
            return ret

    return MemoDict(f)

@memoize
def a():
    """blah"""
    print('Hello world!')

print(a.__doc__)     # -> blah
print(a.__name__)    # -> a
print(a.__module__)  # -> __main__
a()                  # -> Hello world!
Grangerize answered 7/1, 2017 at 18:32 Comment(0)
A
1

@wraps(f) is primarily designed to be used as a function decorator, rather than as a class decorator, so using it as the latter may lead to the occasional odd quirk.

The specific error message you're receiving relates to a limitation of builtin types on Python 2:

>>> class C(object): pass
... 
>>> C.__doc__ = "Not allowed"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute '__doc__' of 'type' objects is not writable

If you use Python 3, switch to a classic class in Python 2 (by inheriting from UserDict.UserDict rather than the dict builtin), or use a closure to manage the result cache rather than a class instance, the decorator will be able to copy the docstring over from the underlying function.

Adeline answered 18/8, 2016 at 6:14 Comment(1)
Could you please, exemplify 'switch to a classic class in Python 2 or use a closure to manage the result cache'!Confessedly
T
1

The wraps decorator you're trying to apply to your class doesn't work because you can't modify the docstring of a class after it has been created. You can recreate the error with this code:

class Foo(object):
    """inital docstring"""

Foo.__doc__ = """new docstring""" # raises an exception in Python 2

The exception doesn't occur in Python 3 (I'm not exactly sure why it's changed).

A workaround might be to assign the class variable __doc__ in your class, rather than using wraps to set the docstring after the class exists:

def memoize(f):
    """ Memoization decorator for functions taking one or more arguments.
        Saves repeated api calls for a given value, by caching it.
    """
    class memodict(dict):
       __doc__ = f.__doc__  # copy docstring to class variable
       def __init__(self, f):
           self.f = f
       def __call__(self, *args):
           return self[args]
       def __missing__(self, key):
           ret = self[key] = self.f(*key)
           return ret
     return memodict(f)

This won't copy any of the other attributes that wraps tries to copy (like __name__, etc.). You may want to fix those up yourself if they're important to you. The __name__ attribute however needs to be set after the class is created (you can't assign it in the class definition):

class Foo(object):
    __name__ = "Bar" # this has no effect

Foo.__name__ = "Bar" # this works
Transitory answered 18/8, 2016 at 6:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.