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.
精彩评论