Настройка функций в иерархии классов Python

#python #class #metaclass

#python #класс #метакласс

Вопрос:

У меня есть иерархия классов, определения которых я не могу редактировать напрямую. Я хотел бы обернуть каждый класс в иерархии таким образом, чтобы некоторые функции были настроены.

Чтобы привести упрощенный пример, предположим, у нас есть иерархия животных:

 from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def sound(self):
        pass

class Cat(Animal):
    def sound(self):
        return "Meow"

class Dog(Animal):
    def sound(self):
        return "Woof"

class Cow(Animal):
    def sound(self):
        return "Moo"
  

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

 class LoudDog(Dog):
    def sound(self):
        return super().sound().upper()
  

Но в идеале я должен иметь возможность создавать оболочку, которая позволяет мне создавать громкую версию любого Animal класса, что-то вроде этого:

 LoudDog = Loud(Dog)
LoudCat = Loud(Cat)
LoutCow = Loud(Cow)
  

Какой был бы лучший / самый питонический способ добиться этого? Синтаксис не обязательно должен быть точно таким, как указано выше. Я думал, что здесь могут быть полезны метаклассы, но у меня нет большого опыта работы с ними, и я буду доволен и другими решениями.

Комментарии:

1. Вы могли бы использовать его как mix-in — class LoudDog(Loud, Dog): pass .

Ответ №1:

Вы можете попробовать этот подход, который создаст новый класс на основе заданного и настроит его метод «sound»

 from copy import deepcopy

def loud(animal_class):
    loud_class = deepcopy(animal_class) # to avoid modifying the real class
    old_sound = animal_class.sound # to avoid recursion
    loud_class.sound = lambda self: old_sound(self).upper()
    return loud_class

LoudCow = loud(Cow)
LoudCow().sound() # prints "MOO'
  

Вы также можете попробовать, как было предложено, смешанный:

 class Loud():
  def __init__(self):
    old_sound = self.sound
    self.sound = lambda: old_sound.upper()

class LoudDog(Dog, Loud): pass
LoudDog().sound() # prints "WOOF"
  

Комментарии:

1. Должен ли я заменять подобные функции в решении mix-in или я могу как-то использовать super ?

2. Поскольку Loud класс не имеет «super», вы не можете. Это своего рода хак, поскольку он полагается на self объект, уже имеющий sound метод, если вы попытаетесь использовать его без Animal него, он потерпит неудачу.

Ответ №2:

Проще говоря, если вам не нужны дополнительные классы, просто другое поведение, ваш вызываемый объект может просто привязывать вызываемые объекты, которые будут работать как привязанные методы к экземплярам:

 def bound(instance: Animal):
    original_sound = instance.sound
    def loud(*args):
        new = original_sound()
        return new.upper()
    instance.sound = loud
    # not needed, but can be used to curry various such calls:
    return instance
  

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

 class Loud:
    def sound(self):
         original = super.sound()
         return original.upper()

class LoudCat(Loud, Cat):
   pass
  

В этом случае в новом классе Python сначала найдет метод «sound» в классе Loud и super() выберет следующий класс __mro__ (порядок разрешения метода), который будет другим базовым классом.

Ответ №3:

Поскольку я упоминал в своем посте, что вы могли бы решить эту проблему с помощью метаклассов, вот решение, использующее их. После определения классов в моем исходном сообщении вы можете сделать следующее:

 from abc import ABCMeta

class Loud(ABCMeta):
    def __new__(metacls, name, bases, namespace):
        instance = super().__new__(metacls, name, bases, namespace)
        if not issubclass(instance, Animal):
            raise AttributeError("The base class must derive from Animal.")

        def sound(self):
            return super(instance, self).sound().upper()

        instance.sound = sound
        
        return instance

class LoudCat(Cat, metaclass=Loud):
    pass

class LoudDog(Dog, metaclass=Loud):
    pass

class LoudCow(Cow, metaclass=Loud):
    pass
  

Теперь мы получаем:

 >>> LoudCat().sound()
MEOW
>>> LoudDog().sound()
WOOF
>>> LoudCow().sound()
MOO