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


118

Дзен Python заявляє, що робити лише один спосіб - але часто я стикаюся з проблемою вирішення питання про те, коли використовувати функцію, а не коли використовувати метод.

Візьмемо тривіальний приклад - об’єкт ChessBoard. Скажімо, нам потрібен певний спосіб отримати всі законні кроки короля, доступні на дошці. Чи пишемо ми ChessBoard.get_king_moves () чи get_king_moves (chess_board)?

Ось кілька пов’язаних питань, які я подивився:

Відповіді, які я отримав, були значною мірою непереконливими:

Чому Python використовує методи для деяких функцій (наприклад, list.index ()), але функціонує для інших (наприклад, len (список))?

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

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

Хоча цікаво, вище сказане насправді не говорить про те, яку стратегію слід прийняти.

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

Трохи цікавіше. Я вважаю, що функції в певному розумінні є пітонічною версією інтерфейсів.

Нарешті, від самого Гвідо :

Якщо говорити про здібності / інтерфейси, то змусило мене задуматися про деякі наші "негідні" назви спеціальних методів. У мовній довідці йдеться: "Клас може реалізувати певні операції, які викликаються спеціальним синтаксисом (наприклад, арифметичні операції або підписка та розрізання) шляхом визначення методів зі спеціальними іменами". Але є всі ці методи зі спеціальними іменами на кшталт __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__()прийшов набагато пізніше). Насправді є дві взаємопов'язані причини, обидві ІСІ:

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

(b) Коли я читаю код, який говорить, len(x)я знаю, що він запитує про довжину чогось. Це говорить мені про дві речі: результат - ціле число, а аргумент - якийсь контейнер. Навпаки, коли я читаю x.len(), я вже повинен знати, що xце якийсь контейнер, що реалізує інтерфейс або успадковує клас, у якого є стандарт len(). Зауважте, що ми час від часу виникаємо плутанини, коли у класу, який не реалізує відображення, є метод get()чи keys()метод, або щось, що не є файлом, має write()метод.

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

Другий біт обґрунтування Python, який я пообіцяв пояснити, є причиною, чому я обрав спеціальні методи, щоб виглядати, __special__а не просто special. Я передбачив безліч операцій, які класи можуть захотіти переосмислити, деякі стандартні (наприклад, __add__або __getitem__), деякі не такі стандартні (наприклад, підберезники __reduce__довгий час взагалі не підтримували код C). Я не хотів, щоб ці спеціальні операції використовували звичайні назви методів, тому що тоді вже існуючі класи або класи, написані користувачами без енциклопедичної пам'яті для всіх спеціальних методів, могли б випадково визначити операції, які вони не мали на увазі здійснити , з можливими катастрофічними наслідками. Іван Крстіч пояснив це більш лаконічно у своєму повідомленні, яке надійшло після того, як я все це написав.

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

Я розумію це в тому, що в деяких випадках позначення префікса просто має більше сенсу (тобто, Duck.quack має більше сенсу, ніж квак (Duck) з мовної точки зору.), І знову ж таки, функції дозволяють "інтерфейси".

У такому випадку, я думаю, було б реалізувати get_king_moves виходячи виключно з першої точки Гуйдо. Але це все ще залишає багато відкритих питань щодо скажімо, реалізація класу стеків та черг з подібними методами push та pop - чи це вони повинні бути функціями чи методами? (тут я б здогадався про функції, тому що мені дуже хочеться сигналізувати про push-pop інтерфейс)

TLDR: Чи може хтось пояснити, якою має бути стратегія вирішення питання щодо використання функцій проти методів?


2
Мех, я завжди вважав це як абсолютно довільним. Введення качок дозволяє мати неявні "інтерфейси", це не має великої різниці в тому, чи є у вас, X.frobчи X.__frob__вільно стоять frob.
Cat Plus Plus

2
Хоча я з вами переважно згоден, у принципі ваша відповідь не є пітонічною. Нагадаємо: "В умовах неоднозначності відмовтеся від спокуси здогадатися". (Звичайно, терміни змінили б це, але я роблю це для розваги / самовдосконалення.)
Ceasar Bautista

Це одне, що мені не подобається в python. Я відчуваю, що якщо ви збираєтесь змусити вводити литий текст, як int для рядка, то просто зробіть це методом. Прикро, що потрібно вкладати це в парени і забирає багато часу.
Мет

1
Це найважливіша причина, що мені не подобається Python: ви ніколи не знаєте, чи потрібно шукати функцію чи метод, коли хочете чогось досягти. І це навіть стає більш заплутаним, коли ви використовуєте додаткові бібліотеки з новими типами даних, такими як вектори або кадри даних.
фондж

1
"Дзен Python заявляє, що робити тільки один спосіб", крім цього немає.
покладено

Відповіді:


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."

Віддаляючись від цієї аналогії, чому ми використовуємо методи та класи? Тому що ми хочемо містити дані та сподіваємось структурувати наш код таким чином, щоб він був повторним та розширеним у майбутньому. Це підводить нас до поняття капсулювання, яке дуже дороге для OO-дизайну.

Принцип інкапсуляції - це дійсно те, до чого це зводиться: як дизайнер ви повинні приховувати все про внутрішню реалізацію та клас, до яких не обов'язково отримувати доступ будь-який користувач чи інший розробник. Оскільки ми маємо справу з екземплярами класів, це зводиться до "які операції мають вирішальне значення в цьому випадку ". Якщо операція не є конкретною інстанцією, то вона не повинна бути функцією-членом.

TL; DR : що сказав @Bryan Якщо він працює над екземпляром і потребує доступу до даних, що є внутрішніми для екземпляра класу, це повинна бути функцією-членом.


Отже, коротко кажучи, функції, які не належать до членів, діють на незмінних об'єктах, а об'єкти, що змінюються, використовують функції члена? (Або це занадто суворе узагальнення? Це для певних творів лише тому, що незмінні типи не мають стану.)
Ceasar Bautista

1
Зі строгої точки зору ООП, я думаю, це справедливо. Оскільки Python має як публічні, так і приватні змінні (змінні з іменами, що починаються з __) та забезпечує нульовий гарантований захист доступу на відміну від Java, немає абсолютних просто тому, що ми обговорюємо дозвільну мову. Однак менш дозволеною мовою, як Java, пам’ятайте, що функції adFFF (() adFoo () є нормою, тому незмінність не є абсолютною. Клієнтському коду просто заборонено приймати завдання членам.
arrdem

1
@Ceasar Це неправда. Незмінні об'єкти мають стан; інакше не було б нічого, щоб розрізнити будь-яке ціле число від будь-якого іншого. Незмінні об'єкти не змінюють свого стану. Взагалі це робить його набагато менш проблематичним для всіх їх держав. І в цій обстановці набагато простіше осмислено маніпулювати незмінним загальнодоступним об'єктом за допомогою функцій; немає "привілею" бути методом.
Бен

1
@CeasarBautista так, у Бена є пункт. Є три "основні" школи дизайну коду 1) не розроблені, 2) ООП та 3) функціональні. у функціональному стилі взагалі немає держав. Ось так я бачу більшість розроблених кодів пітона, він вимагає введення та отримання результатів з невеликими побічними ефектами. Точка ООП є те , що все має стан. Класи є контейнером для станів, а функції "член", отже, залежать від стану та на основі побічних ефектів, які завантажують стан, визначений у класі при кожному виклику. Python, як правило, схиляється до функціональності, отже, перевага коду, який не є членом.
arrdem

1
їсти (само), лайно (само), вмирати (само). hahahaha
Wapiti

27

Використовуйте клас, коли хочете:

1) Виділіть викликовий код від деталей реалізації - скориставшись абстракцією та інкапсуляцією .

2) Коли ви хочете бути заміненими іншими об'єктами - скористайтеся поліморфізмом .

3) Коли ви хочете повторно використовувати код для подібних об'єктів - скористайтеся спадщиною .

Використовуйте функцію для дзвінків, які мають сенс для багатьох різних типів об'єктів - наприклад, вбудовані функції len і repr застосовуються до багатьох видів об'єктів.

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


8

Ось просте правило: якщо код діє на один екземпляр об'єкта, використовуйте метод. Ще краще: використовуйте метод, якщо немає вагомих причин писати його як функцію.

У вашому конкретному прикладі ви хочете, щоб він виглядав так:

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

Не думайте про це. Завжди використовуйте методи, доки ви не скажете собі, що "це не має сенсу робити це методом", і в цьому випадку ви можете зробити функцію.


2
Чи можете ви пояснити, чому ви замовчуєте методи над функціями? (А чи можете ви пояснити, чи все-таки це правило має сенс у випадку методів
стеки

11
Це правило не має сенсу. Сама стандартна бібліотека може бути керівництвом щодо використання класів проти функцій. heapq і математика - це функції, оскільки вони працюють на звичайних об'єктах python (плаває і списки) і тому, що їм не потрібно підтримувати складний стан, як це робить випадковий модуль.
Реймонд Хеттінгер

1
Ви кажете, що правило не має сенсу, оскільки стандартна бібліотека його порушує? Я не думаю, що такий висновок має сенс. Для одного, великі правила - це саме те - вони не є абсолютними правилами. Крім того , велика частина стандартної бібліотеки робить слідувати цьому правилу великого пальця. ОП шукала кілька простих вказівок, і я думаю, що моя порада ідеально підходить для того, хто тільки починає. Однак я ціную вашу точку зору.
Брайан Оуклі

Ну, STL має деякі вагомі причини для цього. Для математики та спів - це, очевидно, марно мати клас, оскільки у нас його немає (в інших мовах - це класи, що мають лише статичні функції). Для речей, які працюють на декількох різних контейнерах, як-от скажімо, len()можна також стверджувати, що дизайн має сенс, хоча я особисто вважаю, що функції теж не будуть поганими - ми просто матимемо ще одну умову, яка len()завжди повинна повертати integer (але з усіма додатковими проблемами backcomp я також не виступав би за пітон)
Voo

-1, оскільки ця відповідь є абсолютно довільною: ви, по суті, намагаєтесь продемонструвати, що X кращий за Y, припускаючи, що X кращий за Y.
позначено

7

Я зазвичай думаю про предмет як людина.

Атрибути - ім’я людини, зріст, розмір взуття тощо.

Методи та функції - це операції, які людина може виконувати.

Якщо операцію може зробити будь-яка людина, не вимагаючи нічого унікального для цієї конкретної людини (і не змінюючи нічого в цій конкретній особі), то це функція і повинна бути записана як така.

Якщо операція діє на людину (наприклад, їжа, прогулянка, ...) або вимагає чогось унікального для цієї людини, щоб долучитися до неї (наприклад, танці, написання книги, ...), то це повинен бути метод .

Звичайно, не завжди банально перекладати це на конкретний об’єкт, з яким ти працюєш, але я вважаю, що це хороший спосіб продумати це.


але ти можеш виміряти зріст будь-якої старої людини, так що за такою логікою воно має бути height(person), чи не так person.height?
ендоліт

@endolith Звичайно, але я б сказав, що висота краще як атрибут, тому що для її отримання не потрібно виконувати жодної фантазії. Написання функції для отримання числа здається непотрібним обручем для переходу через нього.
процвітає

@endolith
процвітає

Це не відповідає дійсності. Що робити, якщо у вас є are_married(person1, person2)? Цей запит є дуже загальним і тому повинен бути функцією, а не методом.
Пітікос

@ Pithikos Звичайно, але це також включає більше, ніж просто атрибут або дію, що виконується на одному Об'єкті (Особі), а скоріше відношення між двома об'єктами.
процвітає

4

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

Все, що є частиною основної абстракції того, "що ти можеш зробити з річчю ", як правило, має бути методом. Це, як правило, включає все, що може змінити річ , оскільки внутрішній стан даних зазвичай вважається приватним і не є частиною логічної ідеї "що ти можеш зробити з річчю ".

Коли ви приходите до підвищення операцій на рівні, особливо якщо вони пов'язані з кількох речей , я вважаю , що вони, як правило , найбільш природно виражені як функції, якщо вони можуть бути побудовані з громадської абстракції речі без необхідності спеціального доступу до внутрішніх (якщо вони » повторні методи якогось іншого об'єкта). Це має велику перевагу в тому, що коли я вирішу повністю переписати внутрішні відомості про те, як працює моя річ (не змінюючи інтерфейс), у мене є лише невеликий набір методів для перезапису, а потім усі зовнішні функції, написані в рамках цих методів буде просто працювати. Я вважаю, що наполягати на тому, що всі операції з класом X - це методи класу X, призводять до надскладних класів.

Це залежить від коду, про який я пишу. Для деяких програм я моделюю їх як сукупність об'єктів, взаємодія яких породжує поведінку програми; тут найважливіша функціональність тісно поєднана з одним об’єктом і так реалізується в методах, з розсіюванням корисних функцій. Для інших програм найважливіший матеріал - це набір функцій, що управляють даними, а класи використовуються лише для реалізації природних "типів качок", якими маніпулюють функції.


0

Можна сказати, що "за умови неоднозначності відмовтеся від спокуси здогадатися".

Однак це навіть не здогад. Ви абсолютно впевнені, що результати обох підходів однакові, оскільки вони вирішують вашу проблему.

Я вважаю, що лише добре мати декілька способів досягнення цілей. Я покірно сказав би вам, як це робили інші користувачі, використовувати той, хто "смачніше" / відчуває себе більш інтуїтивно , з точки зору мови.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.