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?
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