Reputation: 1095
The documentation on __slots__
says they're "implemented as descriptors." Is it possible to customize the getters/setters for descriptors created via __slots__
? Can I do something like
class Foo(object):
__slots__ = ['a']
def __init__(self, a):
self._a = a
@property
def a(self):
return self._a.title()
@a.setter
def a(self, x):
if len(x) == 10:
self._a = x
else:
raise ValueError('incorrect length!')
@a.deleter
def a(self):
self._a = ''
ETA: Only semi-relatedly, the self._a = a
bit above would mean that the initial value of a
wouldn't be run through the setter. Is there a way to pass the value into the setter on __init__
as well?
ETA2: So based on Bi Rico's answer, I worked out this:
class Foo(object):
__slots__ = ('_a',)
def __init__(self, x):
self._a = self.validate_a(x)
@staticmethod
def validate_a(x):
if x % 2 == 0:
return x
else:
raise ValueError(':(')
@property
def a(self):
return str(self._a)
@a.setter
def a(self, x):
self._a = self.validate(x)
@a.deleter
def a(self):
self._a = 0
The separate validate_a
method solves my 'add-on' question about treating the value(s) passed into __init__
the same as values passed in through the setter (and as such isn't necessary if you don't want to do that.)
It feels a little hacky to only put 'dummy' var names in __slots__
(i.e., Foo._a
only exists for the benefit of the Foo.a
property), but it works.
Upvotes: 1
Views: 1313
Reputation: 515
Absolutely. You just reference the property and use your setter in your init. Make your class follow the same rules internally that your users are expected to follow, at least when __init__
is taking input like this.
class Foo(object):
__slots__ = ['_a']
def __init__(self, a):
self.a = a
@property
def a(self):
return self._a
@a.setter
def a(self, x):
if len(x) == 10:
self._a = x
else:
raise ValueError('incorrect length!')
@a.deleter
def a(self):
self._a = ''
This way, if someone instantiates with an "incorrect length" they'll hit the same error path as setting object values.
You absolutely don't need the static method, that's a major use case of using setters in the first place, to validate or sanitize the data before accepting it! A reason to use a method like that is if you have multiple properties that all need the same scrubbing. e.g. Foo.b, Foo.c, Foo.d all work like Foo.a. In which case, you'd also want to indicate that it's private with a leading underscore: def _validate_nums(x):
For your new code, it's exactly the same as the old, no need for a static method:
class Foo(object):
__slots__ = ('_a',)
def __init__(self, x):
self.a = x
@property
def a(self):
return str(self._a)
@a.setter
def a(self, x):
if x % 2 == 0:
self._a = x
else:
raise ValueError(':(')
@a.deleter
def a(self):
self._a = 0
A reason you might break from this is if you want an explicit undefined or default value, that your class normally prohibits (such as your original class prohibits any length that's not 10). In which case you could do:
def __init__(self, a="four"):
if a == "four":
self._a = "four"
else:
self.a = a
This example is a little weird, you'd probably use None
, 0
, an empty value (like []
or ()
), or something else to test against elsewhere, like you do with your newer code, which doesn't need this.
It feels a little hacky to only put 'dummy' var names in slots (i.e., Foo._a only exists for the benefit of the Foo.a property), but it works.
I think you are looking at it backwards, but it makes sense since without directors we all would prefer to use Foo.a
over Foo._a
. However, Foo._a
is your class attribute. It's what your class uses. It's the property decorators that allow Foo.a
that are a little bit hacky. They are defensive measures to protect your class from misuse or bad behavior. They aren't your class attribute at all.
By also using _a
instead of like internal_data
you communicate to other users: _a
is meant to be private, access it directly at your own risk, use the property decorators if you want expected behavior!
Upvotes: 0
Reputation: 25833
You don't need to list properties in __slots__
only attributes, if you change slots to __slots__ = ['_a']
, your class will work as expected.
Sorry I didn't see you add-on question in my first read through. Having a static validate method is fine, but you don't need to call it explicitly in the in __init__
, instead set self.a
directly, only use self._a
when you want to intentionally bypass the setter/getter.
I don't know what you mean by,
It feels a little hacky to only put 'dummy' var names in
__slots__
a
is a property of the class (which btw is also implemented using descriptors) so if you also include it in __slots__
you're instructing the class to create two conflicting descriptors for the same thing.
_a
is an attribute of the class so if you're using __slots__
it must be there. Just because _a
starts with an underscore doesn't make it any better or worse than any other attribute, I would call it a "protected" attribute instead of a dummy attribute :). Are you sure you want to use __slots__
at all, that seems to be the most "hacky" part of this whole thing.
Upvotes: 0
Reputation: 281843
Your code almost works as is; the only changes you need to make are
__slots__ = ['_a'] # _a instead of a
def __init__(self, a):
self.a = a # a instead of _a
If for some reason you really want to avoid the separate _a
slot and a
wrapper, you can replace the default slot descriptor with your own descriptor, but I wouldn't recommend it. Also, the replacement has to happen after the class is created:
class Foo(object):
__slots__ = ['a']
underlying_descriptor = Foo.a
@property
def a(self):
return underlying_descriptor.__get__(self, Foo).title()
@a.setter
def a(self, x):
if len(x) == 10:
underlying_descriptor.__set__(self, x)
else:
raise ValueError('incorrect length!')
@a.deleter
def a(self):
underlying_descriptor.__del__(self)
Foo.a = a
You might try to simplify all that underlying_descriptor
stuff by setting Foo._a = Foo.a
and accessing it through self._a
, but then you have an _a
attribute on your objects, and the whole point of this version is to avoid the second attribute.
Upvotes: 1