Piyush Deshmukh
Piyush Deshmukh

Reputation: 529

Enforce a sequence for calling methods in a class

Lets say we want to implement a functionality using a class

class MyClass():
    def __init__(self):
        pass

    def generate_query(self):
        # Generate the query
        pass

    def send_query(self):
        # send the query over the network
        pass

    def receive_response(self):
        # read the response from buffer
        pass

What's the best way to make sure that generate_query() has been called before send_query(), ofcourse vice versa doesn't make sense. It's important because simply mentioning in the API documentation to call another method before you've called send_query() is another thing, but checking it explicitly in the code in send_query() that generate_query() has been called before is a good practice IMO.

I am expecting a solution like if generate_query() has not been called, we raise an exception or so.

There was a nice pythonic way to do this, I had read somewhere, but I forgot the source and the solution both.

Help is appreciated!

Upvotes: 1

Views: 1247

Answers (3)

TakingItCasual
TakingItCasual

Reputation: 811

I've come up with a base class you can inherit from to enforce method call order (tested in Python 2.7 and 3.6):

from types import FunctionType, MethodType

class Sequenced(object):

    def __init__(self):
        _methods = [name for name, obj in self.__class__.__dict__.items()
            if type(obj) == FunctionType]
        assert set(self._method_sequence).issubset(set(_methods))
        self._sequence_pos = 0

    def __getattribute__(self, name):
        attr = object.__getattribute__(self, name)
        if type(attr) == MethodType:
            if attr.__name__ in self._method_sequence:
                if self._sequence_pos >= len(self._method_sequence):
                    raise RuntimeError("All sequenced methods already called.")
                if attr.__name__ != self._method_sequence[self._sequence_pos]:
                    raise RuntimeError("{0} method call expected.".format(
                        self._method_sequence[self._sequence_pos]))
                self._sequence_pos += 1
            def func_wrapper(*args, **kwargs):
                return attr(*args, **kwargs)
            return func_wrapper
        else:
            return attr

Be warned, I don't fully understand how this works (I've managed to stop __getattribute__ from causing infinite recursion, but I don't understand what caused it in the first place, and I don't understand why I have to use FunctionType in one place and MethodType in another). It passed my minimal testing (Python 2.7 and 3.6), but you'll want to make sure you test it as well.

All you need to do with your class is make it inherit from Sequenced, and modify its __init__ method like so:

class MyClass(Sequenced):

    def __init__(self):
        self._method_sequence = [
            "generate_query",
            "send_query",
            "receive_response"
        ]
        super(MyClass, self).__init__()

    def generate_query(self):
        # Generate the query
        pass

    def send_query(self):
        # send the query over the network
        pass

    def receive_response(self):
        # read the response from buffer
        pass

The base class allows your class to contain other methods (sequencing is only enforced on methods within the _method_sequence list). _method_sequence can contain duplicates if you want methods to be called more than once.

In the case of a sequenced method being called out of sequence, or attempting to use a sequenced method after already having gone through the list, RuntimeError is raised.

It's possible to modify the base class so that sequence methods can be called repeatedly once available, but I'll leave that as an exercise to the reader.

Upvotes: 1

OptimusCrime
OptimusCrime

Reputation: 14863

One option could be to use boolean flags. This approach is simple, but not very clean.

class MyClass():
    def __init__(self):
        self.generated = False

    def generate_query(self):
        self.generated = True

        # Generate query

    def send_query(self):
        if not self.generated:
            self.generate_query()

        # Send query

    def receive_response(self):
        pass

I would advice you to try to reconstruct the class and methods. Requiring methods to run in a particular order is prone for errors, especially if it is not documented.

Upvotes: 0

Max
Max

Reputation: 1363

My idea would be to keep a instance variable name flag which will be True,if the generate_query is called, and if flag is false, it means that generate_query is not called and so, you will call the generate_query inside send_query or show a message.

Upvotes: 0

Related Questions