Menasheh
Menasheh

Reputation: 3708

Create python properties from an array, sharing getters and setters?

I've been working on a python script where every time certain values are changed, a parallel value should be modified as well. For example:

self.empire.gold = 345
self.empire.update_times.gold = time.time()

Doing this every time was becoming a pain, so I've been researching how to make this happen automatically. This is what I came up with:

import time


class boring_object:
    pass


class Object:
    def __init__(self, gold):
        self._gold = gold
        self._updates = boring_object()

    @property
    def gold(self):
        return self._gold

    @gold.setter
    def gold(self, value):
        self._gold = value
        self._updates.gold = time.time()

Using this, I can automatically update the time.

However, I want to apply similar setters to a bunch of other resources as well.

I could duplicate this code five times to cover gold, grain, potatoes, etc., but I just want to pass in an array of the resource names to a constructor and have them share generic getters and setters. Is that possible?

Before implementing any of this I'm using something closer to the boring_object():

class Object:
    def __init__(self, array):
        for each in array:
            setattr(self, each, 0)

Besides for lacking the property functionality, this doesn't print() nicely. Bonus points for a solution that both allows for generic getters and setters and prints nicely without too much extra code.

Once I wrote this up, I found this question about getters and setters. One answer suggested using __getattr__ and __setattr__, which would get me closer to my goal, but I don't want to store update times for and mutate all the attributes which I ultimately set on this object - just the ones named when I initialize it. Other attributes should behave as if there was no custom getter/setter logic.

Upvotes: 2

Views: 1915

Answers (2)

Bastian Venthur
Bastian Venthur

Reputation: 16570

I agree with SmCaterpillar as long as you don't need add new resources (e.g. gold, silver, etc.) at run time, I'd avoid the __(g|s)etattr__ magic here and use something like the proposed update method.

Upvotes: 0

SmCaterpillar
SmCaterpillar

Reputation: 7020

__getattr__ and the like are nice magical methods that allow you to be clever. However, I advise you not to be. They make your code hard to understand and to follow. Imagine someone else (or you in a year from now) trying to understand what you did there and why. If there is an explicit and easy to comprehend approach that gets the job done, I would always prefer this.

A python @property is nice if you want to limit access to an attribute or add some small logic to it, like here storing an update time. However, properties don't really fit in your case because you want something that is dynamic and might be configured during runtime. So why not using the obvious? How about some "private" dictionaries (see _possessions and _updates below) to simply store the empire's resources?

Accordingly, here is my suggestion on how to tackle your problem:

import time


class Empire(object):

    def __init__(self, resources):
        self._possessions = {r: 0 for r in resources}
        self._updates = {r: None for r in resources}

    def update(self, resource, value):
        if resource not in self._possessions:
            raise ValueError('Your mighty Empire has no clue what {}'
                             ' really is and how to handle'
                             ' it.'.format(resource))
        self._possessions[resource] = value
        self._updates[resource] = time.time()

    def get(self, resource):
        if resource not in self._possessions:
            raise ValueError('Your poor empire has no {}.'.format(resource))
        return self._possessions[resource]


# Some use cases:
my_little_kingdom = Empire(('gold', 'celebrities', 'vanilla_ice'))

print(my_little_kingdom._updates['gold'])
my_little_kingdom.update('gold', 10)

print(my_little_kingdom.get('gold'))
print(my_little_kingdom._updates['gold'])

try:
    my_little_kingdom.update('bitcoin', 42)
except ValueError as e:
    print(e) 

If you insist on attribute access (1st edit)

By the way, if you really, really want to use the fancy __getattr__ and __setattr__ methods, you can easily extend the class via

def __getattr__(self, item):
    try:
        return self.get(item)
    except ValueError as e:
        # We are trying to get an attribute, so better
        # raise the corresponding error here.
        raise AttributeError(str(e))

def __setattr__(self, key, value):
    if key in ('_possessions', '_updates'):
        # We need to do this to avoid infinite loops.
        # You do see how quickly this gets really complicated!?
        return super().__setattr__(key, value)
    try:
        self.update(key, value)
    except ValueError as e:
        raise AttributeError(str(e))

The problem with __setattr__ becomes quite obvious here. We need to check for real attributes to avoid infinite loops. Stuff like this makes your code pretty convoluted. Anyhow, if you added these two magic methods to your Empire, you can do this now:

my_little_kingdom.gold = 123

print(my_little_kingdom.gold)
print(my_little_kingdom._updates['gold'])

try:
    my_little_kingdom.balloons = 99
except AttributeError as e:
    print(e)

Bonus points: The printable empire (2nd edit)

Of course, to make a pretty printable empire just add this method:

def __str__(self):
    possessions = ', '.join('{value} {key}'.format(value=p[1],
                                                   key=p[0])
                            for p in self._possessions.items())
    return ('A glorious and filthy rich empire ' 
            'owning {}.'.format(possessions))

Now you can print your kingdom via

print(my_little_kingdom)

to see this:

A glorious and filthy rich empire owning 123 gold, 0 celebrities, 0 vanilla_ice.

Upvotes: 2

Related Questions