Как вызвать super, если родительский метод не может быть определен?

Некоторые классы в стандартной библиотеке Python (и в более широком смысле) используют динамическую диспетчеризацию для вызова специализированных методов в подклассах.

Например, класс ast.NodeVisitor определяет метод visit. Этот метод вызывает visit_classname методов, где это необходимо. Эти методы не определены в самом ast.NodeVisitor, но могут быть предоставлены заинтересованными подклассами.

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

class SpecialNodeVisitor(ast.NodeVisitor):
    def visit_FunctionDef(self, node):
        print(node)  # prints any node of type FunctionDef

Все усложняется, если SpecialNodeVisitor сам является подклассом. super() можно использовать, если visit_FunctionDef переопределено, но не в других случаях, например:

class EvenMoreSpecialNodeVisitor(SpecialNodeVisitor):
    def visit_FunctionDef(self, node):
        super().visit_FunctionDef(node)  # works fine
        # ...

    def visit_Call(self, node):
        super().visit_Call(node)  # AttributeError
        # ...

В частности, второй пример вызывает AttributeError: 'super' object has no attribute 'visit_Call'.


Приведенное выше поведение имеет смысл: родительский класс нет рассматриваемого метода. Однако это вызывает две проблемы:

  • При написании подкласса некоторые динамические методы должны вызывать super(), а некоторые нет. Из-за этого несоответствия действительно легко совершать ошибки.
  • Если позже к родительскому классу будет добавлен новый динамический метод, все подклассы должны быть изменены для вызова super(). Это нарушает действительно фундаментальное правило объектно-ориентированного программирования.

В идеале все методы подкласса должны иметь возможность использовать super(), при этом вызов не будет выполняться, если метод не определен. Есть ли «питоновский» способ добиться этого?

Мне особенно нужно решение, прозрачное для подкласса (например, я не хочу пробовать/за исключением AttributeError в каждом отдельном методе, так как это было бы так же легко забыть, и это чертовски уродливо).

(Стоит отметить, что во многих случаях, и в этом конкретном примере, невозможно просто определить все возможные методы в родительском классе, поскольку это может иметь побочные эффекты.)


person sapi    schedule 24.12.2014    source источник
comment
A) добавить методы без операций в родительский класс B) инкапсулировать подход try-except.   -  person Karoly Horvath    schedule 24.12.2014
comment
То, что вы описываете здесь, это просто нормальное поведение языка. Мне это кажется очень логичным, если в одном из родительских классов нет метода, вызывающего ошибку атрибута в super(). Как можно назвать то, чего нет?! Если вы действительно хотите это сделать, добавьте метод no-op в родительские классы.   -  person andrefsp    schedule 24.12.2014
comment
Чувак, добавлять методы к классам во время выполнения просто неправильно. Почему ты бы так поступил? Это порождает такие проблемы. Я уверен, что этого можно избежать. Может быть, попробовать изменить дизайн вашего приложения.   -  person freakish    schedule 24.12.2014
comment
@andrefsp — стандартное поведение фреймворков, использующих эту парадигму (например, Objective C), заключается в игнорировании недопустимых сообщений. Я понимаю, что Python обычно не использует это, но я не разрабатывал библиотеку ast.   -  person sapi    schedule 24.12.2014
comment
@freakish - в приведенных выше примерах методы не добавляются к классам во время выполнения.   -  person sapi    schedule 24.12.2014
comment
@KarolyHorvath - Инкапсуляция была моей первой мыслью, но поскольку super() в Python 3 является «волшебным» (т. Е. Полагается на ячейку __class__), я даже не уверен, как заставить это работать хорошо. Вы знаете (не хакерский) способ?   -  person sapi    schedule 24.12.2014
comment
@sapi Я имею в виду это предложение: If a new dynamic method is later added to the parent class. Если вы не имели в виду добавление методов во время выполнения, то вы говорите нам, что в какой-то момент вы не будете знать код, с которым работаете? Да, иногда вам приходится рефакторить много кода, когда вы добавляете метод в родительский класс. Но скрывать логику за try: except: просто неправильно, и позже это обернется для вас неприятными последствиями. Это плохой дизайн.   -  person freakish    schedule 24.12.2014
comment
@sapi Явное лучше, чем неявное (дзен Python)   -  person freakish    schedule 24.12.2014


Ответы (1)


Вы не можете иметь то, что хотите; самый читаемый метод - просто использовать try..except для этого AttributeError:

def visit_Call(self, node):
    try:
        super().visit_Call(node)
    except AttributeError:
        pass

В качестве альтернативы вы можете добавить псевдонимы для NodeVisitor.generic_visit для каждого типа узла в SpecialNodeVisitor:

import inspect

class SpecialNodeVisitor(ast.NodeVisitor):     
    def visit_FunctionDef(self, node):
        print(node)  # prints any node of type FunctionDef

_ast_nodes = inspect.getmembers(
    ast,
    lambda t: isinstance(t, type) and issubclass(t, ast.AST) and t is not ast.AST)
for name, node in _ast_nodes:
    name = 'visit_' + name
    if not hasattr(SpecialNodeVisitor, name):
        setattr(SpecialNodeVisitor, name, ast.NodeVisitor.generic_visit)

Вы можете инкапсулировать это в метакласс, если хотите. Поскольку super() смотрит непосредственно в пространства имен класса __dict__, вы не можете просто определить метод __getattr__ в метаклассе для динамического поиска, к сожалению.

person Martijn Pieters    schedule 24.12.2014
comment
Это на самом деле гениально. Я думал об использовании inspect для получения всех возможных методов, но мне даже не пришло в голову присвоить им псевдоним generic_visit; Спасибо! - person sapi; 24.12.2014