开发者

What would be the most pythonic way to make an attribute that can be used in a lambda?

More specifically, I want to be able to support lambda: <some_or_other_setter>, but I want to keep the code clear and to a concise. I have to validate the value, so I need a setter of some kind. I need to use lambda because I need to pass callbacks to Tkinter events. I also need to be able to modify the value of the attribute outside a binding.

In my following examples, assume that a button widget called spam_button has been declared globally. Also asume that the class Eggs will have at least 10 attributes than need to all be accessed the same way (I like consistency).

The first possible way I could do this is using just getters and setters:

class Eggs(object):

    def __init__(self):
        self._spam = ''
        self.set_spam('Monster')
        print self.get_spam()
        spam_button.bind('<Enter>', lambda: self.set_spam('Ouch'))

    def set_spam(self, spam):
        if len(spam) <= 32:
            self._spam = spam
开发者_StackOverflow中文版
    def get_spam(self):
        return self._spam

But if I use this method and have lots of attributes, the code may get too long to manage.

The second way I could do this is use properties, and use the setter in the callback:

class Eggs(object):

    def __init__(self):
        self._spam = ''
        self.spam = 'Monster'
        print self.spam
        spam_button.bind('<Enter>', lambda: self.set_spam('Ouch'))

    def set_spam(self, spam):
        if len(spam) <= 32:
            self._spam = spam

    def get_spam(self):
        return self._spam

    spam = property(get_spam, set_spam)

This way is a bit simpler than the first when accessing the attribute directly, but is inconsistent when using lambda.

The third way to do this would be to make an additional class with get and set methods:

class Spam(object):

    def __init__(self):
        self._value = ''

    def set(self, value):
        if len(spam) <= 32:
            self._value = value

    def get(self):
        return self._value


class Eggs(object):

    def __init__(self):
        self._spam = ''
        self.spam = Spam()
        self.spam.set('Monster')
        print self.spam.get()
        spam_button.bind('<Enter>', lambda: self.spam.set('Ouch'))

This method is more organised than the first, but I will need to make a class for each type of validation.

The last way I might do it is use methods instead of properties (properties were the second example):

class Eggs(object):

    def __init__(self):
        self._spam = ''
        self.spam('Monster')
        print self.spam()
        spam_button.bind('<Enter>', lambda: self.spam('Ouch'))

    def spam(self, spam=None):
        if spam != None: 
            if len(spam) <= 32:
                self._spam = spam
        else:
            return self._spam

This method will probably be the shortest, but it is harder at a glance to tell whether I am getting or setting.

Which (if any) of these methods are preferred?


Use a closure to return a function to be used as a callback, and define each condition as a function (using lambda or def, your preference) instead of each setter as a function.

class Eggs(object):

    def __init__(self):
        self.spam = 'Monster'
        def spam_condition(string = None):
            return (string is not None) and (len(string) <= 32)
        self.spam_setter = self.set('spam', 'Ouch', spam_condition)
        spam_button.bind('<Enter>', self.spam_setter)
        self.spam_setter(val='Horse')


    def set(self, name, value, condition):
        def setter(val):
            if type(val) is not .... :  # fill this in with the TYPE of the event
                value = val
            if condition(value):
                setattr(self, name, value)
        return setter

Edit: Now you can call the setter, which will check the condition, from anywhere.

Edit 2: This way, the setter will check if it got an event, and if not, use the value it's passed. You could also use:

if not isinstance(val, ....) :  # fill this in with the CLASS of the event


I still suggest using a dict and calling it a day. Subclass it and override __setitem__ to do your data validation, then don't worry about getters:

#!/usr/bin/env python

class Egg(dict):
    def __init__(self, *args, **kwargs):
        super(Egg, self).__init__(*args, **kwargs)
        self['spam'] = 'foo'
        spam_button.bind('<Enter>', lambda: self.__setitem__('spam', 'Ouch'))

    def __setitem__(self, key, value):
        if key == 'spam':
            if len(value) > 32:
                raise ValueError('"%s" is longer than 32 characters')
            return super(Egg, self).__setitem__(key, value)
        raise KeyError(key)


The validation issue can be handled using a property:

class Egg(object):
    @property
    def spam(self):
        return self._spam

    @spam.setter
    def spam(self, value):    
        if len(value) <= 32:
            self._spam = value

Obviously, you can still use self._spam = 'spam '*10+'baked beans and spam' with impunity.

Use the builtin setattr:

lambda: setattr(self, 'spam', 'Ouch')

If you object to ..."spam"... and prefer just ...spam..., you can use methods of property:

lambda: self.__class__.spam.fset(self, 'Ouch')

or, since property is a descriptor:

lambda: type(self).spam.__set__(self, 'Ouch')

But the first version is preferred, I hope for obvious reasons.


I like the class-based approach. You'll probably have a limited set of validation that you want to do (for example maximum length of a string, valid range for a number, etc.), so you'll have a limited number of classes.

For example:

class LimitedLengthString(object):
    def __init__(self, max_length):
        self.max_length = max_length
    def set(self, value):
        if len(value) <= self.max_length:
            self.value = value
    def get(self):
        return value

class Eggs(object):
    def __init__(self):
        self.spam = LimitedLengthString(32)
        self.spam.set('Monster')
        print self.spam.get()
        spam_button.bind('<Enter>', lambda: self.spam.set('Ouch'))

If you really have unique validations that need to be done to the different attributes, you can't get away from writing a lot of code. But as always in programming, generalize when you start repeating yourself.


Update after suggestion by TokenMacGuy: Alternative using the fairly unknown Python feature "descriptors":

class LimitedLengthString(object):
    def __init__(self, name, max_length):
        self.name = name
        self.max_length = max_length

    def __set__(self, instance, value):
        if len(value) <= self.max_length:
            instance.__dict__[self.name] = value

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

class Eggs(object):
    spam = LimitedLengthString('spam', 32)
    def __init__(self):
        self.spam = 'Monster'
        print self.spam  # prints 'Monster'
        self.spam = 'x' * 40
        print self.spam  # still 'Monster'
        spam_button.bind('<Enter>', lambda: self.spam = 'Ouch')

I found a pretty good introduction to descriptors.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜