В Python, когда я должен использовать функцию вместо метода?

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

Давайте рассмотрим тривиальный пример - объект ChessBoard. Пусть говорят, что нам нужен какой-то способ получить все законные корольские движения, доступные на доске. Мы пишем ChessBoard.get_king_moves() или get_king_moves (chess_board)?

Вот некоторые связанные вопросы, на которые я смотрел:

Ответы, которые я получил, были в основном неубедительными:

Почему Python использует методы для некоторых функций (например, list.index()), но работает для других (например, len (list))?

Основная причина - история. Функции были использованы для тех операций, которые были типичными для группы типов, и которые были предназначен для работы даже для объектов, которые вообще не имеют методов (например, кортежи). Также удобно иметь функцию, которая может легко применять к аморфной коллекции объектов, когда вы используете функциональные возможности Python (map(), apply() и др.).

Фактически, реализация len(), max(), min() в качестве встроенной функции на самом деле меньше кода, чем реализация их как методов для каждого типа. Можно окунуться в отдельные случаи, но это часть Python и его слишком поздно сделать такие фундаментальные изменения. Эти функции чтобы избежать значительного повреждения кода.

Несмотря на то, что вышеизложенное не говорит особо о том, какую стратегию принять.

Это одна из причин - с помощью пользовательских методов разработчики бесплатно выбрать другое имя метода, например getLength(), length(), getlength() или вообще. Python обеспечивает строгое именование так, что можно использовать общую функцию len().

Чуть более интересный. Я считаю, что функции в некотором смысле являются версией интерфейсов Pythonic.

Наконец, из самого Guido:

Говоря о способностях/интерфейсах, я подумал о некоторых из наших   Названия специальных методов "изгоев". В Справочнике по языку говорится: "A   класс может реализовать определенные операции, которые вызываются специальными   синтаксис (например, арифметические операции или субтипирование и нарезка) на   определяя методы со специальными именами". Но есть все эти методы   со специальными именами, такими как __len__ или __unicode__, которые кажутся   предоставляемых в интересах встроенных функций, а не для   поддержка синтаксиса. Предположительно, в Python на основе интерфейса эти   методы превратились бы в регулярно названные методы на ABC, так что   __len__ станет

class container:
  ...
  def len(self):
    raise NotImplemented
     

Хотя, подумав об этом еще, я не понимаю, почему все синтаксические   операции не будут просто вызывать соответствующий нормальный метод   на конкретной ABC. Например, "<", предположительно, вызывает    "object.lessthan" (или, возможно, "comparable.lessthan" ). Так что еще один   выгодой будет способность отучить Питона от этого   странное имя, которое кажется мне улучшением HCI.

Hm. Я не уверен, что согласен (рисунок: -).

Есть два бита "обоснования Питона", которые я хотел бы объяснить первый.

Прежде всего, я выбрал len (x) по x.len() по причинам HCI (def __len__()пришел намного позже). На самом деле есть две взаимосвязанные причины: и HCI:

(a) Для некоторых операций префиксная нотация читается лучше, чем Операции postfix - prefix (и infix!) имеют давнюю традицию в математика, которая любит записи, в которых математик думает о проблеме. Сравните легкость, с которой мы переписать формулу типа x*(a+b) в x*a + x*b на неуклюжесть делая то же самое, используя необработанную нотацию OO.

(b) Когда я читаю код, который говорит len(x), я знаю, что он просит длина чего-то. Это говорит мне о двух вещах: результат integer, а аргумент - это какой-то контейнер. Иначе, когда я читаю x.len(), я должен уже знать, что x - это своего рода контейнер, реализующий интерфейс или наследующий от класса, который имеет стандарт len(). Свидетельствуйте путаницу, которую мы иногда имеем, когда класс, не реализующий отображение, имеет get() или keys()метод или что-то, что не является файлом, имеет метод write().

Говоря то же самое по-другому, я вижу "len" как встроенный операция. Мне бы очень хотелось потерять это. Я не могу точно сказать, означает ли вы это или нет, но "def len (self):...", похоже, похоже на вас хочу понизить его до обычного метода. Я сильно -1 на этом.

Второй бит обоснования Python, который я обещал объяснить, является причиной почему я выбрал специальные методы для поиска __special__, а не просто special. Я ожидал много операций, которые могут потребоваться классам для переопределения, некоторые стандартные (например, __add__ или __getitem__), некоторые не такие стандартный (например, рассол __reduce__ в течение длительного времени не имел поддержки в C код вообще). Я не хотел, чтобы эти специальные операции использовали обычные имена методов, потому что тогда уже существовавшие классы или классы, написанные пользователи без энциклопедической памяти для всех специальных методов, будут подвержены случайному определению операций, которые они не с возможными катастрофическими последствиями. Иван Крстич объяснил это более кратким в своем послании, которое прибыло после того, как я все это написано.

- --Guido van Rossum (домашняя страница: http://www.python.org/~guido/)

Мое понимание этого заключается в том, что в некоторых случаях префиксная нотация имеет смысл больше (т.е. Duck.quack имеет больше смысла, чем quack (Duck) с лингвистической точки зрения.) И снова функции допускают "интерфейсы".

В таком случае я предполагал бы реализовать get_king_moves, основываясь исключительно на первой точке Guido. Но это все еще оставляет много открытых вопросов, касающихся, скажем, реализации класса стека и очереди с похожими методами push и pop - должны ли они быть функциями или методами? (здесь я бы угадал функции, потому что я действительно хочу сигнализировать push-поп-интерфейс)

TL;DR: Может кто-нибудь объяснить, какая стратегия для решения, когда использовать функции против методов, должна быть?

Ответ 1

Мое общее правило - это операция, выполняемая над объектом или объектом?

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

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

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

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

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

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

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

TL; DR: что @Bryan сказал. Если он работает с экземпляром и ему нужно получить доступ к данным, которые являются внутренними для экземпляра класса, он должен быть функцией-членом.

Ответ 2

Используйте класс, если хотите:

1) Изолировать код вызова из деталей реализации - воспользовавшись abstraction и encapsulation.

2) Если вы хотите быть заменяемым для других объектов - воспользуйтесь polymorphism.

3) Если вы хотите повторно использовать код для похожих объектов - воспользуйтесь inheritance.

Используйте функцию для вызовов, которые имеют смысл для многих разных типов объектов - например, встроенный len и repr применяются ко многим типам объектов.

Таким образом, выбор иногда сводится к вкусу. Подумайте о том, что наиболее удобно и доступно для типичных вызовов. Например, что было бы лучше (x.sin()**2 + y.cos()**2).sqrt() или sqrt(sin(x)**2 + cos(y)**2)?

Ответ 3

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

В вашем конкретном примере вы хотите, чтобы он выглядел следующим образом:

chessboard = Chessboard()
...
chessboard.get_king_moves()

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

Ответ 4

Я обычно думаю об объекте, как о человеке.

Атрибуты - это имя, высота, размер обуви и т.д.

Методы и функции - это операции, которые может выполнять человек.

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

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

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

Ответ 5

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

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

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

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