Non-член против функций-членов в Python

Я относительно новичок в Python и изо всех сил пытаюсь совместить особенности языка с привычками, которые я извлек из своего опыта в C++ и Java.

Последний вопрос, который у меня есть, связан с инкапсуляцией, а именно с идеей, лучше всего описанной в пункте 23 " Эффективного C++ " Мейера:

Предпочитать функции, не являющиеся членами, не являющимися друзьями.

Игнорирование отсутствия friend механизм на мгновение, считаются ли функции, не являющиеся членами, более предпочтительными, чем функции-члены в Python?

Обязательный пример:

class Vector(object):
    def __init__(self, dX, dY):
        self.dX = dX
        self.dY = dY

    def __str__(self):
        return "->(" + str(self.dX) + ", " + str(self.dY) + ")"

    def scale(self, scalar):
        self.dX *= scalar
        self.dY *= scalar

def scale(vector, scalar):
    vector.dX *= scalar
    vector.dY *= scalar

Дано v = Vector(10, 20) теперь мы можем позвонить v.scale(2) или же scale(v, 2) удвоить величину вектора.

Учитывая тот факт, что мы используем свойства в этом случае, какой из двух вариантов - если таковые имеются - лучше и почему?

6 ответов

Решение

Интересный вопрос.

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

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

Возможно, вы захотите сделать его автономной функцией (мы на самом деле не используем слово "член" или "не член"), если вам нужно, чтобы она работала с несколькими классами, которые не обязательно наследуются друг от друга или общая база. Благодаря утилитарному типированию это довольно распространенная практика: указать, что ваша функция ожидает объект с определенным набором атрибутов или методов, и что-то делать с ними.

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

Функция-член дает вам выразительность ассоциации функциональности с классом.

Выбирай соответственно. Как правило, функции создаются равными, поэтому все они должны иметь одинаковые предположения об интерфейсе класса. После того, как вы публикуете бесплатную функцию scaleвы эффективно рекламируете .dX а также .dY являются частью публичного интерфейса Vector, Это, вероятно, не то, что вы хотите. Вы делаете это в обмен на возможность повторного использования той же функции с другими объектами, которые имеют .dX а также .dY, Это, вероятно, не будет ценным для вас. Так что в этом случае я бы предпочел функцию-член.

Для хороших примеров предпочтения бесплатной функции нам не нужно смотреть дальше стандартной библиотеки: sorted является свободной функцией, а не функцией-членом listпотому что концептуально вы должны быть в состоянии создать список, который получается в результате сортировки любой итерируемой последовательности.

Посмотрите на свой собственный пример - функция, не являющаяся членом, должна получить доступ к данным-членам класса Vector. Это не победа для инкапсуляции. Это особенно важно, поскольку он изменяет члены данных передаваемого объекта. В этом случае может быть лучше вернуть масштабированный вектор и оставить оригинал без изменений.

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

В итоге:

  1. использовать функции-члены для работы с объектами классов, которыми вы управляете;
  2. использовать функции, не являющиеся членами, для выполнения чисто общих операций, которые реализуются в терминах методов и операторов, которые сами по себе полиморфны.
  3. Вероятно, лучше сохранить объектную мутацию в методах.

Предпочитать функции, не являющиеся членами, не являющиеся друзьями

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

Если вы можете обойтись без необходимости частного / защищенного доступа к членам класса, у вашего дизайна нет причин включать функцию - члена класса. Чтобы думать об этом иначе, при разработке класса, после того как вы перечислили все свойства, вам нужно определить минимальный набор поведений, который был бы достаточен для создания класса. Любая функция-член, которую вы можете написать, используя любой из доступных открытых методов / функций-членов, должна быть общедоступной.

Насколько это применимо в Python

В некоторой степени, если вы осторожны. Python поддерживает более слабую инкапсуляцию по сравнению с другими языками ООП (например, Java/C++), особенно потому, что здесь нет закрытых членов. (Существует нечто, называемое Закрытыми переменными, которое программист может легко написать, добавив префикс "_" перед именем переменной. Это становится классом private через функцию искажения имени.). Так что, если мы буквально примем слово Скотта Мейера, полностью учитывая, что существует тонкое подобие между тем, что должно быть доступно из класса, и тем, что должно быть снаружи. Лучше всего оставить проектировщику / программисту решение, должна ли функция быть неотъемлемой частью класса или нет. Один принцип проектирования, который мы можем легко принять, "Unless your function required to access any of the properties of the class you can make it a non-member function".

Как scale полагается на умножение вектора на член, я бы рассмотрел реализацию умножения в качестве метода и определения scale чтобы быть более общим:

class Vector(object):
    def __init__(self, dX, dY):
        self._dX = dX
        self._dY = dY

    def __str__(self):
        return "->(" + str(self._dX) + ", " + str(self._dY) + ")"

    def __imul__(self, other):
        if other is Vector:
            self._dX *= other._dX
            self._dY *= other._dY
        else:
            self._dX *= other
            self._dY *= other

        return self

def scale(vector, scalar):
    vector *= scalar

Таким образом, интерфейс класса является богатым и оптимизированным при сохранении инкапсуляции.

Один момент еще не озвучен. Предположим, у вас есть класс и класс, и оба должны быть масштабируемыми.

Для функций, не являющихся членами, вам необходимо определить двафункции. Один дляи один для. Но Python динамически типизирован. Это означает, что вы должны дать обеим функциям разные имена, напримерии вам всегда нужно явно вызывать один из них. Если вы назовете их одинаково, Python не будет знать, какой из них вы хотите вызвать.

Это существенная разница между обоими вариантами. Если вы заранее (статически) знаете, какие у вас типы, то решение с разными именами будет работать. Но если вы не знаете этого заранее, методы — это единственный способ, с помощью которого Python обеспечивает «динамическую диспетчеризацию».

Часто «динамическая диспетчеризация» — это именно то, что вам нужно для достижения динамического поведения и желаемого уровня абстракции. Интересно, что классы и объекты — это единственный механизм, с помощью которого объектно-ориентированные языки (а также Python) обеспечивают «динамическую диспетчеризацию». И там для Python тоже подходит рекомендация Скотта Мейера. Его можно прочитать: Используйте методы только тогда, когда вам действительно нужна динамическая диспетчеризация. Это не вопрос, какой синтаксис вам больше нравится.

Хотя методы вам нужны часто (или вы чувствуете, что они вам нужны часто), этот ответ не является рекомендацией в отношении методического подхода. Если вы не планируете использовать такие вещи, как наследование, полиморфизм или операторы, лучше использовать решение, не являющееся членом, поскольку оно может быть реализовано неизменяемым (если вы предпочитаете такой стиль), его проще тестировать, оно имеет меньшую сложность. (поскольку он не привносит в игру все объектно-ориентированные механизмы), его лучше читать, и он более гибок (поскольку его можно использовать для всех вещей, которые имеют два компонента,).

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

Другие вопросы по тегам