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开发者_StackOverflow.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!
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.
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
.
To answer your immediate question, you call a function or method retrieved using getattr()
the same way you call any function: by putting the arguments, if any, in parentheses following the reference to the function. The fact that the reference to the function comes from getattr()
rather than an attribute access doesn't make any difference.
func = getattr(product, name)
result = func(arg)
These can be combined and the temporary variable func
eliminated:
getattr(product, name)(arg)
In addition to what Cat Plus Plus said, if you really want to invoke magic anyway (please don't! There are unbelievably many disturbing surprises awaiting you with such an approach in practice), you could test for the presence of the attribute in the Product class, and create a sum_with
wrapper dynamically:
def __getattribute__(self, attr):
return (
lambda *args: self.sum_with(attr, *args)
if hasattr(Product, attr)
else super(Package, self).__getattribute__(attr)
)
精彩评论