Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using __getattribute__ or __getattr__ to call methods in Python

I am trying to create a subclass which acts as a list of custom classes. However, I want the list to inherit the methods and attributes of the parent class and return a sum of the quantities of each item. I am attempting to do this using the __getattribute__ method, but I cannot figure out how to pass arguments to callable attributes. The highly simplified code below should explain more clearly.

class Product:
    def __init__(self,price,quantity):
        self.price=price
        self.quantity=quantity
    def get_total_price(self,tax_rate):
        return self.price*self.quantity*(1+tax_rate)

class Package(Product,list):
    def __init__(self,*args):
        list.__init__(self,args)
    def __getattribute__(self,*args):
        name = args[0]
    # the only argument passed is the name...
        if name in dir(self[0]):
            tot = 0
            for product in self:
                tot += getattr(product,name)#(need some way to pass the argument)
            return sum
        else:
            list.__getattribute__(self,*args)

p1 = Product(2,4)
p2 = Product(1,6)

print p1.get_total_price(0.1) # returns 8.8
print p2.get_total_price(0.1) # returns 6.6

pkg = Package(p1,p2)
print pkg.get_total_price(0.1) #desired output is 15.4.

In reality I have many methods of the parent class which must be callable. I realize that I could manually override each one for the list-like subclass, but I would like to avoid that since more methods may be added to the parent class in the future and I would like a dynamic system. Any advice or suggestions is appreciated. Thanks!

like image 274
AJ Medford Avatar asked Aug 30 '11 18:08

AJ Medford


2 Answers

This code is awful and really not Pythonic at all. There's no way for you to pass extra argument in the __getattribute__, so you shouldn't try to do any implicit magic like this. It would be better written like this:

class Product(object):
    def __init__(self, price, quantity):
        self.price    = price
        self.quantity = quantity

    def get_total_price(self, tax_rate):
        return self.price * self.quantity * (1 + tax_rate)

class Package(object):
    def __init__(self, *products):
        self.products = products

    def get_total_price(self, tax_rate):
        return sum(P.get_total_price(tax_rate) for P in self.products)

If you need, you can make the wrapper more generic, like

class Package(object):
    def __init__(self, *products):
        self.products = products

    def sum_with(self, method, *args):
        return sum(getattr(P, method)(*args) for P in self.products)

    def get_total_price(self, tax_rate):
        return self.sum_with('get_total_price', tax_rate)

    def another_method(self, foo, bar):
        return self.sum_with('another_method', foo, bar)

    # or just use sum_with directly

Explicit is better than implicit. Also composition is usually better than inheritance.

like image 124
Cat Plus Plus Avatar answered Oct 31 '22 20:10

Cat Plus Plus


You have a few points of confusion here:

1) __getattribute__ intercepts all attribute access, which isn't what you want. You only want your code to step in if a real attribute doesn't exist, so you want __getattr__.

2) Your __getattribute__ is calling the method on the list elements, but it shouldn't be doing real work, it should only return a callable thing. Remember, in Python, x.m(a) is really two steps: first, get x.m, then call that thing with an argument of a. Your function should only be doing the first step, not both steps.

3) I'm surprised that all the methods you need to override should be summed. Are there really that many methods, that really all should be summed, to make this worthwhile?

This code works to do what you want, but you might want to consider more explicit approaches, as others suggest:

class Product:
    def __init__(self,price,quantity):
        self.price = price
        self.quantity = quantity

    def get_total_price(self,tax_rate):
        return self.price*self.quantity*(1+tax_rate)

class Package(list):
    def __init__(self,*args):
        list.__init__(self,args)

    def __getattr__(self,name):
        if hasattr(self[0], name):
            def fn(*args):
                tot = 0
                for product in self:
                    tot += getattr(product,name)(*args)
                return tot
            return fn
        else:
            raise AttributeError

Things to note in this code: I've made Package not derive from Product, because all of its Product-ness it gets from delegation to the elements of the list. Don't use in dir() to decide if a thing has an attribute, use hasattr.

like image 39
Ned Batchelder Avatar answered Oct 31 '22 18:10

Ned Batchelder