swizzard
swizzard

Reputation: 1095

python __slots__ descriptor getter/setter

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

Answers (3)

Zim
Zim

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

Bi Rico
Bi Rico

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.

Update

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

user2357112
user2357112

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

Related Questions