Python: cómo habilitar la siguiente API usando class magic

Objetivo:

Dado algo como:

stackoverflow.users['55562'].questions.unanswered()

I want it converted into the following:

http://api.stackoverflow.com/1.1/users/55562/questions/unanswered

I have been able to achieve that, using the following class:

class SO(object):

    def __init__(self,**kwargs):
        self.base_url = kwargs.pop('base_url',[]) or 'http://api.stackoverflow.com/1.1'
        self.uriparts = kwargs.pop('uriparts',[])
        for k,v in kwargs.items():
            setattr(self,k,v)

    def __getattr__(self,key):
        self.uriparts.append(key)
        return self.__class__(**self.__dict__)        

    def __getitem__(self,key):
        return self.__getattr__(key)

    def __call__(self,**kwargs):
        return "%s/%s"%(self.base_url,"/".join(self.uriparts))

if __name__ == '__main__':
    print SO().abc.mno.ghi.jkl()
    print SO().abc.mno['ghi'].jkl()

#prints the following
http://api.stackoverflow.com/1.1/abc/mno/ghi/jkl
http://api.stackoverflow.com/1.1/abc/mno/ghi/jkl

Now my problem is I can't do something like:

stackoverflow = SO()
user1 = stackoverflow.users['55562']
user2 = stackoverflow.users['55462']
print user1.questions.unanswered
print user2.questions.unanswered

#prints the following
http://api.stackoverflow.com/1.1/users/55562/users/55462/questions/unanswered
http://api.stackoverflow.com/1.1/users/55562/users/55462/questions/unanswered/questions/unanswered

Essentially, the user1 and user2 refer to the same SO object, so it can't represent different users.

I have been thinking any pointers to do that would be helpful, because this additional level of functionality would make the API far more interesting.

preguntado el 31 de enero de 12 a las 08:01

En lugar de __call__, it might be more elegant if you implement __str__ instead, since the semantics of the call is to convert the object to a string. This way, you can also print SO objetos directamente. -

@FerdinandBeyer, disagree. The object is not a string nor equivalent to a string. Adding __str__ for print will make it look like a string, but result in confusion because it doesn't act like a string for other purposes. -

@WinstonEwert -- "Adding __str__ for print will make it look like a string" -- that's not true. __str__ is used to get a string representation of an object, not to create a string-like object. It is only used by str() y print(). This is exactly what the OP is trying to achieve -- convert his object to a string. See also his second example, where he forgot the parentheses for __call__. __str__ would be much more intuitive here. -

@FerdinandBeyer, sorry, the OP posted here after a previous question: codereview.stackexchange.com/questions/8455/…. I was thinking in terms of what's he's doing in that question rather then the simplified version he's presented here. Your suggestion does make sense in this context, but not in the context of the original question. -

2 Respuestas

IMHO, when you recreate a new stackoverflow object, you need to separate the arguments from old instance attributes with a deep copy

import copy
........    
def __getattr__(self,key):
    dict = copy.deepcopy(self.__dict__)
    dict['uriparts'].append(key)
    return self.__class__(**dict)
....

If you want more flexibility on the URI parts, an abstraction is needed for a cleaner design. For example:

class SOURIParts(object):
    def __init__(self, so, uriparts, **kwargs):
        self.so = so
        self.uriparts = uriparts
        for k,v in kwargs.items():
            setattr(self,k,v)

    def __getattr__(self,key):
        return SOURIParts(self.so, self.uriparts+[key])

    def __getitem__(self,key):
        return self.__getattr__(key)

    def __call__(self,**kwargs):
        return "%s/%s"%(self.so.base_url,"/".join(self.uriparts))

class SO(object):
    def __init__(self, base_url='http://api.stackoverflow.com/1.1'):
        self.base_url =  base_url

    def __getattr__(self,key):
        return SOURIParts(self, [])

    def __getitem__(self,key):
        return self.__getattr__(key)

Espero que esto ayude.

Respondido el 31 de enero de 12 a las 14:01

Podrías anular __getslice__(Python 2.7), or getitem()(Python3.x) and use a memorizing decorator so that if the slice you request (the userid) has already been looked up it would use cached results -- otherwise it could retrieve the results and populate the existing SO instance object.

However, I think a more OO way to solve the problem is make SO a pure lookup module that returns stack overflow user objects which would then have the deeper-digging lookups for profile details. But thats just me.

Respondido el 31 de enero de 12 a las 13:01

Regarding the OO way, this is not something that I'm going to use by itself and is a part of something else. - lprsd

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.