Является ли наследование Python стилем наследования «как есть» или композиционным стилем?

10

Учитывая, что Python допускает множественное наследование, как выглядит идиоматическое наследование в Python?

В языках с единичным наследованием, таких как Java, наследование будет использоваться, когда вы можете сказать, что один объект «a-a» другого объекта и вы хотите разделить код между объектами (от родительского объекта до дочернего объекта). Например, вы можете сказать, что Dogэто Animal:

public class Animal {...}
public class Dog extends Animal {...}

Но поскольку Python поддерживает множественное наследование, мы можем создать объект, составив множество других объектов вместе. Рассмотрим пример ниже:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

Это приемлемое или хорошее использование множественного наследования в Python? Вместо того чтобы говорить, что наследование - это когда один объект «является-а» другого объекта, мы создаем Userмодель, состоящую из UserServiceи LoggingService.

Вся логика для баз данных или сетевых операций может храниться отдельно от Userмодели, помещая их в UserServiceобъект и сохраняя всю логику для входа в систему LoggingService.

Я вижу некоторые проблемы с этим подходом:

  • Создает ли это объект Бога? Поскольку Userнаследует или состоит из, UserServiceи LoggingServiceдействительно ли он следует принципу единой ответственности?
  • Для того, чтобы получить доступ к методам родительского / следующего в строке объекта (например, UserService.validate_credentialsмы должны использовать super. Это немного затрудняет просмотр объекта, который будет обрабатывать этот метод, и не так ясно, как, скажем, , создавая UserServiceи делая что-то вродеself.user_service.validate_credentials

Что бы Pythonic способ реализовать вышеупомянутый код?

Iain
источник

Ответы:

9

Является ли наследование Python стилем наследования «как есть» или композиционным стилем?

Python поддерживает оба стиля. Вы демонстрируете отношение has-a of состав, где у пользователя есть функция ведения журнала из одного источника и проверка учетных данных из другого источника. LoggingServiceИ UserServiceоснованиями являются Примеси: они обеспечивают функциональные возможности и не предназначены , чтобы быть создан сами по себе.

Составляя миксины в типе, у вас есть пользователь, который может войти, но он должен добавить свою собственную функцию создания экземпляров.

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

Создает ли это объект Бога?

Ведение журнала кажется несколько тангенциальным - у Python есть свой собственный модуль ведения журнала с объектом средства ведения журнала, и соглашение состоит в том, что для каждого модуля существует одно средство ведения журнала.

Но отложите модуль регистрации. Возможно, это нарушает единоличную ответственность, но, возможно, в вашем конкретном контексте это крайне важно для определения пользователя. Разделение ответственности может быть спорным. Но более широкий принцип заключается в том, что Python позволяет пользователю принимать решение.

Супер менее понятно?

superнеобходимо только в том случае, если вам необходимо делегировать родителю в Порядке разрешения методов (MRO) изнутри функции с тем же именем. Лучше всего использовать его вместо жесткого кодирования вызова метода родителя. Но если вы не собираетесь жестко кодировать родителя, вам это не нужно super.

В вашем примере здесь вам нужно только сделать self.validate_credentials. selfне более понятно, с вашей точки зрения. Они оба следуют за MRO. Я бы просто использовал каждый, где это уместно.

Если бы вы позвонили authenticate, validate_credentialsвместо этого вам пришлось бы использовать super(или жестко закодировать родительский), чтобы избежать ошибки рекурсии.

Альтернативный код предложения

Итак, предполагая, что семантика в порядке (например, ведение журнала), я бы сделал в классе User:

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True
Аарон Холл
источник
1
Я не согласен. Наследование всегда создает копию открытого интерфейса класса в его подклассах. Это не отношения "есть". Это подтип, простой и понятный, и поэтому он не подходит для описанного приложения.
Жюль
@Jules С чем ты не согласен? Я сказал много вещей, которые наглядны, и сделал логические выводы. Вы находитесь неправильно , когда вы говорите, «Наследование всегда создает копию открытого интерфейса класса в его подклассов.» В Python нет копии - методы ищутся динамически в соответствии с порядком разрешения методов алгоритма C3 (MRO).
Аарон Холл
1
Дело не в конкретных деталях того, как работает реализация, а в том, как выглядит открытый интерфейс класса. В случае примера Userобъекты имеют в своих интерфейсах не только элементы, определенные в Userклассе, но также и элементы , определенные в UserServiceи LoggingService. Это не отношение «есть», потому что общедоступный интерфейс копируется (хотя не путем прямого копирования, а косвенным поиском интерфейсов суперклассов).
Жюль
Имеет-значит состав. Mixins являются формой композиции. Класс пользователя является не UserService или LoggingService, но имеет такую функциональность. Я думаю, что наследование Python больше отличается от Java, чем вы думаете.
Аарон Холл
@AaronHall Вы слишком упрощаете (это имеет тенденцию противоречить вам другой ответ , который я нашел случайно). С точки зрения взаимосвязи подтипов пользователь является одновременно и UserService, и LoggingService. Теперь, дух здесь состоит в том, чтобы составить функциональные возможности так, чтобы у Пользователя были такие и такие функциональные возможности. Миксины в общем случае не требуют реализации с множественным наследованием. Однако это обычный способ сделать это в Python.
coredump
-1

Помимо факта, что он допускает множественные суперклассы, наследование Python существенно не отличается от Java, то есть члены подкласса также являются членами каждого из своих супертипов [1]. Тот факт, что Python использует duck-typing, также не имеет значения: ваш подкласс имеет всех членов своих суперклассов, поэтому может использоваться любым кодом, который может использовать эти подклассы. Тот факт, что множественное наследование эффективно реализуется с помощью композиции, представляет собой «красную сельдь»: проблема заключается в автоматическом копировании свойств одного класса в другой, и не имеет значения, использует ли он композицию или просто волшебным образом догадывается, как предполагаются члены. работать: иметь их неправильно.

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

При проектировании объектно-ориентированных систем в Python применяется тот же принцип, который проповедуется в книгах по Java-дизайну: предпочтение композиции наследованию. То же самое верно для (большинства [2]) других систем с множественным наследованием.

[1]: вы могли бы назвать это отношением «есть», хотя лично мне не нравится этот термин, потому что он предполагает идею моделирования реального мира, а объектно-ориентированное моделирование отличается от реального мира.

[2]: я не уверен в C ++. C ++ поддерживает «частное наследование», которое по сути является композицией, без необходимости указывать имя поля, когда вы хотите использовать открытые члены унаследованного класса. Это никак не влияет на публичный интерфейс класса. Я не люблю его использовать, но не вижу веских причин не делать этого.

Жюль
источник