2016-05-10 7 views
2

Работа в Python, как метод, полученный метаклассом (метаметодом), извлекается через класс, который он создавал? В следующем сценарии, это легко - просто использовать getattr или точечную нотацию:Как получить метод метакласса

* Все примеры используют версию безопасного with_metaclass

class A(type): 
     class A(type): 
    """a metaclass""" 

    def method(cls): 
     m = "this is a metamethod of '%s'" 
     print(m % cls.__name__) 

class B(with_metaclass(A, object)): 
    """a class""" 
    pass 

B.method() 
# prints: "this is a metamethod of 'B'" 

Однако это свойственно так 'method' не может быть найден в любом месте в dir(B) , В результате этого факта, перекрывая такой метод динамически становится затруднительным, поскольку метакласс не в цепочку поиска для super:

class A(type): 
    """a metaclass""" 

    def method(cls): 
     m = "this is a metamethod of '%s'" 
     print(m % cls.__name__) 

class B(with_metaclass(A, object)): 
    """a class""" 

    @classmethod 
    def method(cls): 
     super(B, cls).method() 

B.method() 

# raises: "AttributeError: 'super' object has no attribute 'method'" 

Итак, что это самый простой способ правильно переопределить метаметод? Я придумал свой собственный ответ на этот вопрос, но с нетерпением жду любых предложений или альтернативных ответов. Заранее благодарим за ваши ответы.

ответ

4

Как вы выразились, и обнаружил на практике A.method является не на цепочке поиска Б - отношение классов и метаклассов не один наследование - это один из «экземпляров» как класс является экземпляром метакласса.

Python - прекрасный язык, на котором он ведет себя в ожидании, с очень небольшим количеством сюрпризов, - и в этих обстоятельствах это одно и то же: если бы мы имели дело с «обычными» объектами, ваша ситуация была бы такой же, как и примерB со значением A. И B.method будет присутствовать в B.__dict__ - вызов «супер», помещенный в метод, определенный для такого экземпляра, никогда не сможет добраться до A.method - это фактически приведет к ошибке. Поскольку B - объект класса, super внутри метода в B's __dict__ имеет смысл - но он будет искать цепочку классов B __mro__ (в данном случае (object,)) - и это то, что вы нажмете.

Такое положение не должно происходить часто, и я даже не думаю, что это должно произойти вообще; семантически очень сложно найти какой-либо смысл в методе, который имел бы смысл как метаклассовый метод, так и как метод самого класса. Более того, если method не переопределено в B, обратите внимание, что оно не будет даже видимым (и не вызываемым) из экземпляров B.

Возможно, ваш дизайн должен:

a. есть BaseClass Base, используя свой метакласса A, который определяет method вместо того, чтобы он определил в A - а затем определить class B(Base): вместо

б.Или метаклассом A достаточно впрыснуть method в каждом class он создает, с кодом для этого в его __init__ или __new__ метод - вместе:

def method(cls): 
    m = "this is an injected method of '%s'" 
    print(m % cls.__name__) 

class A(type): 
    def __init__(cls, name, bases, dct): 
     dct['method'] = classmethod(method) 

Это будет мой предпочтительный подход - но это не позволяет один переопределить этот method в классе, который использует этот метакласс - без какой-либо дополнительной логики там, вышеприведенный подход скорее переопределит любой такой method явный в теле. Чем проще вещь, чтобы иметь базовый класс Base, как указано выше, или придать метод с другим именем, как base_method на выпускном классе, и жёстко призывает к нему в любой наиважнейшей method:

class B(metaclass=A): 
    @classmethod 
    def method(cls): 
     cls.base_method() 
     ... 

(Использование дополнительный if на метаклассом-х __init__ так, что по умолчанию method является псевдонимом base_method)

что вы буквально просили здесь начинается

Теперь, если у вас действительно есть прецедент для методов в метаклассе, который должен быть вызван из класса, «один и очевидный» способ состоит в простом кодировании вызова, как это было сделано до существования super

Вы можете сделать что-либо:

class B(metaclass=A): 
    @classmethod 
    def method(cls): 
     A.method(cls) 
     ... 

Который менее динамичен, но меньше магии и более удобным для чтения - или:

class B(metaclass=A): 
    @classmethod 
    def method(cls): 
     cls.__class__.method(cls) 
     ... 

Что является более динамичным (атрибут __class__ работает cls только л икэ она будет работать в случае B был просто какой-то экземпляр A как мой пример во втором абзаце: B.__class__ is A)

В обоих случаях, вы можете защитить себя от вызова в метакласса несуществующий method с простым if hasattr(cls.__class__, "method"): ...

+0

Это было скорее интеллектуальное преследование, чем практическое. По сути, у меня есть вариант использования, когда я вызываю метод 'setup_class' в' __init__' метакласса. Существует базовая функциональность для 'setup_class', поэтому было бы совершенно неинтуитивно, чтобы это было определено в метаклассе, но после того, как я столкнулся с этой проблемой, я понял, что ее можно так же легко определить в базовом классе. Я скоро отправлю свою попытку решения. Спасибо за ясный ответ – rmorshea

+1

Если у вас есть класс setup_class, который имеет смысл в метаклассе и что может дополнять это, что имеет смысл в классе, вы можете документировать стандарт именования или декоратор, чтобы иметь методы, называемые самим классом путем выполнения самого setup_class. (однако есть трюк - метод класса, который вызывается перед возвратом метакласса '__init__', не может использовать' super' - см. http://stackoverflow.com/questions/13126727/how-is-super-in-python- 3-реализован/28605694 # 28605694 – jsbueno

1

Переопределение method не представляет проблемы; используя super правильно есть. method не наследуется от какого-либо базового класса, и вы не можете гарантировать, что каждый класс в MRO использует тот же метакласс для предоставления метода, поэтому вы запускаете ту же проблему, что и любой метод без корневого класса для обработки, не вызывая super ,

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

0

Это моя первая попытка решения:

def metamethod(cls, name): 
    """Get a method owned by a metaclass 

    The metamethod is retrieved from the first metaclass that is found 
    to have instantiated the given class, or one of its parent classes 
    provided the method doesn't exist in the instantiated lookup chain. 

    Parameters 
    ---------- 
    cls: class 
     The class whose mro will be searched for the metamethod 
    """ 
    for c in cls.mro()[1:]: 
     if name not in c.__dict__ and hasattr(c, name): 
      found = getattr(c, name) 
      if isinstance(found, types.MethodType): 
       new = classmethod(found.__func__) 
       return new.__get__(None, cls) 
    else: 
     m = "No metaclass attribute '%s' found" 
     raise AttributeError(m % name) 

Однако, это приходит с уловом - он не работает с six.with_metaclass. Обходной путь состоит в том, чтобы воссоздать его и использовать его, чтобы создать новую базу в mro классов, которые наследуются от нее. Поскольку эта база не является временной, реализация на самом деле довольно проста:

def with_metaclass(meta, *bases): 
    """Create a base class with a metaclass.""" 
    return meta("BaseWithMeta", bases, {}) 

низким и вот, этот подход не решает случай я представил:

class A(type): 
    """a metaclass""" 

    def method(cls): 
     m = "this is a metamethod of '%s'" 
     print(m % cls.__name__) 

class B(six.with_metaclass(A, object)): 
    """a class""" 

    @classmethod 
    def method(cls): 
     metamethod(cls, 'method')() 

B.method() 
# prints: "this is a metamethod of 'B'" 

Однако, это не получится, и не может таким же образом, и по тем же причинам, что это делает:

class A(type): 
    """a metaclass""" 

    def method(cls): 
     m = "this is a metamethod of '%s'" 
     print(m % cls.__name__) 

class C(object): 
    """a class""" 

    @classmethod 
    def method(cls): 
     m = "this is a classmethod of '%s'" 
     print(m % cls.__name__) 

class B(six.with_metaclass(A, C)): 
    """a class""" 
    pass 

B.method() 
# prints: "this is a classmethod of 'B'" 
+0

Повторная реализация 'with_metaclass' не переносит' метод' из метакласса в базовый класс, который он возвращает, - он по существу тот же, что и чистый Python (или original with_metaclass), в котором существует 'method' в 'B .__ class__', но не в B. Увеличьте его, чтобы скопировать нужные метатоды из метакласса в динамический базовый класс, прежде чем возвращать его (таким образом, чтобы' метод' существовал в обычном поиске mro). Но они, вы может сделать этот метод инъекции на самом метаклассе (это то, что метаклассы предназначены для инъекций и изменения материала во время создания класса). – jsbueno

+0

Инъекция метода имеет смысл, но по какой-то причине я никогда не исследовал атрибут '__class__' класса. Я был под впечатлением t вы потеряли доступ к метаклассу. В этом случае вы можете просто искать 'B .__ class__' для' method', а не итерации по mro, как сейчас. Думаю, лучший способ рассказать о моем вопросе - это спросить, как вы можете получить метакласс класса. – rmorshea

0

Я думаю, что это наиболее близко рассматривает исходную задачу извлечения метаметод.

def metamethod(cls, name): 
    """Get an unbound method owned by a metaclass 

    The method is retrieved from the instantiating metaclass. 

    Parameters 
    ---------- 
    cls: class 
     The class whose instantiating metaclass will be searched. 
    """ 
    if hasattr(cls.__class__, name): 
     return getattr(cls.__class__, name) 

Однако, это не реально решить проблему должным образом перекрывая метод, который вызывается перед классом полностью инстанцирован, как @jsbueno explained в ответ на мой конкретный случай использования.