[Tutor] class decorator question

Albert-Jan Roskam fomcl at yahoo.com
Sat Oct 5 21:26:14 CEST 2013


_______________________________

> From: Steven D'Aprano <steve at pearwood.info>
>To: tutor at python.org 
>Sent: Saturday, October 5, 2013 3:14 PM
>Subject: Re: [Tutor] class decorator question
>
>On Sat, Oct 05, 2013 at 05:33:46AM -0700, Albert-Jan Roskam wrote:
>> Hi,
>> 
>> On http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ I saw 
>> a very cool and useful example of a class decorator. It (re)implements 
>> __str__ and __unicode__ in case Python 2 is used. For Python 3, the 
>> decorator does nothing. I wanted to generalize this decorator so the 
>> __str__ method under Python 2 encodes the string to an arbitrary 
>> encoding. This is what I've created: http://pastebin.com/vghD1bVJ.
>> 
>> It works, but the code is not very easy to understand, I am affraid. 
>
>It's easy to understand, it's just doing it the wrong way. It creates 
>and subclass of your class, which it shouldn't do. 



Why not? Because it's an unusual coding pattern? Or is it ineffecient?
I subclassed because I needed the encoding value in the decorator.
But subclassing may indeed have been overkill.



Here's a better 
>approach: inject the appropriate methods into the class directly. Here's 
>a version for Python 3:
>
>
>def decorate(cls):
>    if '__str__' not in cls.__dict__:
>        # inject __str__ method
>        def __str__(self):
>            ...
>        cls.__str__ = __str__
>
>    if '__bytes__' not in cls.__dict__:
>        # like above
>
>    return cls
>
>
>This avoids overwriting __str__ if it is already defined, and likewise 
>for __bytes__.


Doesn't a class always have __str__ implementation?

>>> class Foo(object): pass
>>> f = Foo()
>>> f.__str__
<method-wrapper '__str__' of Foo object at 0xb583642c>
>>> Foo.__str__
<slot wrapper '__str__' of 'object' objects>


>>> 
>> Or is it? And I have no idea how to call the class Klass. Maybe 
>> reimplements_Test? Is this a good approach, or can this be done in an 
>> easier way? I would *really* like keep statements "if 
>> sys.version_info[0] == 3..." separate from the "main" code. Also, 
>> learning about class decorators is cool ;-). So the code below... 
>> mehhh  no sir, I don't like it.

Btw, that was a quote: http://www.youtube.com/watch?v=dQ3acvz5LfI ;-)

>> 
>> 
>> def __str__(self):
>> 
>>     if sys.version_info[0] == 3:
>>         blah
>>     else:
>>         bleh
>>   
>> if sys.version_info[0] == 2:
>>     def __unicode__(self):
>>         blooooh
>
>
>That performs the version check every time the __str__ method is called. 


Good point.

>Better would be something like this:
>
>if sys.version_info[0] == 2:
>    def __str__(self):
>        ...
>
>    def __unicode__(self):
>        ...
>
>else:
>    def __bytes__(self):
>        ...
>
>    def __str__(self):
>        ...
>
>If you don't like repeating the code twice, once for version 2 and once 
>for version 3, you may be able to define the methods once, then rename 
>them, something like this:
>
># Assume version 3
>def __str__(self):
>    ...
>
>def __bytes__(self):
>    ...
>
>if sys.version_info[0] == 2:
>    __str__.__name__ = '__unicode__'
>    __bytes.__name__ = '__str__'
>    # Inject into the class, as above.
>    cls.__unicode__ = __str__
>    cls.__str__ = __bytes__
>
>else:
>    cls.__str__ = __str__
>    cls.__bytes__ = __bytes__
>
>
>Combining this with the decorator is left for you :-)

Nice, thanks Steven. I made a couple of versions after reading your advise. The main change that I still had to somehow retrieve the encoding value from the class to be decorated (decoratee?). I simply stored it in __dict__. Here is the second version that I created: http://pastebin.com/te3Ap50C. I tested it in Python 2 and 3. The Test class contains __str__ and __unicode__ which are renamed and redefined by the decorator if Python 3 (or 4, or..) is used.


General question: I am using pastebin now. Is that okay, given that this is not part of the "memory" of the Python Tutor archive? It might be annoying if people search the archives and get 404s if they try to follow these links. Just in case I am also pasting the code below:

from __future__ import print_function
import sys
    
def decorate(cls):
    print("decorate called")
    if sys.version_info[0] > 2:
        cls.__dict__["__str__"].__name__ = '__bytes__'
        cls.__dict__["__unicode__"].__name__ = '__str__'
        cls.__bytes__ = cls.__dict__["__str__"]
        cls.__str__ = cls.__dict__["__unicode__"]  
    return cls

@decorate
class Test(object):

    def __init__(self):
        self.__dict__["encoding"] = self.encoding

    def __str__(self):
        return "str called".encode(self.encoding)

    def __unicode__(self):
        return "unicode called"

    @property
    def encoding(self):
        """In reality this method extracts the encoding from a file"""
        return "utf-8" # rot13 no longer exists in Python3

if __name__ == "__main__":
    t = Test()
    if sys.version_info[0] == 2:
        print(unicode(t))
    print(str(t))


More information about the Tutor mailing list