Dynamic attributes or custom dictionary for fixed read-only fields
<p>I am creating a class for retrieving details about a computer such as host_name, kernel_version, bios_version, and so on. Some details are more expensive to collect than others so I have a get_* function to retrieve them, but keep the results cached in the object if they are needed again. I am considering implementing them to look like a dictionary object so the kernel version can be retrieved as so:</p>

<pre><code>system = System()
kver = system['kernel_version']
</code></pre>

<p>This will call the instance method <code>get_kernel_version(self)</code> internally to retrieve the data. If the kernel version is retrieved a second time from the above instantiated object, it will returned the cached result from the original call to <code>get_kernel_version(self)</code>. Note that all these key/value pairs are read-only, there are a fixed number of them based on the available get_* methods, and no new keys can be added later so it doesn't feel like a regular dictionary. There also shouldn't be a need to call something like the <code>values()</code> function which would simply cause all the get_* functions to be needlessly hit. Also, the syntax is a little more verbose than I'd like. Using <code>system.kernel_version</code> instead seems more natural for this use case. </p>

<p>I'm considering whether a better approach is to use dynamic attributes on a class instance. However, I need a natural way to retrieve a list of all attributes, but not the internal methods supporting them. I would probably use the <code>__dir__</code> special method to return a list similar the <code>keys()</code> list of the dictionary. I would want to see kernel_version and host_name in the list, but not <code>__class__</code> or <code>get_kernel_version</code>. This seems to go against recommended practice for the definition of <code>__dir__</code> so I'm not sure if this is the right approach to use.</p>

<p>I could return a proxy class instance whose sole job calls back to a concrete class with the get_* functions when it doesn't have the appropriate attribute already defined.</p>

<p>Here is an example of a version I'm experimenting with implementing the dictionary approach:</p>

<pre><code>class System(dict):
    def __getitem__(self, key):
        try:
            return getattr(self, 'get_'+key)()
        except AttributeError as ex:
            raise KeyError(ex.message)
    def __setitem__(self, key, value):
        raise Exception('Read-only')
    def __delitem__(self, key, value):
        raise Exception('Read-only')
    def keys(self):
        return [ x[4:] for x in dir(self) if x.startswith('get_') ]
    def get_host_name(self):
        return 'localhost'
    def get_kernel_version(self):
        return '4.7.0'

system = System()
print repr(system.keys())
for key in system.keys():
    print '{0}: {1}'.format(key, system[key])

try:
    system['bios']
except Exception as ex:
    print str(ex)

try:
    system['kernel_version'] = '5.0'
except Exception as ex:
    print str(ex)
</code></pre>

<p>Which produced the following output:</p>

<pre><code>['host_name', 'kernel_version']
host_name: localhost
kernel_version: 4.7.0
"'System' object has no attribute 'get_bios'"
Read-only
</code></pre>

<p>The code above does not yet implement the caching of values yet, but that is easy to add. However, it's feeling more like I should be doing this as attributes. I am just not sure if when doing so I should abuse <code>__dir__</code> to emulate the same functionality above I get with <code>keys()</code>.</p>

<p>Should I stick with emulating a read-only dictionary or present a class instance with dynamic attributes?</p>

<p>I think sticking with the read-only dictionary subclass approach you're using is fine. However your implementation could be improved somewhat by creating a generic read-only dictionary superclass from which to derive your specific subclass, and using a metaclass to create the value returned by the <code>keys()</code> method. Doing both is illustrated below.</p>

<p>This  means you don't have to "abuse" <code>dir()</code> (there's no such thing as a <code>__dir__</code> attribute) any longer. You can also use reuse the generic <code>MetaReadonlyDict</code> and <code>ReadonlyDict</code> classes to create other similar types.</p>

<pre><code>class MetaReadonlyDict(type):
    def __new__(mcls, classname, bases, classdict):
        classobj = type.__new__(mcls, classname, bases, classdict)
        prefix = classdict['prefix']
        _keys = set(name[len(prefix):] for name in classdict
                                        if name.startswith(prefix))
        setattr(classobj, 'keys', lambda self: list(_keys))  # define keys()
        return classobj

class ReadonlyDict(dict):
    def __getitem__(self, key):
        try:
            return getattr(self, self.prefix + key)()
        except AttributeError as ex:
            raise Exception(
                "{} object has no {!r} key".format(self.__class__.__name__, key))
    def __setitem__(self, key, value):
        verb = "redefined" if key in self else "defined"
        raise Exception(
            "{} object is read-only: {!r} "
            "key can not be {}".format(self.__class__.__name__, key, verb))
    def __delitem__(self, key):
        raise Exception(
            "{} object is read-only: {!r} "
            "key can not be deleted".format(self.__class__.__name__, key))
    def __contains__(self, key):
        return key in self.keys()

class System(ReadonlyDict):
    __metaclass__ = MetaReadonlyDict
    prefix = '_get_'

    def _get_host_name(self):
        return 'localhost'
    def _get_kernel_version(self):
        return '4.7.0'

system = System()
print('system.keys(): {!r}'.format(system.keys()))
print('values associated with system.keys():')
for key in system.keys():
    print('  {!r}: {!r}'.format(key, system[key]))

try:
    system['bios']
except Exception as ex:
    print(str(ex))

try:
    system['kernel_version'] = '5.0'
except Exception as ex:
    print(str(ex))

try:
    del system['host_name']
except Exception as ex:
    print(str(ex))
</code></pre>

<p>Output:</p>

<pre class="lang-none prettyprint-override"><code>system.keys(): ['kernel_version', 'host_name']
values associated with system.keys():
  'kernel_version': '4.7.0'
  'host_name': 'localhost'
System object has no 'bios' key
System object is read-only: 'kernel_version' key can not be redefined
System object is read-only: 'host_name' key can not be deleted
</code></pre>

python python-2.7 python-2.6