Может ли конструктор класса возвращать дочерний класс?

#python #python-3.x #class

#python #python-3.x #класс

Вопрос:

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

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

 class MyNum(MyTerm):
    def __init__(self, n):
        self.num = n

    def latex(self):
        return str(self.num)


class MyDivision(MyTerm):
    def __init__(self, n, d):
        self.numerator = n
        self.denominator = d

    def latex(self):
        return '\frac {{ {} }} {{ {} }}'.format(self.numerator, self.denominator)


def parseTerm(term):
    matches = re.match(r'^[0-9] $', term)
    if matches is not None:
        return MyNum(term)

    matches = re.match(r'^([0-9] )/([0-9] )$', term)
    if matches is not None:
        return MyDivision(matches[1], matches[2])
  

Итак, у нас есть заводская функция parseTerm , которая вернет соответствующий класс. parseTerm("2") выдаст MyNum , в то время как parseTerm("2/3") выдаст MyDivision .

Я создал это по совету из другого вопроса, в котором предполагалось, что классы никогда не должны возвращать другую функцию. Но чем больше я думаю об этом, тем больше я неудовлетворен. Каждый класс будет реализовывать точно такие же внешние методы, и вопрос о том, с каким из них вы остаетесь, — это детали реализации. Мне кажется уместным, чтобы пользователь вызывал num = MyTerm("2") и получал тот дочерний класс, который подходит, а вызов функции вместо создания экземпляра класса добавляет путаницы.

Я приветствую критику за то, что мои рассуждения неверны, поскольку я иду против советов, но мой технический вопрос таков: как мне на самом деле это сделать? Как конструктор класса возвращает другой класс?

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

1. Я бы сказал, что создание объекта и получение чего-то другого создало бы путаницу. Вызов функции / метода этого не делает. Даже если бы это было возможно, я бы никогда так не поступил.

2. Можете ли вы поделиться определением MyTerm

3. Текущий код в порядке, единственное, что вы можете получить, усложнив, это больше ошибок — в итоге клиентский код все равно будет выглядеть как вызов функции, приводящий к некоторому экземпляру класса.

4. Я бы сказал, что достаточно просто реализовать Parser класс, который заботится о разборе термина и возвращает правильный MyTerm подкласс. Проверьте, имеет ли смысл мой ответ @wyatt

Ответ №1:

Это возможно с помощью __new__ , но добавленная функциональность не стоит такой сложности.

Наивный:

 class MyTerm:
    def __new__(cls, *args):
        return parseTerm(*args)
  

завершится неудачей с RecursionError , потому что создание объекта подкласса (в ParseTerm вызвало бы … MyTerm.__new__ !

Хорошо, давайте просто делегируем object для подклассов:

 class MyTerm:
    def __new__(cls, *args):
        if cls is MyTerm:
            return parseTerm(*args)
        return object.__new__(cls)
  

Это будет работать для "2" , а не для "4/2" , потому что MyDivision.__init__ вызывается дважды:

  • первый раз изнутри parseTerm с аргументами «4», «2»
  • второй раз с помощью Python-машин после MyTerm.__new__ с исходным аргументом «4/2»

Таким образом, вам придется разрешить MyDivision.__init__ принимать и игнорировать этот вызов:

 class MyDivision(MyTerm):
    def __init__(self, n, d = None):
        print(self, n, d)
        if d is not None:
            self.numerator = n
            self.denominator = d
  

Но это позволило бы MyDivision("4") … Итак, новый тест:

 class MyDivision(MyTerm):
    def __init__(self, n, d = None):
        print(self, n, d)
        if d is not None:
            self.numerator = n
            self.denominator = d
        if not hasattr(self, denominator):
            raise TypeError("Missing required argument")
  

ИМХО, оно того не стоит…

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

1. Опасность игнорирования советов. Вы правы, я пропустил большую часть сложности, которую это повлечет за собой. Спасибо.

2. Поможет ли каким-либо образом добавление Parser класса, о котором я упоминал в своем ответе @wyatt @serge-ballesta

3. @DeveshKumarSingh: Я так не думаю. Ожидается, что конструктор класса будет создавать объекты этого класса, точка. Любая попытка действовать по-другому закончится ужасным взломом со всеми оговорками.

4. Ааа, так вместо того, чтобы помещать parseTerm , возврат объекта класса в __init__ будет работать для Parser класса?

Ответ №2:

Вы должны просто вызвать функцию, которая выполняет логику, необходимую вам для определения, какой класс должен быть возвращен, и вернуть этот дочерний класс. В этом случае num = parseTerm("2") , имело бы смысл. Поместите все сходства в init функцию вашего родительского класса и выполните следующее в init функции вашего дочернего класса.

 class MyDivision(MyTerm):
    def __init__(self, n, d):
        super(MyDivision, self).__init__()
        self.numerator = n
        self.denominator = d
  

super() выполнит функцию инициализации родительского класса.

Кроме того, я не думаю, что использование функции здесь сбивает с толку. Если вы назовете его достаточно хорошо, это не вызовет путаницы.

term = create_new_term("2")

Ответ №3:

Я бы сказал, что более чистым подходом будет определение Parser класса, который принимает термин в своем конструкторе и имеет parseTerm функцию, которая возвращает экземпляр класса для вызова.

 class Parser:

    def __init__(self, term):
        self.term = term

    def parseTerm(self):

        matches = re.match(r'^[0-9] $', self.term)
        if matches is not None:
            return MyNum(self.term)

        matches = re.match(r'^([0-9] )/([0-9] )$', self.term)
        if matches is not None:
            return MyDivision(matches[1], matches[2])
  

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

 terms = ["2", "2/3"]
for term in terms:
    obj = Parser(term).parseTerm()
    print(obj.latex())
  

И результат таков :

 2
frac { 2 } { 3 }
  

Мы видим, что теперь, когда мы делегировали все Parser , это становится намного проще. Мы можем добавить условия в parseTerm для большего количества MyX классов и получить объекты.

Ответ №4:

Я бы исключил возможные совпадения и вместо этого использовал именованные группы совпадений:

 valid_terms = [{'regex': r'^(?P<n>[0-9] )$', 'class': MyNum},
               {'regex': r'^(?P<n>[0-9] )/(?P<d>[0-9] )$', 'class': MyDivision}]

def parseTerm(term):
    for valid_term in valid_terms:
        match = re.match(valid_term['regex'], term)
        if match is not None:
            return valid_term['class'](**match.groupdict())

    raise ValueError(f'{term} is not a valid term.')