Search code examples
pythonlambdapyqt5qaction

Store and evaluate a condition in an instance variable


I start my question by describing the use case:

A context-menu should be populated with actions. Depending on the item for which the menu is requested, some actions should be hidden because they are not allowed for that specific item.

So my idea is to create actions like this:

edit_action = Action("Edit Item")
edit_action.set_condition(lambda item: item.editable)

Then, when the context-menu is about to be opened, I evaluate every possible action whether it is allowed for the specific item or not:

allowed_actions = list(filter(
    lambda action: action.allowed_for_item(item),
    list_of_all_actions
))

For that plan to work, I have to store the condition in the specific Action instance so that it can be evaluated later. Obviously, the condition will be different for every instance.

The basic idea is that the one who defines the actions also defines the conditions under which they are allowed or not. I want to use the same way to enable/disable toolbar buttons depending on the item selected.

So that is how I tried to implement Action (leaving out unrelated parts):

class Action:
    _condition = lambda i: True
    def set_condition(self, cond):
        self._condition = cond

    def allowed_for_item(self, item):
        return self._condition(item)

My problem is now:

TypeError('<lambda>() takes 1 positional argument but 2 were given')

Python treats self._condition(item) as call of an instance method and passes self as the first argument.

Any ideas how I can make that call work? Or is the whole construct too complicated and there is a simpler way that I just don't see? Thanks in advance!


Update: I included the initializer for _condition, which I found (thanks @slothrop) to be the problem. This was meant as default value, so allowed_for_item() also works when set_condition() has not been called before.


Solution

  • Setting the class attribute _condition to a function (whether through a lambda or a def) makes that function into a method: i.e. when accessed as an instance attribute, the instance is inserted as the first argument to the function call.

    So this:

    class Action:
        _condition = lambda i: True
    

    does the same as this:

    class Action:
        def _condition(i):
            return True
    

    While these two are equivalent, the def version is more familiar, so the problem with it (lack of self in the signature) is more obvious.

    The underlying mechanism for this is summarised on the Python wiki:

    The descriptor protocol specifies that during an attribute lookup, if a name resolves to a class attribute and this attribute has a __get__ method, then this __get__ method is called. The argument list to this call includes either:

    the instance and the class itself, or

    None and the class itself

    Possible solutions are:

    1. Set the default as an instance attribute (the solution you arrived at)

    2. Add the extra parameter to the lambda, so _condition = lambda _self, _i: True

    3. Make the method static: _condition = staticmethod(lambda _i: True)