Если функция 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)

Или я должен избегать этого?

nukl
источник
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

Это то, что ты искал? Это называется закрытием .

user225312
источник
14
Это намного лучшее объяснение. Я удалил свой ответ
pyfunc
почему бы просто не сделать def sum (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
На самом деле он не полностью компилирует внутреннюю функцию каждый раз, когда вызывается внешняя функция, хотя ему действительно нужно создать функциональный объект, который занимает немного времени. С другой стороны, имя функции становится локальным, а не глобальным, поэтому вызов функции происходит быстрее. В моих испытаниях по времени это в основном мойка большую часть времени; это может быть даже быстрее с внутренней функцией, если вы вызываете ее много раз.
любезно
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

Встроенное закрытие Python

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

CivFan
источник
-1 да, они обычно используются как замыкания. Теперь перечитайте вопрос. Он в основном спросил, можно ли использовать концепцию, которую он показывает, для случая b. Сказать ему, что это часто используется для случая а, - не плохой ответ, но неправильный ответ на этот вопрос. Он интересуется, полезно ли, например, делать это для инкапсуляции.
Mayou36
@ Mayou36 Чтобы быть справедливым, вопрос довольно открытый - вместо того, чтобы пытаться ответить на каждый возможный случай, я подумал, что лучше сосредоточиться на одном. Также вопрос не очень понятен. Например, это подразумевает закрытие во втором примере, хотя это, вероятно, не то, что имелось в виду.
CivFan
@ Mayou36 «Он в основном просил , если понятие он показывает , может быть использован для случая б.» Нет, вопрос спрашивает, нужно ли это использовать. ОП уже знает, что его можно использовать.
CivFan
1
хорошо, "если method_b требуется для и вызывается только из method_a, я должен объявить method_b внутри method_a?" совершенно ясно и не имеет ничего общего с замыканиями, верно? Да, я согласен, я использовал баллончик в пути должно . Но это не имеет отношения к замыканиям ... Я просто удивлен, что так много людей ответили о замыканиях и их использовании, когда ОП задал совершенно другой вопрос.
Mayou36
@ Mayou36 Это может помочь переформулировать вопрос, каким вы его видите, и открыть другой вопрос, чтобы ответить на него.
CivFan
17

Как правило, нет, не определяйте функции внутри функций.

Если у вас нет действительно веской причины. Что вы не делаете.

Почему нет?

Что является действительно хорошей причиной для определения функций внутри функций?

Когда то, что вы действительно хотите, это закрытие дингданга .

CivFan
источник
1
Этот ответ для тебя @ Mayou36.
CivFan
2
да, спасибо, это ответ, который я искал. Это отвечает на вопрос наилучшим образом. Хотя он не говорит строго одно или другое, он сильно препятствует встроенным определениям, указывая причины. Вот как должен быть (pythonic :)) ответ!
Mayou36
10

На самом деле нормально объявить одну функцию внутри другой. Это особенно полезно при создании декораторов.

Однако, как правило, если функция сложная (более 10 строк), лучше объявить ее на уровне модуля.

vz0
источник
2
Это возможно, но я согласен, вам нужна веская причина для этого. Было бы лучше, если бы Python использовал предыдущее подчеркивание для функции, предназначенной для использования только в вашем классе.
chmullig
3
Да, внутри класса, но как насчет только внутри функции ? Инкапсуляция является иерархической.
Пол Дрэйпер
@PaulDraper "Инкапсуляция иерархическая" - нет! Кто так говорит? Инкапсуляция - это гораздо более широкий принцип, чем просто наследование.
Mayou36
7

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

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 .

miracle173
источник
5
Сравнение, которое вы делаете, не очень полезно. Это все равно что добавить в функцию фиктивные операторы и сказать, что это медленнее. Пример martineau фактически использует инкапсулированные функции, и я не замечаю никакой разницы в производительности, выполняя его пример.
Кон Псих
-1 пожалуйста перечитайте вопрос. Хотя вы можете сказать, что это может быть немного медленнее, он спросил, стоит ли это делать, не в основном из-за производительности, а из-за общей практики.
Mayou36
@ Mayou36 извините, через три года после того, как вопрос и ответ были опубликованы, я не буду.
miracle173
Хорошо, к сожалению, до сих пор нет принятого (или хорошего) ответа.
Mayou36
4

Это просто принцип работы API-интерфейсов.

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

Это может быть хорошей идеей. когда вы обеспечите

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

Несмотря на то, что злоупотребление этой техникой может вызвать проблемы и подразумевает недостаток дизайна.

Просто из моего опыта, Может быть, неправильно понял ваш вопрос.

chao787
источник
4

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

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

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

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

Не делайте преждевременной оптимизации, просто используя «внутренние функции BAD» во всем написанном вами коде Python. Пожалуйста.


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

Это нормально, но если вам не нужно использовать замыкание или возвращать функцию, которую я, вероятно, поместил бы на уровне модуля. Я представляю, что во втором примере кода вы имеете в виду:

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

в противном случае some_data будет функцией.

Наличие этого на уровне модуля позволит другим функциям использовать method_b (), и если вы используете что-то вроде Sphinx (и autodoc) для документации, это также позволит вам документировать method_b.

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

mikelikespie
источник
1

Сделать что-то вроде:

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

если бы вы запустили, some_function()он запустился бы some_other_function()и вернул 42.

РЕДАКТИРОВАТЬ: Я первоначально заявил, что вы не должны определять функцию внутри другого, но было отмечено, что это иногда целесообразно делать.

mdlp0716
источник
Я ценю усилия, которые вы приложили к их ответам, но ваши были прямыми и точными. Ницца.
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) позволяет использовать переменные в области действия внешней функции без необходимости объявлять их во внутренней функции. Может быть полезно в некоторых ситуациях.

Elmex80s
источник
0

Функция В функции Python

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))
user12069064
источник