Всегда ли лучше писать функцию для всего, что нужно повторить дважды?

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

Для кода, которому требуется более двух строк, я напишу функцию. Но когда вы сталкиваетесь с такими вещами, как:

print "Hi, Tom"
print "Hi, Mary"

Я не решаюсь писать:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

Второй кажется слишком много, не так ли?


Но что, если у нас есть:

for name in vip_list:
    print name
for name in guest_list:
    print name

И вот альтернатива:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

Все становится сложным, нет? Сейчас трудно решить.

Каково ваше мнение об этом?

129 голосов | спросил Zen 13 Jam1000000amTue, 13 Jan 2015 07:15:50 +030015 2015, 07:15:50

15 ответов


198

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

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

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

ответил Karl Bielefeldt 13 Jam1000000amTue, 13 Jan 2015 08:47:46 +030015 2015, 08:47:46
104

Только если дублирование преднамеренное , а не случайное .

Или, по-другому:

Только если бы вы ожидали их совместное развитие в будущем.


Вот почему:

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

Общее правило:

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

ответил Mehrdad 14 Jam1000000amWed, 14 Jan 2015 11:38:25 +030015 2015, 11:38:25
60

Нет, это не всегда лучшая практика.

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

Так почему же код реорганизуется в отдельные методы? Чтобы улучшить модульность. Чтобы собрать значительную функциональность за индивидуальным методом и дать ему значимое имя. Если вы этого не достигнете, вам не нужны эти отдельные методы.

ответил Robert Harvey 13 Jam1000000amTue, 13 Jan 2015 07:41:57 +030015 2015, 07:41:57
25

В вашем конкретном примере создание функции может показаться излишним; вместо этого я задал бы вопрос: возможно ли, что это особое приветствие изменится в будущем? Как это так?

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

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

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

ответил Svalorzen 13 Jpm1000000pmTue, 13 Jan 2015 13:31:37 +030015 2015, 13:31:37
22

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

Я согласен с тем, что сказал Карл Билефельдт и Роберт Харви, и моя интерпретация того, что они говорят, заключается в том, что преобладающее правило является читабельностью. Если это облегчит чтение кода, подумайте о создании функции, помните о таких вещах, как DRY и SLAP . Если я смогу избежать изменения уровня абстракции в своей функции, тогда я нахожу, что мне легче управлять в голове, поэтому не перепрыгивать между функциями (если я не могу понять, что делает функция просто, читая ее имя) означает меньше переключателей в моей психические процессы.
Точно так же не нужно переключать контекст между функциями и встроенным кодом, например print "Hi, Tom" работает для меня, в этом случае я мог бы извлечь функцию PrintNames() iff остальная часть моей общей функции была в основном вызовом функций.

ответил Simon Martin 13 Jpm1000000pmTue, 13 Jan 2015 13:24:13 +030015 2015, 13:24:13
13

Это очень редко, поэтому вам нужно взвешивать варианты:

  • Крайний срок (исправить запись серверной комнаты как можно скорее)
  • Чтение кода (может повлиять на выбор в любом случае)
  • Уровень абстракции общей логики (связанный с выше)
  • Требование повторного использования (т. е. имеет то же самое логическое значение или просто удобно сейчас)
  • Сложность совместного использования (здесь есть свет python, см. ниже).

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

Однако, в Python довольно легко повторять, а не повторять логику. Более того, чем на многих других языках (по крайней мере исторически), ИМО.

Итак, рассмотрите возможность повторного использования логики локально, например, путем создания списка из локальных аргументов:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Вы можете использовать кортеж кортежей и разбивать его непосредственно на цикл for, если вам нужно несколько аргументов:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

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

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

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

Либо лучше, чем повторять одну и ту же логику, IMO, а также меньше загромождать, чем объявлять глобальную /классную функцию, которая используется только одним вызывающим (хотя и дважды).

ответил Macke 13 Jam1000000amTue, 13 Jan 2015 09:16:40 +030015 2015, 09:16:40
9

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

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

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

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

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

print "Hi, Tom"
print "Hello, Mary"

Для вашей второй части, чтобы решить, сколько «пакетов» в функции, вероятно, зависит от двух факторов:

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

В идеале, когда происходит изменение, вы подумаете: «Мне нужно изменить большую часть следующего блока», а не «Мне нужно изменить код где-то внутри большого блока».

ответил Gerenuk 13 Jpm1000000pmTue, 13 Jan 2015 16:53:10 +030015 2015, 16:53:10
5

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

  1. Необходимо одинаково изменить оба приветствия (например, к Bonjour, Tom и Bonjour, Mary].

  2. Необходимо изменить одно приветствие, но оставить другое как есть [например. Hi, Tom и Guten Tag, Mary].

  3. Необходимо изменить оба приветствия по-разному (например, Hello, Tom и Howdy, Mary].

  4. Ни одно приветствие никогда не нуждается в изменении.

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

Конечно, не всегда можно предсказать будущее, но если у кого есть основания полагать, что более вероятно, что оба приветствия должны будут измениться вместе, это будет фактором в пользу использования общего кода (или named constant); если у вас есть основания полагать, что более вероятно, что один или оба могут измениться, чтобы они отличались, это было бы фактором против попытки использовать общий код.

ответил supercat 13 Jpm1000000pmTue, 13 Jan 2015 20:58:10 +030015 2015, 20:58:10
4

Ни один из случаев, которые вы даете, кажется, стоит рефакторинга.

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

Я рекомендую вам всегда искать способы изолировать код, где:

  1. Существует идентифицируемая, значимая задача, которую вы можете выделить, и

  2. Либо

    а. задача сложна для выражения (с учетом доступных вам функций) или

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

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

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

2b возможно без 2a: трюк имеет ощущение, какие изменения более вероятны. Скорее всего, политика вашей компании изменится с «Привет» повсюду на «Привет» все время или ваше приветствие изменится на более формальный тон, если вы отправляете заявку на оплату или извинение за провал службы? Иногда это может быть из-за того, что вы используете внешнюю библиотеку, о которой вы не уверены, и хотите, чтобы ее можно было быстро заменить на другую: реализация может измениться, даже если функциональность не работает.

Чаще всего, у вас будет смесь 2a и 2b. И вам придется использовать свое мнение, принимая во внимание деловую ценность кода, частоту, которую он использует, хорошо ли он документирован и понятен, состояние вашего тестового набора и т. Д.

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

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

ответил user52889 14 Jam1000000amWed, 14 Jan 2015 01:08:15 +030015 2015, 01:08:15
4

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

  1. абстракция
  2. Модульность
  3. Навигация кода
  4. согласованность обновлений
  5. рекурсии
  6. Тестирование

Абстрактные

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

Модульность

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

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

Навигация кода

В больших системах поиск кода, связанного с конкретным поведением системы, может быть сложным. Иерархическая организация кода внутри библиотек, модулей и процедур может помочь в решении этой проблемы. В частности, если вы пытаетесь: a) организовать функциональность под значащими и предсказуемыми именами; b) размещать процедуры, относящиеся к аналогичной функциональности, рядом друг с другом.

Обновление согласованности

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

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

Рекурсия

Использование рекурсии требует создания процедур

Тестирование

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

Заключение

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

ответил flamingpenguin 15 Jpm1000000pmThu, 15 Jan 2015 18:10:22 +030015 2015, 18:10:22
3

В общем, я согласен с Робертом Харви, но хотел добавить случай, чтобы разделить функциональность на функции. Улучшить читаемость. Рассмотрим случай:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

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

ответил UldisK 13 Jam1000000amTue, 13 Jan 2015 11:12:55 +030015 2015, 11:12:55
2

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

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

ответил W.Walford 13 Jam1000000amTue, 13 Jan 2015 07:52:07 +030015 2015, 07:52:07
2

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

Я был безжалостным рефактором и сильным сторонником СУХОЙ, так как до того, как возникли условия. В основном это связано с тем, что у меня проблемы с хранением большой кодовой базы в голове и отчасти потому, что мне нравится СУХОЕ кодирование, и мне не нравится что-либо о кодировании C & P, на самом деле это болезненно и ужасно медленно для меня.

Дело в том, что настаивание на DRY дало мне много практики в некоторых методах, которые я редко вижу в других. Многие люди настаивают на том, что Java трудно или невозможно сделать СУХОЙ, но на самом деле они просто не пытаются.

Один пример из давней давности, который несколько похож на ваш пример. Люди склонны думать, что создание GUI GUI сложно. Конечно, если вы код такой:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Любой, кто считает, что это просто сумасшедший, абсолютно прав, но это не ошибка Java - код никогда не должен быть написан таким образом. Эти вызовы методов - это функции, предназначенные для переноса в другие системы. Если вы не можете найти такую ​​систему, постройте ее!

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

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Как только вы определили свои отношения каким-то образом, которые являются краткими и описательными, вы пишете метод, чтобы справиться с ним - в этом случае вы перебираете методы класса, переданного и создаете дерево, затем используйте это для создайте свои меню и действия, а также (очевидно) отобразите меню. У вас не будет дублирования, и ваш метод будет многократно использоваться ... и, вероятно, легче писать, чем было бы большим количеством меню, и если вы действительно наслаждаетесь программированием, у вас было бы намного веселее. Это не сложно - метод, который вам нужно написать, вероятно, меньше строк, чем создание вашего меню вручную!

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

Вы получаете «бесплатную» практику просто с помощью кодирования - шансы на то, что после того, как вы добьетесь этого, вы обнаружите, что кодирование чего-то СУХОЕ (включая написание инструмента многократного использования) происходит быстрее, чем копирование и вставка, а также все ошибки , дублированные ошибки и сложные изменения, вызывающие тип кодирования.

Я не думаю, что смогу наслаждаться своей работой, если бы я не практиковал СУХИЕ техники и инструменты для строительства, насколько мог. Если бы мне пришлось заплатить, чтобы не копировать и вставлять программы, я бы взял его.

Итак, мои пункты:

  • Копировать & Вставка стоит больше времени, если вы не знаете, как правильно реорганизовать.
  • Вы учитесь хорошо реорганизовать, делая это - настаивая на DRY даже в самых сложных и тривиальных случаях.
  • Вы программист, если вам нужен небольшой инструмент для создания кода DRY, его постройте.
ответил Bill K 15 Jam1000000amThu, 15 Jan 2015 07:41:02 +030015 2015, 07:41:02
1

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

ответил user90766 4 MarpmWed, 04 Mar 2015 16:26:35 +03002015-03-04T16:26:35+03:0004 2015, 16:26:35
1

Как следствие комментария @ DocBrown к @RobertHarvey об использовании функций для создания абстракций: если вы не можете найти адекватно информативное имя функции, которое не является значительно более кратким или ясным, чем код, стоящий за ним, вы не отвлекаясь. Отсутствие каких-либо других веских оснований для того, чтобы сделать его функцией, нет необходимости делать это.

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

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

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

ответил sdenham 14 Jpm1000000pmWed, 14 Jan 2015 19:53:12 +030015 2015, 19:53:12

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132