David542
David542

Reputation: 110257

Combining checks at the beginning of a method

I have several methods that are similar to the following:

@property
def tpr_num_days(self):
    """
    The duration, in days, of the TPR interval.
    """
    if not self.prices:
        raise RuntimeError("Must initialize self.prices first.")

    # do something

@property
def revenue_during_tpr_is_greatest(self):
    """
    Tells us if the Revenue that was generated during the TPR was both greater
    than the revenue that was generated in the same-duration interval before the TPR and after the TPR.
    """
    if not self.prices:
        raise RuntimeError("Must initialize self.prices first.")

    # do something

def build_pricing_data(self):
    """
    This will build a list of pricing objects.

    Note that we are currently excluding "returns" because the price of the returned
    item may be different than the current price of the product, which could open up
    a can of worms trying to track down when that item was purchased.
    """
    cursor.execute('''SELECT
                            date,
                            max(if(offer_code='sdbuy', price, null)) sd_buy_price,
                            max(if(offer_code='hdbuy', price, null)) hd_buy_price,
                            max(if(offer_code='sdrent', price, null)) sd_rent_price,
                            max(if(offer_code='hdrent', price, null)) hd_rent_price,
                            sum(revenue) revenue
                      FROM price_date WHERE apple_id=%s and territory=%s and revenue > 0
                      GROUP BY date
                      ORDER BY date
                      ''', (self.apple_id, self.territory))

    # let's make sure it's re-initialized in case it's called multiple times.
    self.prices = []     
    for row in cursor:
        pricing_obj = {"date": item[0], "SDBUY": item[1], "HDBUY": item[2], "SDRENT": item[3], "HDRENT": item[4], "REVENUE": item[5]}
        self.prices.append(pricing_obj)

Instead of having this if statement at the start of dozens of methods, what would be a better way to encapsulate that?

Upvotes: 2

Views: 56

Answers (2)

Aran-Fey
Aran-Fey

Reputation: 43196

You could use a function decorator:

import functools

def requires_price(func):
    @functools.wraps(func)
    def wrapper(obj, *args, **kwargs):
        if not obj.prices:
            raise RuntimeError("Must initialize self.prices first.")

        return func(obj, *args, **kwargs)

    return wrapper

Which would be used like this:

@property
@requires_price
def tpr_num_days(self):
    ... # do something

Or you could go a step further and implement the decorator as a descriptor, which would let you omit the @property:

class price_property:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self

        if not instance.prices:
            raise RuntimeError("Must initialize self.prices first.")

        return self.func(instance)

Which is used like this:

@price_property
def tpr_num_days(self):
    ... # do something

Upvotes: 2

Barmar
Barmar

Reputation: 781096

You can add a method to do the check.

def validate_prices(self):
    if not self.prices:
        raise RuntimeError("Must initialize self.prices first.")

and then put self.validate_prices() at the beginning of each function.

It only saves one line, but you don't have to repeat the same error message every time.

If you want something more automatic, you'd probably have to define a metaclass that adds this code to every property method.

Upvotes: 2

Related Questions