Якщо функція A потрібна лише функцією B, чи слід A визначати всередині B? [зачинено]


147

Простий приклад. Два методи, один викликаний від іншого:

def method_a(arg):
    some_data = method_b(arg)

def method_b(arg):
    return some_data

У Python ми можемо оголосити defвсередині іншого def. Отже, якщо method_bце вимагається і дзвонять лише з method_a, я повинен заявити method_bвсередину method_a? подобається це :

def method_a(arg):

    def method_b(arg):
        return some_data

    some_data = method_b(arg)

Або я повинен уникати цього робити?


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

6
Ви усвідомлюєте, що другий приклад інший, тому що ви не дзвоните method_b ? (@inspector: Вам потрібно, строго кажучи, але це надзвичайно корисно, коли ви потрапляєте у трохи функціонального програмування, зокрема закриття).

3
@delnan: Я думаю, ти мав на увазі "Вам не потрібно, строго кажучи, але ..."
martineau

4
Випадки використання внутрішніх функцій чудово узагальнені за посиланням: https://realpython.com/blog/python/inner-functions-what-are-they-good-for/ . Якщо ваше використання не вписується в жоден із випадків, краще уникайте цього.

1
Відмінне запитання, але немає реальної відповіді, як здається ...
Mayou36

Відповіді:


136
>>> def sum(x, y):
...     def do_it():
...             return x + y
...     return do_it
... 
>>> a = sum(1, 3)
>>> a
<function do_it at 0xb772b304>
>>> a()
4

Це те, що ви шукали? Це називається закриттям .


14
Це набагато краще пояснення. Я видалив свою відповідь
pyfunc

чому б просто не зробити суму def (x, y): повернути x + y?
манго

4
@mango: Це лише іграшковий приклад для передачі концепції - в реальному використанні те do_it(), що, мабуть, може бути трохи складніше, ніж те, що може бути оброблено деякою арифметикою в одному returnвисловлюванні.
мартіно

2
@mango відповів на ваше запитання трохи кориснішим прикладом. stackoverflow.com/a/24090940/2125392
CivFan

10
Це не відповідає на питання.
Хунсу

49

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

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

Оновлення:

Ось доказ того, що гніздування їх відбувається повільніше (використовуючи Python 3.6.1), хоча, мабуть, не дуже в цьому тривіальному випадку:

setup = """
class Test(object):
    def separate(self, arg):
        some_data = self._method_b(arg)

    def _method_b(self, arg):
        return arg+1

    def nested(self, arg):

        def method_b2(self, arg):
            return arg+1

        some_data = method_b2(self, arg)

obj = Test()
"""
from timeit import Timer
print(min(Timer(stmt='obj.separate(42)', setup=setup).repeat()))  # -> 0.24479823284461724
print(min(Timer(stmt='obj.nested(42)', setup=setup).repeat()))    # -> 0.26553459700452575

Примітка. Я додав деякі selfаргументи до ваших функцій вибірки, щоб зробити їх більш схожими на реальні методи (хоча method_b2технічно все ще не є методом Testкласу). Також вкладена функція насправді викликається у цій версії, на відміну від вашої.


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

7
Так, вам знадобиться кілька дзвінків до внутрішньої функції. Якщо ви викликаєте його в циклі, або просто в кілька разів, користь від наявності локальної назви функції почне перевершувати вартість створення функції. У моїх випробуваннях це відбувається, коли ви викликаєте внутрішню функцію приблизно 3-4 рази. Звичайно, ви можете отримати ту саму вигоду (без майже великих витрат), визначивши локальну назву функції, наприклад, method_b = self._method_bа потім зателефонуйте, method_bщоб уникнути повторного пошуку атрибутів. (Буває так, що я останнім часом
займаюсь великою

3
@kindall: Так, це правда. Я змінив свій тест на час, щоб він здійснив 30 дзвінків на вторинну функцію, і результати повернулися. Я буду видаляти свою відповідь після того, як у вас з'явилася можливість побачити цю відповідь. Дякую за просвітлення.
мартіно

2
Просто хотів пояснити свій -1, тому що це інформативна відповідь: я дав цей -1, оскільки він зосереджує увагу на тривіальній різниці в продуктивності (у більшості сценаріїв створення об'єкта коду займе лише частину часу виконання функції ). Що важливо враховувати в цих випадках, чи може вбудована функція покращити читабельність коду та ремонтопридатність, що, на мою думку, це часто робиться, оскільки вам не потрібно шукати / прокручувати файли, щоб знайти відповідний код.
Blixt

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

27

Функція всередині функції зазвичай використовується для закриття .

(Існує велика суперечка з приводу того, що саме робить це закриття .)

Ось приклад використання вбудованої sum(). Він визначає startодин раз і використовує його з цього моменту:

def sum_partial(start):
    def sum_start(iterable):
        return sum(iterable, start)
    return sum_start

У вживанні:

>>> sum_with_1 = sum_partial(1)
>>> sum_with_3 = sum_partial(3)
>>> 
>>> sum_with_1
<function sum_start at 0x7f3726e70b90>
>>> sum_with_3
<function sum_start at 0x7f3726e70c08>
>>> sum_with_1((1,2,3))
7
>>> sum_with_3((1,2,3))
9

Вбудований пітон закриття

functools.partial є прикладом закриття.

З Документів python це приблизно еквівалентно:

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*(args + fargs), **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

(Кудо для @ user225312 нижче для відповіді. Я вважаю цей приклад простіше розібратися, і, сподіваюся, допоможе відповісти на коментар @ mango.)


-1 так, вони зазвичай використовуються як закриття. Тепер перечитайте питання. В основному він запитав, чи може концепція, яку він показує, використовувати для випадку b. Сказати йому, що це часто використовується для випадку a - це не погана відповідь, але неправильна для цього питання. Його цікавить, чи добре робити описане вище для інкапсуляції, наприклад.
Mayou36

@ Mayou36 Щоб бути справедливим, питання є скоріше відкритим, а не намагаючись відповісти на кожен можливий випадок, я вважав, що найкраще зосередитися на одному. Також питання не дуже зрозуміле. Наприклад, це передбачає закриття у другому прикладі, хоча це, мабуть, не те, що малося на увазі.
CivFan

@ Mayou36 «Він в основному просив , якщо поняття він показує , може бути використаний для випадку б.» Ні, питання задає, чи слід його використовувати. ОП вже знає, що його можна використовувати.
CivFan

1
добре, "якщо method_b потрібен і викликається лише з method_a, я повинен оголосити method_b всередині method_a?" цілком зрозуміло і не має нічого спільного із закриттями, правда? Так, я погоджуюсь, я використовував може так, як слід . Але це не пов'язане із закриттями ... Я просто здивований, що так багато відповідей про закриття та як їх використовувати, коли ОП задало зовсім інше питання.
Mayou36

@ Mayou36 Це може допомогти переформулювати питання про те, як ви його бачите, і відкрити ще одне питання для його вирішення.
CivFan

17

Як правило, ні, не визначайте функції всередині функцій.

Якщо у вас є справді вагомі причини. Чого ти не робиш.

Чому ні?

Що таке справді вагомою причиною для визначення функцій всередині функцій?

Коли ви насправді хочете, це закриття dingdang .


1
Ця відповідь для вас @ Mayou36.
CivFan

2
так, дякую, це відповідь, яку я шукав. Це відповідає на питання найкращим чином. Хоча воно не говорить суворо про те чи інше, воно сильно відлякує вбудовані визначення, мотивуючи причини. Ось так має бути (пітонічний :)) відповідь!
Mayou36

10

Насправді прекрасно оголосити одну функцію всередині іншої. Це особливо корисно для створення декораторів.

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


2
Це можливо, але я згоден, для цього вам потрібна вагома причина. Пітоном було б більше використовувати попередній підкреслення для функції, яка повинна була використовуватися лише у вашому класі.
chmullig

3
В межах класу, так, але як бути тільки у межах функції ? Інкапсуляція є ієрархічною.
Пол Дрейпер

@PaulDraper "Інкапсуляція є ієрархічною" - Ні! Хто це каже? Інкапсуляція є набагато ширшим принципом, ніж просто деяка спадщина.
Mayou36

7

Я знайшов це питання, тому що хотів поставити питання, чому виникає ефективність роботи, якщо використовується вкладені функції. Я проводив тести на наступні функції за допомогою Python 3.2.5 на ноутбуці Windows із Quad Core 2.5 ГГц процесором Intel i5-2530M

def square0(x):
    return x*x

def square1(x):
    def dummy(y):
        return y*y
    return x*x

def square2(x):
    def dummy1(y):
        return y*y
    def dummy2(y):
        return y*y
    return x*x

def square5(x):
    def dummy1(y):
        return y*y
    def dummy2(y):
        return y*y
    def dummy3(y):
        return y*y
    def dummy4(y):
        return y*y
    def dummy5(y):
        return y*y
    return x*x

Я вимірював наступні 20 разів, також для square1, square2 та square5:

s=0
for i in range(10**6):
    s+=square0(i)

і отримали такі результати

>>> 
m = mean, s = standard deviation, m0 = mean of first testcase
[m-3s,m+3s] is a 0.997 confidence interval if normal distributed

square? m     s       m/m0  [m-3s ,m+3s ]
square0 0.387 0.01515 1.000 [0.342,0.433]
square1 0.460 0.01422 1.188 [0.417,0.503]
square2 0.552 0.01803 1.425 [0.498,0.606]
square5 0.766 0.01654 1.979 [0.717,0.816]
>>> 

square0не має вкладеної функції, square1має одну вкладену функцію, square2має дві вкладені функції таsquare5 має п'ять вкладених функцій. Вкладені функції лише оголошуються, але не викликаються.

Отже, якщо ви визначили 5 вкладених функцій у функції, яку ви не викликаєте, то час виконання функції вдвічі перевищує функцію без вкладеної функції. Думаю, слід бути обережними при використанні вкладених функцій.

Файл Python для всього тесту, який генерує цей вихід, можна знайти у ideone .


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

-1 перечитайте питання. Хоча ви можете сказати, що це, можливо, трохи повільніше, він запитав, чи це гарна ідея робити це не головним чином завдяки продуктивності, а загальній практиці.
Mayou36

@ Mayou36 Вибачте, через три роки після опублікування запитання та відповіді я не буду.
чудо173

Гаразд, на жаль, досі немає прийнятої (або хорошої) відповіді.
Mayou36

4

Це лише принцип щодо API експозиції.

Використовуючи python, Це гарна ідея уникати експозиції API в космічному просторі (модуль або клас), функція є хорошим місцем капсулювання.

Це може бути гарною ідеєю. коли ви забезпечите

  1. внутрішня функція ТИЛЬКО використовується зовнішньою функцією.
  2. Інсайдерська функція має гарне ім'я, щоб пояснити її призначення, оскільки код говорить.
  3. код не можуть безпосередньо зрозуміти ваші колеги (або інший зчитувач кодів).

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

Просто з мого досвіду, можливо, неправильно розумію ваше питання.


4

Отже, врешті-решт, це багато в чому питання про те, наскільки розумною є реалізація python, особливо якщо внутрішня функція не є закриттям, а просто потрібною лише помічницею у функції.

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

І коли внутрішня функція - це закриття, яке також може трохи допомогти в ясності, навіть якщо ця функція не повертається із функції, що містить, для використання в інших місцях.

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

Не робіть передчасну оптимізацію просто за допомогою "внутрішніх функцій BAD" протягом усього написаного вами python-коду. Будь ласка.


Як показують інші відповіді, ви насправді не маєте хітів на продуктивність.
Mayou36

1

Цілком нормально це робити так, але якщо вам не потрібно використовувати закриття або повернути функцію, яку я, мабуть, поставив би на рівні модуля. Я уявляю, що в другому прикладі коду ви маєте на увазі:

...
some_data = method_b() # not some_data = method_b

в іншому випадку деякими_даними буде функція.

Наявність його на рівні модуля дозволить іншим функціям використовувати method_b (), і якщо ви використовуєте для документації щось на зразок Sphinx (та autodoc), це дозволить також документувати method_b.

Ви також можете розглянути можливість простого розміщення функціональності двома методами у класі, якщо ви робите щось, що може бути представленим об'єктом. Це також містить логіку, якщо це все, що ви шукаєте.


1

Зробіть щось на кшталт:

def some_function():
    return some_other_function()
def some_other_function():
    return 42 

якби ти запустив, some_function()то він би запустився some_other_function()і повертається 42.

EDIT: Спочатку я говорив, що не слід визначати функцію в іншому, але вказувалося, що іноді це практично можливо.


Я ціную зусилля тих, хто ви вище доклали, щоб відповісти, але ваш був прямим і суттєвим. Приємно.
C0NFUS3D

1
Чому? Чому б вам цього не робити? Хіба це не чудова інкапсуляція? Я не пропускаю жодних аргументів. -1
Mayou36

@ Mayou36 Я не знав, що таке капсулювання на момент написання коментаря, і я не знаю, що це зараз. Я просто думав, що це не добре робити. Чи можете ви пояснити, чому було б вигідно визначити функцію всередині іншої, а не просто визначати її поза нею?
mdlp0716

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

0

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

А) Використання функцій без глобалів

def calculate_salary(employee, list_with_all_employees):
    x = _calculate_tax(list_with_all_employees)

    # some other calculations done to x
    pass

    y = # something 

    return y

def _calculate_tax(list_with_all_employees):
    return 1.23456 # return something

Б) Використання функцій з глобалами

_list_with_all_employees = None

def calculate_salary(employee, list_with_all_employees):

    global _list_with_all_employees
    _list_with_all_employees = list_with_all_employees

    x = _calculate_tax()

    # some other calculations done to x
    pass

    y = # something

    return y

def _calculate_tax():
    return 1.23456 # return something based on the _list_with_all_employees var

C) Використання функцій всередині іншої функції

def calculate_salary(employee, list_with_all_employees):

    def _calculate_tax():
        return 1.23456 # return something based on the list_with_a--Lemployees var

    x = _calculate_tax()

    # some other calculations done to x
    pass
    y = # something 

    return y

Розчин C) дозволяє використовувати змінні в області зовнішньої функції, не маючи необхідності оголошувати їх у внутрішній функції. Може бути корисним у деяких ситуаціях.


0

Функція У функціональному пітоні

def Greater(a,b):
    if a>b:
        return a
    return b

def Greater_new(a,b,c,d):
    return Greater(Greater(a,b),Greater(c,d))

print("Greater Number is :-",Greater_new(212,33,11,999))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.