3

I have a number of similar fields in one of my classes modelling json data. All fields are initialized to None to help static tools know they exist then helper functions help initialize them based on a piece of json data that they are modelling (The SecondHandSongs API if you want to know).

Some pieces of data only retrieves the uri of extra data you have to fetch. So I want to use the old trick of initializing a hidden variable to None and fetching/decoding data on first request. But setattr(self.__class__) looks ugly.

Is there any nicer way to do (setting property dynamically in python)?

def  _initialize_url_fields(self, attrNamesToFactoryFunction, json_data):
    for (name, factoryFunction) in attrNamesToFactoryFunction.iteritems():
        try:
            url = json_data[name]
        except KeyError:
            continue
        setattr(self, name + "_url", url)
        setattr(self, "_" + name, None)    
        setattr(self.__class__, name, property(lambda s: s._getter("_" + name, url, factoryFunction)))        

def _getter(self, hidden_prop_name, url, factoryFunction):
    if not getattr(self, hidden_prop_name):
        json_data = SHSDataAcess.getSHSData(url)
        setattr(self, hidden_prop_name, factoryFunction(json_data))
    return getattr(self, hidden_prop_name)

edit: I've just realized I was trying to set a property in a instance method called from init . As could be expected it failed the second time around.

edit 2:

Here's how I fixed it after realizing that I was setting a property per object(impossible if not a singleton class)

class ShsData(object):
    def  _initialize_url_fields(self, attrNamesToFactoryFunctions, json_data):
        for (name, factoryFunction) in attrNamesToFactoryFunctions.items():
            self._getter_factory_functions[name] = factoryFunction 
            uri = None
            try:
                uri = json_data[name]
            except KeyError:
                pass
            setattr(self, name + "_uri", uri)
            setattr(self, "_" + name, None)

def _fetch_shs_data_on_first_access_getter(base_prop_name):
    def getter(self):
        factoryFunction = self._getter_factory_functions[base_prop_name]
        hidden_prop_name = "_" + base_prop_name 
        uri_prop_name = base_prop_name + "_uri"
        if not getattr(self, hidden_prop_name):
            if getattr(self, uri_prop_name):
                json_data = SHSDataAcess.getSHSData(getattr(self, uri_prop_name))
                setattr(self, hidden_prop_name, factoryFunction(json_data))
            else:
                return None
        return getattr(self, hidden_prop_name)
    return getter

class ShsArtist(ShsData):

    performances_data = property(_fetch_shs_data_on_first_access_getter("performances"))
    creditedWorks_data = property(_fetch_shs_data_on_first_access_getter("creditedWorks"))
    releases_data = property(_fetch_shs_data_on_first_access_getter("releases"))

    def __init__(self, json_data):
        ...
        self._initialize_url_fields({"performances": lambda xs: [ShsPerformance(x) for x in xs],
                                     "creditedWorks": lambda xs: [ShsWork(x) for x in xs],
                                     "releases": lambda xs: [ShsRelease(x) for x in xs]},
                                    json_data)
Roman A. Taycher
  • 18,619
  • 19
  • 86
  • 141
  • 3
    The alternative is to use a custom `__getattr__`, but that won't be any prettier, I'd say. – Martijn Pieters Aug 25 '12 at 14:45
  • What's wrong with properties: http://docs.python.org/howto/descriptor.html. I'm sure you can use a caching decorator to build this. – Marcin Aug 25 '12 at 15:05
  • @Marcin: Note that the OP is already using properties, assigning them dynamically. – Martijn Pieters Aug 25 '12 at 15:10
  • If you don't like `getattr` and `setattr` you could use `__dict__`. Even though I don't think this solves anything. – Bakuriu Aug 25 '12 at 15:13
  • 2
    @MartijnPieters Right, I don't see why he doesn't just add caching to that. – Marcin Aug 25 '12 at 15:29
  • I'd say that writing a custom __getattr__ is the right call here. If this class is instantiated frequently with a lot of different custom attribute names, you'll end up creating a lot of getters attached to the class, which will not be garbage collected. Additionally, if different factoryFunctions are associated to the same key, one would override the other. This looks like a bunch of bugs waiting to happen... – Lucas Wiman Aug 29 '12 at 20:44

1 Answers1

0

I might subclass property to handle your common cases. Something like this:

class shs_klass_property(property):

    def __init__(self, name, klass):
        self.name = name
        self.klass = klass
        self.cached_name = '_%s' % name
        super(shs_klass_property, self).__init__(self.compute)

    def compute(self, obj):
        if not hasattr(obj, self.cached_name):
            if self.name in obj._json_data:
                # if needed handle cases where this isn't a list
                val = [self.klass(x) for x in obj._json_data[self.name]]
            else:
                val = None
            setattr(obj, self.cached_name, val)
        return getattr(obj, self.cached_name)

class ShsData(object):
    def __init__(self, json_data):
        self._json_data = json_data

class ShsWork(ShsData):
    pass

class ShsArtist(ShsData):
    works = shs_klass_property('works', ShsWork)

If you always want to set the uri as well, you could do something like:

# if you change this to pass in "_json_data" too,
# you'd have a simple general purpose delegation decorator
class shs_json_property(property):

    def __init__(self, name):
        self.name = name
        super(shs_json_property, self).__init__(self.compute)

    def compute(self, obj):
        return obj._json_data.get(self.name, None)

# a helper to set both. not necessary but saves a line of code.
def shs_property_pair(name, klass):
    return (shs_klass_property(name, klass),
            shs_json_property(name))

class ShsArtist(ShsData):
    works, works_uri = shs_property_pair('works', ShsWork)
jtg
  • 386
  • 1
  • 5