Alexis Drakopoulos
Alexis Drakopoulos

Reputation: 1145

Pythonic way to enforce order in which methods must be called

Let's say I have the following class:

class House:

    def __init__(self, estimated_cost):
        # let's say states are planning, building, built and sold
        self.state = "planning"
        self.estimated_cost = estimated_cost

    def hire_contractors():
        # HIRE SOME CONTRACTORS
        if self.state not in ("planning", "contractors_hired"):
            raise ValueError("We cannot hire contracts if the house is not in planning or building phase!")
        self.state = "contractors_hired"

    def build_house():
        # do stuff to build the house, NEED CONTRACTORS
        if self.state != "contractors_hired":
            raise ValueError("We cannot build a house without having contractors!")
        self.state = "built"

    def sell_house():
        # sell house for 10% more than cost estimate
        if self.state != "built":
            raise ValueError("Need to be built before selling!")
        print("Selling house for," self.estimated_cost*1.1)

where the following rules are enforced:

Otherwise errors are raised. Apologies if this is confusing I can add an example with a, b, c later. The current method "works" but seems unpythonic. Are there any better ways of having a state that allows certain methods to be called? This only needs to be enforced through error raising and/or warnings.

EDIT: I realize things are confusing, in reality I'm building an ML model which has something similar to:

class Model:

    def set_hyperparams():
        # Can be done always

    def train():
        # Can only be done once set_hyperparams

    def predict():
        # Can only be done once trained

Upvotes: 0

Views: 121

Answers (2)

Lcj
Lcj

Reputation: 451

You could use decorators to make your code prettier. The requireState decorator takes a list of required states and checks if one of them is satisfied.

def requiresState(*states):
    def decorator(func):
        def wrapper(self, *args, **kwargs):
            if not self.state in states:
                raise ValueError("Requirements not satisfied")
            return func(self, *args, **kwargs)
        return wrapper
    return decorator    

class House:
    def __init__(self, estimated_cost):
        # let's say states are planning, building, built and sold
        self.state = "planning"
        self.estimated_cost = estimated_cost

    @requiresState("planning", "contractors_hired")
    def hire_contractors(self):
        # HIRE SOME CONTRACTORS
        self.state = "contractors_hired"
    
    @requiresState("contractors_hired")
    def build_house(self):
        # do stuff to build the house, NEED CONTRACTORS
        self.state = "built"
    
    @requiresState("built")
    def sell_house(self):
        # sell house for 10% more than cost estimate
        print("Selling house for,", self.estimated_cost*1.1)

Upvotes: 2

ltrojan
ltrojan

Reputation: 45

There doesn't seem to be anything un-pythonic in what you've got there...

The only design choice I'd recommend is to avoid using an enum state member that will inevitably fail to represent all possible states a house can be in... but instead use conditions on members of the class:

class House():

    def __init__(self, funds=0, contractors=0):
        self.contractors = contractors
        self.funds = funds

    def add_funds(self, funds):
        self.funds += funds

    def hire_contractor(self, cost):
        if self.funds < cost:
            print(f"{self.funds} not sufficient; funds needed: {cost}")
            return
        self.funds -= cost
        self.contractors += 1

    def hire_contractors(self, *costs):
        for cost in costs:
            self.hire_contractor(cost)

    def build(self, required_contractors=1):
        if self.contractors < required_contractors:
            print(f"please hire more contractors ({self.contractors} available)")
            return
        self.contractors -= required_contractors

Upvotes: 0

Related Questions