admin管理员组

文章数量:1388039

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?

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?

Share Improve this question edited Mar 17 at 13:44 peich asked Mar 17 at 10:26 peichpeich 215 bronze badges 8
  • Why are a and b instances of the same class if they have different interfaces? This seems like a design problem that's independent of any IDE-specific features. – chepner Commented Mar 17 at 13:11
  • I edited the post to avoid this confusion. I now call the instances of A a1 and a2, and b is the instance of B. – peich Commented Mar 17 at 13:46
  • That wasn't my confusion: I have the same question about why a1 and a2 are instances of the same class if they have different interfaces. – chepner Commented Mar 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. Different DataFrame instances 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 Commented Mar 17 at 14:22
  • 1 And I'm saying that's not a good design, at least not if you are concerned about static behavior such as autocomplete assumes. – chepner Commented Mar 17 at 14:27
 |  Show 3 more comments

1 Answer 1

Reset to default 0

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

本文标签: Dynamic readonly attributes in python instancesStack Overflow