EL_DON
EL_DON

Reputation: 1496

Is there a compact way to declare several similar properties in a python class?

I am working within a larger python module where "projects" containing assemblies of dict-like class instances can be saved, but in order to save a class's attributes, they must be stored in a specific dictionary in the class. I can have my attributes accessible as simple attributes (self.x instead of self.properties['x']) by writing them as properties. But I'm up to 9 of these things, and giving each of them a getter, setter, and deleter seems like such a waste of space, especially since they're all so trivial. Is there a better way?

Too long:

class MyClass(dict):

    @property
    def variable1(self):
        return self.properties.get('variable1', None)

    @variable1.setter
    def variable1(self, value):
        self.properties['variable1'] = value

    @variable1.deleter
    def variable1(self):
        self.properties['variable1'] = None

    # ... same for variables 2 - 8, so boring

    @property
    def variable9(self):
        return self.properties.get('variable9', None)

    @variable9.setter
    def variable9(self, value):
        self.properties['variable9'] = value

    @variable9.deleter
    def variable9(self):
        self.properties['variable9'] = None

    def __init__(self, variable1='default1', variable9='default9'):
        self.properties = dict(variable1=variable1, variable9=variable9)
        dict.__init__(self)

How can I loop over property declarations so this will be shorter?

BONUS: If I do a loop, is there a way to include minor customizations to some of the variables, should the need arise (maybe declare the things to list as keys in a dictionary, where values are instructions for minor customizations)?

Testing / usage of above example:

from var_test import MyClass  # If you saved it in var_test.py
a = MyClass(variable1=1234)
b = MyClass(variable1='wheee')
assert a.variable1 is not b.variable1
assert a.properties['variable1'] == a.variable1
assert a.variable1 == 1234

Upvotes: 2

Views: 502

Answers (2)

EL_DON
EL_DON

Reputation: 1496

The best I could do was to loop over an exec call to assign the results of a property-generating function. It's shorter than the thing in the question statement, but I feel like there must be a better way:

import copy


class MyClass2(dict):
    properties = {}  # Temporary declaration of properties during startup
    save_attrs = ['variable{}'.format(i) for i in range(1, 10)]

    @staticmethod
    def _make_property(props, name, init_value=None, doc=None):
        """Creates a property which can be assigned to a variable, with getter, setter, and deleter methods"""
        props[name] = init_value

        def getter1(self):
            return self.properties.get(name, None)

        def setter1(self, value):
            self.properties[name] = value

        def deleter1(self):
            self.properties[name] = None

        getter1.__name__ = name
        setter1.__name__ = name
        deleter1.__name__ = name
        return property(getter1, setter1, deleter1, doc)

    for attr in save_attrs:
        exec("{attr:} = _make_property.__func__(properties, '{attr:}')".format(attr=attr))

    def __init__(self, **kw):
        # Instance-specific reassignment of properties, so instances won't share values
        self.properties = copy.deepcopy(self.properties)
        for attr in self.save_attrs:
            kw_attr = kw.pop(attr, None)
            if kw_attr is not None:
                self.properties[attr] = kw_attr
        dict.__init__(self)

This passes the test:

from var_test import MyClass2 as mc  # If you saved it in var_test.py
a = mc(variable1=1234)
b = mc(variable1='wheee')
assert a.variable1 is not b.variable1
assert a.properties['variable1'] == a.variable1
assert a.variable1 == 1234

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1121484

Yes, you can create property objects dynamically, and add them to a class:

def gen_property(name):
    def getter(self):
        return self.properties.get(name, None)
    def setter(self, value):
        self.properties[name] = value
    def deleter(self):
        self.properties[name] = None

    return property(getter, setter, deleter)

class MyClass(dict):
    def __init__(self, variable1='default1', variable9='default9'):
        self.properties = dict(variable1=variable1, variable9=variable9)
        dict.__init__(self)

for number in range(1, 10):
    name = 'variable{}'.format(number)
    setattr(MyClass, name, gen_property(name))

However, it is probably much cleaner to use the attribute access customisation hooks and proxy the attribute names to the self.properties dictionary there:

class MyClass(dict):
    def __init__(self, variable1='default1', variable9='default9'):
        self.properties = dict(variable1=variable1, variable9=variable9)
        dict.__init__(self)

    def __getattr__(self, name):
        if name.startswith('variable') and name[8:].isdigit():
            return self.properties.get(name, None)
        raise AttributeError(name)

    def __setattr__(self, name, value):
        if name.startswith('variable') and name[8:].isdigit():
            self.properties[name] = value
            return
        super().__setattr__(name, value)

    def __delattr__(self, name):
        if name.startswith('variable') and name[8:].isdigit():
            self.properties[name] = None
            return
        super().__delattr__(name)

Upvotes: 2

Related Questions