web-dev-qa-db-ja.com

2回繰り返す必要がある何かのための関数を書くことは常にベストプラクティスですか?

私自身、2回以上行う必要があるときに関数を書くのを待ちきれません。しかし、2回しか表示されないものに関しては、少し注意が必要です。

3行以上必要なコードの場合は、関数を記述します。しかし、次のようなことに直面した場合:

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)

物事はトリッキーになります、いいえ?今決めるのは難しいです。

これについてどう思いますか?

132
Zen

関数を分割することを決定する際の1つの要素ですが、何かが繰り返される回数が唯一の要素であってはなりません。多くの場合、実行されるだけのものに対して関数を作成することは理にかなっていますonce。通常、次の場合に関数を分割します。

  • 個々の抽象化レイヤーを単純化します。
  • スプリットオフ関数にはわかりやすい意味のある名前を付けているため、通常、何が起こっているのかを理解するために抽象化レイヤー間を移動する必要はありません。

あなたの例はその基準を満たしていません。あなたはワンライナーからワンライナーに移行します、そして名前は本当に明確さの点であなたに何も買いません。とはいえ、チュートリアルや学校の割り当て以外では、単純な機能はまれです。ほとんどのプログラマーは、逆の方法で誤解する傾向があります。

201
Karl Bielefeldt

重複がaccidentalではなくintentionalの場合のみ。

または、別の言い方をすると:

あなたが将来的に共進化することを期待する場合にのみ。


理由は次のとおりです。

たまに、2つのコードがhappenになるだけで、お互いに何の関係もないのに同じになることがあります。その場合、あなたはmustそれらを組み合わせる衝動に抵抗します。誰かがそれらの1つで次回メンテナンスを実行するとき、人はしない変更が以前は存在しなかった呼び出し元に伝播することを期待し、その結果、その関数が壊れる可能性があります。したがって、コードのサイズを小さくするように思われるときではなく、意味のある場合にのみコードを分解する必要があります。

経験則:

コードが新しいデータのみを返し、既存のデータを変更しないか、他の副作用がない場合は、別の関数として分解しても安全です。 (関数の意図されたセマンティクスを完全に変更せずに破損を引き起こすシナリオは想像できません。その時点で、関数名またはシグネチャも変更されます。その場合は、とにかく注意する必要があります。 )

104
user541686

いいえ、常にベストプラクティスとは限りません。

他のすべてのものが等しい、線形の行ごとのコードであれば、関数呼び出しをジャンプするよりも読みやすくなります。重要な関数呼び出しは常にパラメーターを使用するため、それらすべてを並べ替えて、関数呼び出しから関数呼び出しへのメンタルコンテキストジャンプを行う必要があります。不明瞭である十分な理由(必要なパフォーマンスの向上を得るなど)がある場合を除いて、常にコードの明確さを向上させることをお勧めします。

では、なぜコードが個別のメソッドにリファクタリングされるのでしょうか。 モジュール性を改善するため。個々のメソッドの背後に重要な機能を収集し、意味のある名前を付けるため。それを達成していない場合は、これらの個別のメソッドは必要ありません。

60
Robert Harvey

あなたの特定の例では、関数を作るのはやり過ぎに見えるかもしれません。代わりに私は質問します。この特定の挨拶が将来変更される可能性はありますか?どうして?

関数は、機能をラップして再利用を容易にするだけでなく、変更を容易にするためにも使用されます。要件が変更された場合、コピー貼り付けされたコードを探し出して手動で変更する必要がありますが、関数ではコードを1回だけ変更する必要があります。

あなたの例は、関数を介さずにこれから利益を得るでしょう-ロジックはほとんど存在しないので-しかしおそらくGREET_OPENING 絶え間ない。次に、この定数をファイルからロードして、たとえばプログラムをさまざまな言語に簡単に適合させることができます。これは大まかなソリューションであり、より複雑なプログラムではi18nを追跡するためのより洗練された方法が必要になる可能性がありますが、やはり要件と実行する作業に依存します。

それは結局のところ、可能な要件についてであり、将来の自己の仕事を容易にするために事前に計画を立てることです。

25
Svalorzen

個人的に私は3つのルールを採用しました。これは [〜#〜] yagni [〜#〜] を呼び出して正当化します。そのコードを書いたら何かをする必要がある場合は、2回コピー/貼り付けするだけです(はい、コピー/貼り付けを許可しただけです!)もう必要ないので、同じことをする必要がある場合もう一度言います。それから、そのチャンクをリファクタリングして独自のメソッドに抽出します。私は、自分自身にwill再び必要とすることを示しました。

私はカールビーレフェルトとロバートハーベイの発言に同意します。彼らが言っていることについての私の解釈は、最優先のルールは読みやすさです。コードが読みやすくなり、関数の作成を検討する場合は、 [〜#〜] dry [〜#〜][〜#〜] slap [〜 #〜] 。関数の抽象化レベルの変更を避けることができれば、頭の中で管理しやすいので、関数間をジャンプしない(関数の名前を読むだけでは関数の機能を理解できない場合)と、スイッチの数が少なくなります。精神プロセス。
同様に、関数と_print "Hi, Tom"_などのインラインコードのコンテキストを切り替える必要がないので、この場合はImight関数を抽出しますPrintNames()iff私の全体的な関数の残りは、ほとんどが関数呼び出しでした。

22
Simon Martin

それが明確になることはめったにないので、オプションを比較検討する必要があります。

  • 期限(書き込みサーバールームをできるだけ早く修正)
  • コードの可読性(どちらにしても選択に影響する可能性があります)
  • 共有ロジックの抽象化レベル(上記に関連)
  • 再利用の要件(つまり、まったく同じロジックが重要である、または今すぐ便利である)
  • 共有の難しさ(ここにpython輝く、以下を参照)

C++では、私は通常3の規則に従います(つまり、同じものが必要な3回目は、適切に共有可能な部分にリファクタリングします)が、経験から、スコープについて詳しく知っているので、最初にその選択を行う方が簡単です。使用しているソフトウェアとドメイン。

ただし、Pythonでは、ロジックを繰り返すのではなく、再利用するのがかなり軽量です。他の多くの言語(少なくとも歴史的には)よりも、IMOのほうがはるかに軽量です。

したがって、たとえばローカル引数からリストを作成して、ロジックをローカルで再利用することを検討してください。

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

または、ローカル関数を作成します(おそらく2番目の例はそれです)。これにより、ロジックが明示的に再利用されますが、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の方が優れており、1回の呼び出し元のみが使用するグローバル/クラス関数を宣言するよりも煩雑さが少なくなります(2回ですが)。

13
Macke

すべてのベストプラクティスには特定の理由があります。そのような質問に答えるために参照できます。将来の1つの潜在的な変更が他のコンポーネントへの変更も示唆する場合は、常に波紋を回避する必要があります。

だからあなたの例では:あなたが標準の挨拶を持っていると仮定し、それがすべての人にとって同じであることを保証したい場合は、

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

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

それ以外の場合は、標準の挨拶が変わった場合、2か所で注意を払い、標準の挨拶を変更する必要があります。

ただし、「こんにちは」が偶然同じであり、挨拶の1つを変更しても必ずしも他の挨拶が変更されるわけではない場合は、別々にしてください。したがって、次の変更の可能性が高いと考える論理的な理由がある場合は、それらを分離してください。

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

2番目の部分では、関数に「パッケージ化」する量を決定することは、おそらく2つの要因に依存します。

  • 読みやすくするためにブロックを小さく保つ
  • 変更が少数のブロックのみで発生するように、ブロックを十分な大きさにしてください。呼び出しパターンを追跡するのが難しいので、あまりクラスタリングする必要はありません。

理想的には、変更が発生した場合、「大きなブロック内のコードどこかを変更する必要がある」ではなく、「次のブロックのほとんどを変更する必要がある」と考えるでしょう。

9
Gerenuk

プロシージャを作成する理由はあると思います。プロシージャを作成する理由はいくつかあります。私が最も重要だと思うものは次のとおりです。

  1. 抽象化
  2. モジュール性
  3. コードナビゲーション
  4. 一貫性の更新
  5. 再帰
  6. テスト

抽象化

手順は、アルゴリズムの特定のステップの詳細から一般的なアルゴリズムを抽象化するために使用できます。適切にコヒーレントな抽象化を見つけることができれば、アルゴリズムが何をするかについて私が考える方法を構築するのに役立ちます。たとえば、リストの表現を必ずしも考慮する必要なく、リストで機能するアルゴリズムを設計できます。

モジュール性

プロシージャを使用して、コードをモジュールに編成できます。多くの場合、モジュールは個別に処理できます。たとえば、個別に構築およびテストされます。

適切に設計されたモジュールは、通常、いくつかの一貫した意味のある機能単位をキャプチャします。したがって、それらは多くの場合、異なるチームによって所有され、完全に代替に置き換えられるか、異なるコンテキストで再利用されます。

コードナビゲーション

大規模なシステムでは、特定のシステム動作に関連付けられたコードを見つけるのは困難な場合があります。ライブラリ、モジュール、およびプロシージャ内のコードの階層的な編成は、この課題に役立ちます。特に、a)意味のある予測可能な名前で機能を整理しようとする場合b)類似した機能に関連する手順を互いに近くに配置します。

整合性の更新

大規模なシステムでは、特定の動作変更を実現するために変更が必要なすべてのコードを見つけるのは困難な場合があります。プロシージャを使用してプログラムの機能を整理すると、これが簡単になります。特に、プログラムの機能の各ビットが1回だけ表示され、まとまりのある手順のグループに含まれている場合、(私の経験では)どこかで更新を行ったり、一貫性のない更新を行ったりする可能性が低くなります。

プログラムの機能性と抽象化に基づいて手順を編成する必要があることに注意してください。現時点で2ビットのコードが同じであるかどうかには基づいていません。

再帰

再帰を使用するには、プロシージャを作成する必要があります

テスト中

通常、手順は互いに独立してテストできます。通常、プロシージャの最初の部分を2番目の部分の前に実行する必要があるため、プロシージャの本体のさまざまな部分を個別にテストすることはより困難です。この観察は、プログラムの動作を指定/検証する他のアプローチにも当てはまります。

結論

これらのポイントの多くは、プログラムの包括性に関連しています。手順は、ドメイン内の問題とプロセスについて整理、書き込み、および読み取るための、ドメイン固有の新しい言語を作成する方法であると言えます。

5
flamingpenguin

名前付き定数と関数の最も重要な側面は、入力の量を減らすほどではなく、使用されるさまざまな場所に「アタッチ」することです。あなたの例に関して、4つのシナリオを検討してください:

  1. 同じ方法で両方の挨拶を変更する必要があります。 Bonjour, TomおよびBonjour, Mary]。

  2. 1つの挨拶を変更する必要がありますが、他の挨拶はそのままにします[例: Hi, TomおよびGuten Tag, Mary]。

  3. 両方の挨拶を異なる方法で変更する必要があります[例: Hello, TomおよびHowdy, Mary]。

  4. どちらの挨拶も変更する必要はありません。

どちらの挨拶も変更する必要がない場合は、どちらの方法を採用してもかまいません。共有機能を使用すると、グリーティングを変更するとすべて同じように変更されるという自然な効果があります。すべての挨拶が同じように変化するのであれば、それは良いことです。ただし、必要ない場合は、各人の挨拶を個別にコーディングまたは指定する必要があります。共通の関数または仕様を使用するために行われた作業は、元に戻す必要があります(そもそも行わない方がよいでしょう)。

確かに、将来を予測することは常に可能であるとは限りませんが、両方の挨拶を一緒に変更する必要がある可能性が高いと考える理由がある場合、それは一般的なコード(または名前付き定数)を使用することを支持する要因になります;どちらかまたは両方を変更して、異なるようにする必要がある可能性が高いと信じる理由がある場合、それは一般的なコードを使用しようとすることに対する要因になります。

5
supercat

あなたが与えるケースのどちらもリファクタリングする価値があるとは思われません。

どちらの場合も、明確かつ簡潔に表現された操作を実行しており、一貫性のない変更が行われる危険はありません。

私は常にコードを分離する方法を探すことをお勧めします:

  1. 区別できる、識別可能な意味のあるタスクがありますand

  2. どちらか

    a。タスクは表現するのが複雑です(使用可能な機能を考慮に入れて)or

    b。タスクは複数回実行され、両方の場所で同じように変更されることを確認する方法が必要です。

条件1が満たされていない場合、コードに適切なインターフェースが見つかりません。強制しようとすると、パラメーターのロード、返したい複数の物、多くのコンテキストロジックが発生し、おそらく可能になります。良い名前が見つかりません。最初に考えているインターフェースを文書化してみてください。これは、それが機能の適切なバンドルであるという仮説をテストする良い方法であり、コードを書くときにすでに仕様化されて文書化されているという追加のボーナスが付属しています!

2aは2bなしで可能です。それが1回しか使用されないことがわかっている場合でも、コードをフローから取り出したことがあります。それは、フローを別の場所に移動して1行で置き換えることで、呼び出されたコンテキストが突然読みやすくなるためです。 (特に、言語がたまたま実装を困難にしている簡単な概念である場合)。抽出された関数を読み取るときに、ロジックが開始および終了する場所と、それが何を行うかも明確です。

2aがなくても2bが可能:トリックは、どのような変更が可能性が高いかを感じさせることです。会社のポリシーがいつでも「こんにちは」から「こんにちは」に変わるか、支払い要求やサービス停止の謝罪を送る場合、挨拶がより正式な口調に変わる可能性が高いですか?不明な外部ライブラリを使用していて、それを別のライブラリとすばやく交換できるようにしたい場合があります。機能が変わらなくても実装が変更される可能性があります。

ただし、ほとんどの場合、2aと2bが混在しています。また、コードのビジネス価値、コードが使用される頻度、十分に文書化され理解されているかどうか、テストスイートの状態などを考慮して、判断を下さなければなりません。

同じロジックが2回以上使用されていることに気付いた場合は、必ずリファクタリングを検討してください。ほとんどの言語でnインデントレベルを超える新しいステートメントを開始する場合、それはもう1つのわずかに重要性の低いトリガーです(nの値を選択して、指定した言語に慣れます: Pythonは6になる可能性があります)。

これらのことを考えて、大きなスパゲッティコードのモンスターを倒す限り、問題はありません。リファクタリングの細かさを心配する必要はありません。テストやドキュメンテーションのようなものがうまくいきました。それを行う必要がある場合は、時間がわかります。

4
user52889

一般的に私はRobert Harveyに同意しますが、機能を機能に分割するケースを追加したいと思いました。読みやすさを向上させるため。ケースを考えてみましょう:

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

これらの関数呼び出しは他の場所では使用されていませんが、3つの関数すべてがかなり長く、ループが入れ子になっている場合などは、読みやすさが大幅に向上します。

3
UldisK

私はここで言及されていないリファクタリングについて何かを信じるようになりました。私はすでにここに多くの答えがあることを知っていますが、これは新しいと思います。

DRY用語が登場する前から、私は冷酷なリファクタラーであり、強い信奉者でした。主に、大きなコードベースを頭の中で維持するのに苦労していることと、DRYコーディングです。C&Pコーディングについては何も楽しめません。実際、私にとっては苦痛であり、ひどく遅いです。

ことは、DRYを主張することで、他の人が使用することはめったにないいくつかのテクニックで多くの練習を私に与えました。多くの人がJavaはDRYを作るのは難しいか不可能ですが、実際には彼らはやってみません。

あなたの例にいくぶん似ている、昔からの1つの例。人々は、Java 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テクニックと構築ツールをできるだけ練習しなかったと、仕事を楽しむことができないと思います。コピーアンドペーストプログラミングを行う必要がある場合は、それを採用します。

だから私のポイントは:

  • うまくリファクタリングする方法を知らない限り、コピーと貼り付けはより多くの時間を費やします。
  • あなたはそれを行うことによってうまくリファクタリングすることを学びます-最も困難で些細な場合でもDRYを主張することによって。
  • あなたはプログラマーであり、コードをDRYにする小さなツールが必要な場合は、それをビルドします。
2
Bill K

適用しなければならない厳格な規則はありません。使用される実際の関数によって異なります。単純な名前の印刷では、関数を使用しませんが、それが数学の合計である場合は、別の話になります。次に、2回しか呼び出されない場合でも関数を作成します。これは、変更された場合でも、数学の合計が常に同じになるようにするためです。別の例では、任意の形式の検証を行う場合は関数を使用します。したがって、この例では、名前を5文字より長くチェックする必要がある場合は、関数を使用して常に同じ検証が行われるようにします。

ですから、「2行以上必要なコードについては、funcを書かなければならない」と述べたときに、あなた自身の質問に答えたと思います。一般的には関数を使用しますが、関数を使用して追加される値があるかどうかを判断するために、独自のロジックも使用する必要があります。

2
W.Walford

一般的に、何かを関数に分割すると、コードの可読性が向上します。または、繰り返されている部分が何らかの理由でシステムのコアである場合は、上記の例で、ロケールに応じてユーザーに挨拶する必要がある場合は、別個の挨拶機能を使用するのが理にかなっています。

1
user90766

関数を使用して抽象化を作成することに関する@RobertHarveyへの@DocBrownのコメントの帰結として:その背後にあるコードよりもはるかに簡潔または明確ではない十分に有益な関数名を思い付かない場合、あまり抽象化していません。それを関数にする他の正当な理由がない限り、そうする必要はありません。

また、関数名がその完全なセマンティクスを取得することはめったにないので、慣れ親しんでも十分に使用されていない場合は、その定義を確認する必要があるかもしれません。特に関数型言語以外では、副作用があるかどうか、発生する可能性のあるエラーとその対処方法、時間の複雑さ、リソースの割り当てや解放の有無、スレッドかどうかを知る必要がある場合があります。安全。

もちろん、ここでは単純な関数のみを考慮して定義していますが、それはどちらの方向にも当てはまります。そのような場合、インライン化によって複雑さが増すことはありません。さらに、読者は見ている限り、それが単純な機能であることに気付かないでしょう。 IDEが定義にハイパーリンクしている場合でも、視覚的なジャンプは理解を妨げます。

一般的に言えば、コードをより小さなより小さな関数に再帰的に因数分解すると、最終的には関数が独立した意味を持たなくなるところに行きます。周囲のコードによって。類推として、それを絵のように考えてください。ズームインしすぎると、何を見ているのかわかりません。

1
sdenham