Использование __getattribute__ или __getattr__ для вызова методов в Python

Я пытаюсь создать подкласс, который действует как список пользовательских классов. Однако я хочу, чтобы список наследовал методы и атрибуты родительского класса и возвращал сумму количеств каждого элемента. Я пытаюсь сделать это с помощью метода __getattribute__, но я не могу понять, как передать аргументы вызываемым атрибутам. Более упрощенный код ниже должен объяснять более четко.

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.

В действительности у меня есть много методов родительского класса, которые должны быть вызываемыми. Я понимаю, что я могу вручную переопределить каждый из них для подкласса, подобранного списком, но я хотел бы избежать этого, поскольку в будущем в родительский класс может быть добавлено больше методов, и мне бы хотелось создать динамическую систему. Любые советы или предложения приветствуются. Спасибо!

Ответ 1

Этот код ужасен и действительно не Pythonic вообще. Нет способа передать лишний аргумент в __getattribute__, поэтому вам не следует пытаться делать какую-либо неявную магию, как это. Лучше было бы написать следующее:

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)

Если вам нужно, вы можете сделать оболочку более общей, например

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

Явный лучше, чем неявный. Также состав обычно лучше, чем наследование.

Ответ 2

У вас здесь несколько путаниц:

1) __getattribute__ перехватывает весь доступ к атрибуту, который не является тем, что вы хотите. Вы хотите, чтобы ваш код включался, если реальный атрибут не существует, поэтому вы хотите __getattr__.

2) Ваш __getattribute__ вызывает метод в элементах списка, но он не должен делать реальной работы, он должен возвращать только вызываемую вещь. Помните, что в Python x.m(a) действительно два шага: во-первых, получите x.m, затем вызовите эту вещь с аргументом a. Ваша функция должна делать только первый шаг, а не оба шага.

3) Я удивлен, что все методы, которые вам нужно переопределить, должны быть суммированы. Есть ли действительно много методов, которые действительно должны быть суммированы, чтобы сделать это стоящим?

Этот код работает, чтобы делать то, что вы хотите, но вы можете рассмотреть более явные подходы, как предлагают другие:

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

Что следует отметить в этом коде: я сделал Package не из Product, потому что все его Product-ness получает от делегирования к элементам списка. Не используйте in dir(), чтобы решить, имеет ли вещь атрибут, используйте hasattr.

Ответ 3

Чтобы ответить на ваш непосредственный вопрос, вы вызываете функцию или метод, полученные с помощью getattr() так же, как вы вызываете какую-либо функцию: путем помещения аргументов, если они есть, в круглые скобки после ссылки на функцию. Тот факт, что ссылка на функцию исходит от getattr(), а не на доступ к атрибуту, не имеет никакого значения.

func   = getattr(product, name)
result = func(arg)

Они могут быть объединены и временная переменная func устранена:

getattr(product, name)(arg)

Ответ 4

В дополнение к тому, что сказал Cat Plus Plus, если вы действительно хотите вызвать магию в любом случае (пожалуйста, не делайте!). На практике вас ждут невероятные сюрпризы с таким подходом на практике), вы можете проверить наличие атрибут в классе Product и динамически создать обертку sum_with:

def __getattribute__(self, attr):
  return (
    lambda *args: self.sum_with(attr, *args) 
    if hasattr(Product, attr)
    else super(Package, self).__getattribute__(attr)
  )