1

I want to refactor a big part of my code into a generic descriptor for read only attribute access. The following is an example of property based implementation

class A:
    def __init__(self, n):
        self._n = n
        self._top = -1

    @property
    def n(self):
        return self._n

    @property
    def top(self):
        return self._top

    def increase(self):
        self._top += 1

you see I can initialize my A class and increase self._top but not to let user set a.top by omitting property setter method

a = A(7)
a.top
Out[25]: -1
a.increase()
a.top
Out[27]: 0

if I do a.top = 4, it will give me an error

AttributeError: property 'top' of 'A' object has no setter

which is expected. Now, I want to refactor this logic into a descriptor

class ReadOnly:

    def __init__(self):
        self._name = None

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        raise AttributeError("Can't set attribute")

    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute")

class A:
    n = ReadOnly()
    top = ReadOnly()

    def __init__(self, n):
        self.n = n
        self.top = -1

    def increase(self):
        self.top += 1

Well, this doesn't work. I can't even initialize the class A anymore, cause in __init__ it will set n and top immediately and prevent my from initialize.

How to write this logic from property into descriptor?

P.S.

Thank @chepner for this solution. This is what I'm looking for. I made it work. One last thing, if I have a attribute is a list say

class Stack:
    S = ReadOnly()
    n = ReadOnly()
    top = ReadOnly()

    def __init__(self, n):
        self._S = [None] * n
        self._n = n
        self._top = -1  # python offset 1

Now I can't change self.top anymore

>>> s = Stack(4)
>>> s.S
[None, None, None, None]

Nor I can change s

>>> s.S = [1, 3]  # not allowed anymore. Great!
But I can still change an element in the list
>>> s.S[3] = 3
[None, None, None, 3]

How can I prevent list element changes?

6
  • 1
    Please read the example in the documentation, which sets a private name prefixed with an underscore. Commented Apr 2 at 9:53
  • @blhsing I took a close look. Seems promising to have both public and private names but I can still change public name using pete.name = "something", so now both pete._name and pete.name is "something". How to raise exception only when setting public name not private name? If i raise in set it's still going to block Commented Apr 2 at 10:30
  • The private name is essentially an implementation detail of the descriptor, not A, so A should not interact with it directly. Using ReadOnly in A has A tacitly agree to "host" the attribute ReadOnly uses so that you can use the descriptor without instance of A needing to be hashable. Commented Apr 2 at 13:41
  • 1
    I don't think there's a way for the container class to control what you can do with the property value. The descriptor is just used to retrieve the value of s.S, it's not used when you operate on the list contents. Commented Apr 2 at 20:09
  • 1
    See stackoverflow.com/questions/13862300/… and stackoverflow.com/questions/53421101/… for similar questions to your PS. Commented Apr 2 at 20:11

1 Answer 1

2

Instead of disallowing all modifications, simply check if the attribute exists on instance before creating it. If it already exists, raise the AttributeError. Otherwise, let the attribute be created.

def __set__(self, instance, value):
    if self._name in instance.__dict__:
        raise AttributeError("Can't set attribute")
    instance.__dict__[self._name] = value

Also, if I remember correctly, --

Update: I did not. If the class attribute has __get__ and __set__, it takes priority over an instance attribute of the same name. Here is a good reference. However, I would still give the instance attribute a different name for clarity.

--you need to use a different name for the underlying private attribute so that you don't shadow the descriptor.

def __set_name__(self, owner, name):
    self._name = "_" + name
Sign up to request clarification or add additional context in comments.

1 Comment

thank you for your insight. I made it work. One more question I want to ask I wrote in newly edited P.S. Can you take a look? Thank you so much for this

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.