Reputation: 322
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.
Upvotes: 7
Views: 10245
Reputation: 123541
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!
Upvotes: 4
Reputation: 104812
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
Upvotes: 1
Reputation: 41556
@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.
Upvotes: 1