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

118

Дзен 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.

Наконец, от самого Гвидо :

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

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

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

Гектометр Я не уверен, что согласен (подумайте, что :-).

Есть две части «обоснования Python», которые я хотел бы объяснить в первую очередь.

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

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

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

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

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

- --Гидо ван Россум (домашняя страница: http://www.python.org/~guido/ )

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

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

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

Цезарь Баутиста
источник
2
Мех, я всегда считал это совершенно произвольным. Утиная типизация допускает неявные "интерфейсы", не имеет большого значения, есть ли у вас X.frobили X.__frob__и автономный frob.
Cat Plus Plus
2
Хотя я в основном с вами согласен, в принципе ваш ответ не является питоническим. Вспомните: «Перед лицом двусмысленности откажитесь от соблазна угадать». (Конечно, дедлайны изменили бы это, но я делаю это для развлечения / самосовершенствования.)
Ceasar Bautista
Это то, что мне не нравится в питоне. Я чувствую, что если вы собираетесь принудительно вводить типизацию, например, int в строку, просто сделайте это методом. Это раздражает, когда приходится заключать его в скобки, и отнимает много времени.
Мэтт
1
Это самая важная причина, по которой мне не нравится Python: вы никогда не знаете, нужно ли вам искать функцию или метод, когда вы хотите чего-то достичь. И это даже становится более запутанным, когда вы используете дополнительные библиотеки с новыми типами данных, такими как векторы или фреймы данных.
vonjd
1
«Дзен Python утверждает, что должен быть только один способ делать что-то», но это не так.
gented

Ответы:

84

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

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

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

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. Если он работает с экземпляром и ему требуется доступ к данным, внутренним по отношению к экземпляру класса, это должна быть функция-член.

arrdem
источник
Короче говоря, функции, не являющиеся членами, работают с неизменяемыми объектами, а изменяемые объекты используют функции-члены? (Или это слишком строгое обобщение? В некоторых случаях это работает только потому, что неизменяемые типы не имеют состояния.)
Цезарь Баутиста
1
Я считаю, что со строгой точки зрения ООП это справедливо. Поскольку Python имеет как общедоступные, так и частные переменные (переменные с именами, начинающимися с __) и обеспечивает нулевую гарантированную защиту доступа, в отличие от Java, здесь нет абсолютов просто потому, что мы обсуждаем разрешительный язык. Однако в менее разрешительном языке, таком как Java, помните, что функции getFoo () и setFoo () являются нормой, поэтому неизменность не является абсолютной. Коду клиента просто не разрешается делать назначения участникам.
arrdem
1
@Ceasar Это неправда. Неизменяемые объекты имеют состояние; в противном случае не было бы ничего, что отличало бы одно целое от другого. Неизменяемые объекты не меняют своего состояния. В общем, это делает гораздо менее проблематичным, чтобы все их состояние было публичным. И в этом случае гораздо проще осмысленно манипулировать неизменяемым общедоступным объектом с помощью функций; нет никакой «привилегии» быть методом.
Бен
1
@CeasarBautista, да, Бен прав. Существует три «основных» школы разработки кода: 1) непроектированный, 2) ООП и 3) функциональный. в функциональном стиле состояний вообще нет. Это то, как я вижу большую часть кода Python, он принимает ввод и генерирует вывод с небольшими побочными эффектами. Точка ООП является то , что все имеет состояние. Классы являются контейнером для состояний, и поэтому функции-члены являются зависимым от состояния кодом, основанным на побочных эффектах, который загружает состояние, определенное в классе, при каждом вызове. Python имеет тенденцию к упрощению функциональности, отсюда и предпочтение кода, не являющегося членом.
arrdem
1
ешь (я), дерьмо (я), умри (я). хахахаха
Wapiti
27

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

1) Изолируйте вызывающий код от деталей реализации - используя абстракцию и инкапсуляцию .

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

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

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

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

Раймонд Хеттингер
источник
7

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

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

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

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

Брайан Окли
источник
2
Можете ли вы объяснить, почему вы по умолчанию предпочитаете методы функциям? (И не могли бы вы объяснить, имеет ли это правило смысл в случае методов stack и queue / pop и push?)
Ceasar Bautista
11
Это практическое правило не имеет смысла. Сама стандартная библиотека может помочь понять, когда использовать классы вместо функций. heapq и math - это функции, поскольку они работают с обычными объектами Python (числами с плавающей запятой и списками) и потому, что им не нужно поддерживать сложное состояние, как это делает модуль random .
Raymond Hettinger
1
Вы хотите сказать, что правило не имеет смысла, потому что стандартная библиотека его нарушает? Я не думаю, что такой вывод имеет смысл. Во-первых, эмпирические правила таковы - они не абсолютные правила. Кроме того, большая часть стандартной библиотеки действительно следует этому правилу. OP искал несколько простых рекомендаций, и я думаю, что мой совет идеально подходит для тех, кто только начинает. Однако я ценю вашу точку зрения.
Bryan Oakley
Что ж, у STL есть на то веские причины. Для математики и других очевидно бесполезно иметь класс, поскольку у нас нет для него состояния (на других языках это классы только со статическими функциями). Для вещей, которые работают с несколькими разными контейнерами, например, скажем, len()также можно утверждать, что дизайн имеет смысл, хотя я лично считаю, что функции там тоже не будут такими уж плохими - у нас просто будет другое соглашение, которое len()всегда должно возвращать целое число (но со всеми дополнительными проблемами backcomp я бы не стал защищать это и для python)
Voo
-1, поскольку этот ответ совершенно произвольный: вы, по сути, пытаетесь продемонстрировать, что X лучше, чем Y, предполагая, что X лучше, чем Y.
Создано
7

Обычно я думаю о предмете как о человеке.

Атрибуты - это имя человека, рост, размер обуви и т. Д.

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

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

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

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

Thriveth
источник
но вы можете измерить рост любого пожилого человека, значит, по этой логике так и должно быть height(person), не так person.heightли?
Эндолит
@endolith Конечно, но я бы сказал, что высоту лучше использовать как атрибут, потому что вам не нужно выполнять какую-либо сложную работу, чтобы получить ее. Написание функции для получения числа похоже на ненужные прыжки.
Thriveth
@endolith С другой стороны, если это ребенок, который растет и меняет рост, было бы очевидно, что об этом позаботится метод, а не функция.
Thriveth
Это неправда. А если да are_married(person1, person2)? Это очень общий запрос, поэтому он должен быть функцией, а не методом.
Pithikos
@Pithikos Конечно, но это также включает в себя нечто большее, чем просто атрибут или действие, выполняемое над одним объектом (человеком), а скорее связь между двумя объектами.
Thriveth
5

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

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

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

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

Бен
источник
0

Вы можете сказать, что «перед лицом двусмысленности откажитесь от соблазна угадать».

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

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

Жоао Силва
источник