EDIT This code contains several bugs, see jsbueno's answer below for a correct version
I would like to create read-only attributes that dynamically retrieve values from an internal dictionary. I have tried to implement these as descriptors:
from typing import Any
class AttDesc:
def __init__(self, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj._d[self.name]
def __set__(self, obj, value):
raise AtrributeError("Read only!")
class A:
def __init__(self, klist: list[str], vlist: list[Any]) -> None:
self._d = dict(zip(klist, vlist))
for k in self._d:
setattr(type(self), k, AttDesc(k))
@property
def d(self):
return self._d
The problem with this approach is that the descriptor instances are class attributes. This means that, in an interactive session:
a1 = A(['x', 'y'], [1, 2])
a2 = A(['z', ], [3])
if I press TAB for autocomplete on a1.
I will be given the option to choose the attribute z
, which "belongs" to instance a2
. I have also tried to implement via the instance's __getattr__
method:
class B:
def __init__(self, klist: list[str], vlist: list[Any]):
object.__setattr__(self, '_d', dict(zip(klist, vlist)))
@property
def d(self):
return self._d
def __getattr__(self, name):
if name in self.d:
return self.d[name]
else:
object.__getattr__(name)
def __setattr__(self, k, v):
if k in self.d:
raise AttributeError("Read only!")
object.__setattr__(self, k, v)
If I try b = B(['w'], [3])
in an interactive session, pressing TAB on b.
won't show w
as an option, because it's not an instance attribute.
Pandas does something similar to what I want: it allows accessing the columns of a DataFrame with the dot operator, and only the appropriate columns for a given instance show up upon pressing TAB in an interactive session. I have tried to look into the Pandas code but it is a bit abstruse to me. I think they use something similar to my second __getattr__
option, but I don't understand how they make it work.
How could I implement this behaviour?
Descriptors are always implemented on the class - So, yes, if you instantiate one object, and change class attributes when doing so, you will change all other instances automatically - that is how class and instances object work in Python and a large number of other OOP languages.
Descriptors are a mechanism which operates in the class namespace - but Python dynamism allows you to create other customizations.
In this case, all you need is a custom __setattr__
attribute, along with a __getattr__
and __dir__
methods (__dir__
should make autocomplete work for most tools).
from types import MappingProxyType
from typing import Any
class A:
_d = {}
def __init__(self, klist: list[str], vlist: list[Any]) -> None:
self._d = dict(zip(klist, vlist))
@property
def d(self):
# Read only dict:
return MappingProxyType(self._d)
def __setattr__(self, attrname, value):
if attrname in self._d:
raise TypeError("Read only attribute")
return super().__setattr__(attrname, value)
def __dir__(self):
attrs = super().__dir__()
attrs.remove("_d")
attrs.extend(self._d.keys())
return attrs
def __getattr__(self, attrname):
try:
return self._d[attrname]
except KeyError as exc:
raise AttributeError(attrname)
a = A(['x', 'y'], [1, 2])
b = A(['z', ], [3])
And in the interactive mode:
In [22]: a.x
Out[22]: 1
In [23]: b = A(['z', ], [3])
In [24]: b.z
Out[24]: 3
In [25]: a.z
---------------------------------------------------------------------------
...
...
AttributeError: z
In [26]: b.z = 5
---------------
...
TypeError: Read only attribute
# and pressing tab after `a.`:
In [29]: a.d
d x y