プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ifのネストが深くなってしまう

一般的にネストは深くなりすぎないほうがいいといわれています。
何段以上が「深い」とするかはコーディング規約に依存しますが、多数の条件を調べようとすると「深くせざるを得ない」と思っていました。

例えとして、複数の条件にマッチするTweetを探したいときを例に示します。
ここではTwitterの検索にはTweepyを使用しています。

条件
・特定のアカウントでない
・NG wordが含まれていない
・RT数が30以上
・お気に入れられが30以上
・日本語
まず思いつくコードは以下のようになると思います。

keyword="電子工作"
NG_ScrName =[aaa,bbb,ccc,ddd]
for tweet in api.search(q=keyword, count=200):  # 検索
    txt = tweet.text #テキスト本文
        if not tweet.user.screen_name in NG_ScrName :
            if not has_NGWord_in(txt):
                if tweet.retweet_count >= 30:
                    if tweet.favorite_count >= 30: 
                        if is_japanese(txt):
                            print(txt)

ネストを深くしてはいけない信者の方からすれば、典型的なNG例です。
じゃあ、こうすればどうでしょう?

keyword="電子工作"
NG_ScrName =[aaa,bbb,ccc,ddd]
for tweet in api.search(q=keyword, count=200):  # 検索
    txt = tweet.text #テキスト本文
        if not tweet.user.screen_name in NG_ScrName and not has_NGWord_in(txt) and tweet.retweet_count >= 30 and tweet.favorite_count >= 30 and is_japanese(txt):
            print(txt)

ネストは深くはないですが、わかりにくさでは同じです。
また、1行の長さでもコーディング規約に引っかかる可能性があります。

1つの解決例が以下のような形ではないでしょうか?

keyword="電子工作"
NG_ScrName =[aaa,bbb,ccc,ddd]
for tweet in api.search(q=keyword, count=200):  # 検索
    txt = tweet.text #テキスト本文
    if tweet.user.screen_name in NG_ScrName :
        continue
    if has_NGWord_in(txt):
        continue
    if tweet.retweet_count < 30:
        continue
    if tweet.favorite_count < 30:
        continue 
    if not is_japanese(txt):
        continue 
    print(txt) #上記のいずれのifにも当てはまらないとき

このとき、判断条件を裏返して非該当の場合にその検索結果は無視します。
continueforに対して、次のlistのオブジェクトへ進むという意味です。
例えば、if tweet.retweet_count < 30:に該当すれば、 if tweet.favorite_count < 30:if not is_Japanese(txt):は評価されません。
アーリー・コンティニュー(early continue)というらしいです。
ちなみに、breakは該当すると、以降のforが評価されません。

NG wordが含まれているかどうかを判定する関数は以下のようなものを定義しました。

def has_NGWord_in(txt):
    for word in NG_Word:
        if txt.find(word) > -1:
            return True
    return False #アーリー・リターンしなかったらFalse

引数にTweetの本文を渡して、リストで定義されたNG_Wordのすべてについて、含まれていないときFalseを返すbool型の関数です。
対象のキーワード(ここでの引数)がリストに含まれているかどうか?であればif not in [List]の形で処理できますが、リストの全てのキーワードがという、逆の形式なので、このような関数を作りました。(もしかしたらやり方あるのでしょうか?)
forのループで、すべてのNG_Wordについて評価し、一つでも含まれていればTrueになります。
1つでも含まれていれば…なので、その時点でreturnし、関数を終了します。
これをアーリー・リターン(early return)といいます。

また、is_japanese(txt)のように関数にまとめることによってネストを浅くする方法もあります。

def is_japanese(string):
    for ch in string:
        name = unicodedata.name(ch)
        if "HIRAGANA" in name or "KATAKANA" in name:
            return True
    return False

この日本語判定は↓で紹介したものです。
s51517765.hatenadiary.jp

まとめ

アーリー・コンティニュー(early continue)とアーリー・リターン(early return)の使い方を実例をもとにまとめました。アーリー・リターンは言葉としてはよく聞きますね。
コードのわかりやすさのために使うもの、という認識はありました。
しかし、どういったときに使えばいいのか?というのがよくわからないでいました。
ifやforのネストが深いときには、こういった手法を使うと、読みやすくなります。
ほかに計算量(ステップ数)の削減にも効果があるかとも思いましたが、なくはないですが一般的にはO(n)の次元で削減しても大した効果はないとされています。

参考

qiita.com
teratail.com