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 Anyclass 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._dThe 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 ona1. I will be given the option to choose the attributez, which "belongs" to instancea2. 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 tryb = B(['w'], [3]) in an interactive session, pressing TAB onb. won't showw 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?
- Why are
aandbinstances of the same class if they have different interfaces? This seems like a design problem that's independent of any IDE-specific features.chepner– chepner2025-03-17 13:11:59 +00:00CommentedMar 17 at 13:11 - I edited the post to avoid this confusion. I now call the instances of
Aa1anda2, andbis the instance ofB.peich– peich2025-03-17 13:46:05 +00:00CommentedMar 17 at 13:46 - That wasn't my confusion: I have the same question about why
a1anda2are instances of the same class if they have different interfaces.chepner– chepner2025-03-17 13:56:38 +00:00CommentedMar 17 at 13:56 - I am still not sure if I understand you correctly. I want to create a class that has attribute-like access to an internal dictionary, much like Pandas provides attribute-like access for the columns of a
DataFrame. DifferentDataFrameinstances can have differently named columns. But they are instances of the same class. Maybe I could dynamically create a factory of classes derived from a common base and have attributes set according to the internal dictionary, but why would I do that if I have a much simpler option available?peich– peich2025-03-17 14:22:47 +00:00CommentedMar 17 at 14:22 - 1And I'm saying that's not a good design, at least not if you are concerned about static behavior such as autocomplete assumes.chepner– chepner2025-03-17 14:27:15 +00:00CommentedMar 17 at 14:27
1 Answer1
Descriptors are always implemented on the class -So, yes, if you instantiate one object, and change class attributes when doing so, youwill 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 MappingProxyTypefrom typing import Anyclass 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.xOut[22]: 1In [23]: b = A(['z', ], [3])In [24]: b.zOut[24]: 3In [25]: a.z---------------------------------------------------------------------------......AttributeError: zIn [26]: b.z = 5---------------...TypeError: Read only attribute# and pressing tab after `a.`:In [29]: a.d d x y2 Comments
__dir__ method existed. Is there any reason to prefersuper().__setattr__ toobject.__setattr__? I see the latter one a lot in the Pandas code. Maybe because the parent class'__setattr__ is also overloaded?super().__setattr__ is the "correct way to do it" , because if you do compose this class with others which also do customize setattr, they could work in a collaborative way. Callign directlyobject.__setattr__ is a way of closing up the design, so no other collaboration on this side is possible - you can simply do that, but if everyone would prefer to simply close all other customization and expansion doors, it is likely Python wouldn't allow anywhere this level of customization for objects to start with. It is part of the language "zeitgeist" to leave doors open.Explore related questions
See similar questions with these tags.