メインコンテンツへスキップ
  1. 第07講 再帰関数(2/2; 探索)/

第07講 週次課題

週次課題 探索 再帰呼び出し
目次

課題07-1 特定の名前のファイルを見つける
#

難易度 ⭐

コマンドライン引数でファイル名と,ディレクトリ名もしくはファイル名が与えられます. コマンドライン引数で最初に与えられた名前を持つファイルを再起的に探索し,そのファイルが見つかったらそのパスを表示するプログラムを作成してください.

ファイル名は find_file.py というファイル名で保存してください.

import sys
import os

# name という名前のファイルを base 以下から探す関数.
# 複数見つかる場合もありうるため,見つかったものをリストで返す.
def find_file(name, base, result = None):
    if result is None:
        result = []
    # base がディレクトリの場合
        # base の一覧を取得し,それぞれを処理する.
            # それぞれのアイテムに対して,base と item を join する.
            # find_file を再起的に呼び出す.
    # そうでない場合
        filename = os.path.basename(base) # base から ファイル名部分を取り出す.
        # filename が name と一致する場合
            # result に base を追加する.
    return result

for base in sys.argv[2:]:
    paths = find_file(sys.argv[1], base)
    if paths:
        print(f"{sys.argv[1]}: {paths}")

find_fileの引数の result はリストを想定しており,デフォルト値として None を与えていいます. これを find_file(name, base, result = []) としたいところですが, 期待通りに動作しない場合もあります. そのため,上記のように None を初期値として,関数内部で初期することが一般的です.

📥 dirs.zipのダウンロード

実行例
#

$ python3 find_file.py file4 root
file4: ['root/dir5/file4', 'root/dir4/file4']

課題07-2: 年齢を当てる
#

難易度 ⭐⭐

コマンドライン引数で年齢を与えます. その年齢を当てるまでにかかるステップ数を求めるプログラムを作成してください. コマンドラインで与える年齢は 0以上100以下とします.

guess_age.py というファイル名で保存してください.

import sys

def find_age(age, low, high, step = 1):
    mid = (low + high) // 2
    pass # この pass を削除して,関数を実装してください.

for age_string in sys.argv[1:]:
    age = int(age_string)
    step = find_age(age, 0, 100)
    print(f'age = {age}, step = {step}')

実行例
#

$ python3 guess_age.py 50 25 75 12 37 62 87
age = 50, step = 1
age = 25, step = 6
age = 75, step = 2
age = 12, step = 6
age = 37, step = 3
age = 62, step = 3
age = 87, step = 7
発展問題 誕生日を当てる

発展問題 誕生日を当てる
#

課題07-2 と同様に,誕生日を当てることにも挑戦してみましょう. コマンドライン引数で誕生日(月,日)を与えます. その与えられた誕生日を当てるまでにかかるステップ数を求めるプログラムを作成してください. コマンドラインで与える誕生日は 11日以上1231日以下とします.

guess_birthday.py というファイル名で作成してください.

import sys

days_of_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

# 月と日が与えられたとき,1月1日からの日数を返す関数
def month_day_to_int(month, day):
    days = 0
    for i in range(month - 1):
        days += days_of_month[i]
    return days + day

# 1〜365 が与えられたとき,月と日を返す関数
def int_to_month_day(n):
    for i, d in enumerate(days_of_month):
        if n <= d:
            return i + 1, n
        n = n - d

def find_birthday(target_day, low, high, step = 1):
    mid = (low + high) // 2
    pass # この pass を修正して,関数を実装してください.

month = int(sys.argv[1])
day = int(sys.argv[2])
target_day = month_day_to_int(month, day)
step = find_birthday(target_day, 1, 365)
print(f'birthday = {month}/{day}, step = {step}')

# 以下,テストコード
assert month_day_to_int(1, 1) == 1
assert month_day_to_int(2, 1) == 32
assert month_day_to_int(12, 31) == 365

assert int_to_month_day(1) == (1, 1)
assert int_to_month_day(60) == (3, 1)
assert int_to_month_day(365) == (12, 31)

プログラムの単純化のため,うるう年には対応していません.更なる挑戦問題として,うるう年にも対応するプログラムを作成してみましょう.

実行例
#

$ python3 guess_birthday.py 7 1
birthday = 7/1, step = 9
$ python3 guess_birthday.py 7 2
birthday = 7/2, step = 1

課題07-3 特定の内容を含むファイルを検索する
#

難易度 ⭐⭐⭐

課題07-1 の応用です. コマンドライン引数でキーワードと,ディレクトリ名もしくはファイル名が与えられます. ディレクトリ,ファイルを再起的に探索し,キーワードを含むファイルがあればそのパスを表示するプログラムを作成してください.

ファイル名は find_grep.py というファイル名で保存してください. grep とは Linux 系OSでのコマンドで,ファイル内の特定の文字列を検索するコマンドです. global regular expression print の頭文字だそうです.🔗

import sys
import os

# file_path というファイルに keyword が含まれているかを判定する関数
# 含まれていれば True を返し,そうでなければ False を返す.
def contains_keyword(file_path, keyword):
    pass # この pass を削除して,関数を実装してください.
    # file_path を読み込みモードで開く.
        # ファイルを一行ずつ読み込む.
            # line に keyword が含まれているかを判定する.
                # 含まれていれば True を返す.
    # 含まれていなければ False を返す.

# name という名前のファイルを base 以下から探す関数.
# 複数見つかる場合もありうるため,見つかったものをリストで返す.
def find_grep(keyword, base, result = None):
    if result is None:
        result = []
    # base がディレクトリの場合
        # base の一覧を取得し,それぞれを処理する.
        for child in os.listdir(base):
            if not child.startswith("."): # 隠しファイルは対象外とする.
                # それぞれのアイテムに対して,base と child を join する.
                # find_grep を再起的に呼び出す.
    # そうでない場合
        if contains_keyword(base, keyword): # base に keyword が含まれているかを判定する.
            # result に base を追加する.
    return result

for base in sys.argv[2:]:
    paths = find_grep(sys.argv[1], base)
    if paths:
        print(f"{sys.argv[1]}: {paths}")

実行例
#

この実行例は以下の dirs.zip をダウンロードしてできたディレクトリ(root)での実行例です.

📥 dirs.zipのダウンロード

$ python3 find_grep.py file2 root             # root ディレクトリの中にある file2 を含むファイルを探す.
file2: ['root/dir2/file2', 'root/dir1/file2'] # root/dir2/file2 と root/dir1/file2 が見つかる.
バイナリファイルの扱い

この課題は,バイナリファイルが含まれている場合,エラーが発生します. バイナリファイルを読む場合は,open 関数の第二引数に rb を指定してください. ただし,バイナリファイルは,1行ずつ読み込めないため,read で1バイトずつ読み込む必要があります.

mimetypes モジュールを用いて,ファイルの種類を判定しテキストファイルのみを対象にする場合は,contains_keywordの最初に以下のようなコードを追加してください. なお,mimetypesimport が必要です(プログラムコードの最初に import mimetypesを入力する).

def contains_keyword(file_path, keyword):
    ret = mimetypes.guess_type(file_path, strict=True)
    if (ret[0] == None) or (not('text' in ret[0])): 
        return False   # テキストファイルでない場合は False を返す.
    # file_path を読み込みモードで開く.
        # ファイルを一行ずつ読み込む.
            # line に keyword が含まれているかを判定する.
                # 含まれていれば True を返す.
    # 含まれていなければ False を返す.