19 日時、スケジュール、プログラムの起動

自分がコンピュータの前に座っていればプログラムを実行するのは簡単ですが、直接監督せずにプログラムを実行できると便利な場合があります。コンピュータの時計でプログラム実行をスケジューリングでき、指定した日時や一定の間隔でコードを実行できます。例えば、変更がないか毎時ウェブサイトをスクレイピングしたり、CPU負荷の高い作業を就寝中の午前4時に行ったりすることができます。Pythonのtimeモジュールとdatetimeモジュールを活用します。

subprocessモジュールを使うと、他のプログラムを起動することもできます。すでに誰かが作ってくれたアプリケーションを活用するのが目的を達成する近道であることが多いです。

timeモジュール

コンピュータのシステムクロックは特定のタイムゾーンの時刻を指し示しています。Pythonに同梱されているtimeモジュールを使うと、Pythonプログラムからシステムクロックの現在時刻を読み取れます。このモジュールでよく使う関数は、エポック秒と呼ばれる値を返すtime.time()と、プログラムを停止させるtime.sleep()です。

エポック秒

プログラミングでは、Unixエポックがよく用いられます。Unixエポックとは、UTC(協定世界時)での1970年1月1日午前0時0分0秒のことです。time.time()関数はUnixエポックからの経過秒数を浮動小数点数値で返します。この数値はエポック秒と呼ばれます。対話型シェルで次のように入力してみてください。

>>> import time
>>> time.time()
1773813875.3518236

2026年3月17日午後11時04分に太平洋標準時でtime.time()を呼び出しました。返り値はUnixエポックからtime.time()が呼び出された瞬間までに経過した秒数です。

time.time()の返り値は役に立ちますが、人間にはわかりづらいです。time.ctime()関数は現在時刻を表す文字列を返します。time.time()の返り値など、Unixエポックからの経過秒数を渡してその時刻の文字列を取得することもできます。以下の式を対話型シェルに入力してみてください。

>>> import time
>>> time.ctime()
'Tue Mar 17 11:05:38 2026'
>>> this_moment = time.time()
>>> time.ctime(this_moment)
'Tue Mar 17 11:05:45 2026'

エポック秒はコードのプロファイリングに使えます。一連のコードの実行にどれくらいの時間がかかるかを測定できます。測定したいコードブロックの最初と最後でtime.time()を呼び出して引き算をすれば、そのコードブロックに要した時間がわかります。例えば、新しいファイルエディタのタブを開いて、次のプログラムを入力してください。

# 100,000個の数値をかけ算するのにかかる時間を測定する
import time
❶ def calculate_product():
    # 最初の100,000個の数値をかけ算する
    product = 1
    for i in range(1, 100001):
        product = product * i
    return product

❷ start_time = time.time()
result = calculate_product()
❸ end_time = time.time()
❹ print(f'It took {end_time – start_time} seconds to calculate.')

❶で1から100,000までの整数のかけ算をするcalculate_product()という関数を定義しています。❷でtime.time()を呼び出し、返り値をstart_timeに格納します。その直後でcalculate_product()を呼び出してから、もう一度time.time()を呼び出して返り値をend_timeに格納します(❸)。calculate_product()を実行するのに要した時間を表示して終了します(❹)。

このプログラムをcalcProd.pyという名前で保存して実行してみてください。出力は次のようになります。

It took 2.844162940979004 seconds to calculate.

cProfile.run()を使ってコードプロファイリングをすることもでき、単純なtime.time()よりも詳細な情報が得られます。私の別の本であるBeyond the Basic Stuff with Python (No Starch Press, 2020)の第13章でcProfile.run()関数について説明しています。

プログラムの停止

プログラムをしばらく停止させる必要があれば、time.sleep()を呼び出して停止させたい秒数を渡します。対話型シェルで次のように入力してみてください。

>>> import time
>>> for i in range(3):
...   ❶ print('Tick')
...   ❷ time.sleep(1)
...   ❸ print('Tock')
...   ❹ time.sleep(1)
...
Tick
Tock
Tick
Tock
Tick
Tock
❺ >>> time.sleep(5)
>>>

forループでは、Tickを表示して(❶)、1秒停止し(❷)、Tockを表示して(❸)、1秒停止して(❹)、Tickを表示して…と、TickとTockが3回ずつ表示されるまで続きます。

time.sleep()関数は渡した秒数が経過するまでブロックします(プログラムのその次のコードは実行されません)。例えば、time.sleep(5)と入力したら(❺)、次のプロンプト(>>>)は5秒経過するまで表示されません。

プロジェクト14:スーパーストップウォッチ

まだ自動化していない退屈な作業にどれくらい時間がかかっているかを測定したいとしましょう。物理的なストップウォッチは持っておらず、広告が表示されずブラウザの閲覧履歴を送信しない無料のストップウォッチアプリを見つけるのは難しいです(アプリの利用時に同意すればブラウザの閲覧履歴が送信されます)。Pythonで単純なストップウォッチプログラムを書くことができます。

このプログラムには以下の内容が必要です。

  • プログラムの開始時とラップの開始時に、time.time()を呼び出して現在時刻を取得して保存する
  • ENTERが押されたらラップカウンターを1増やす
  • エポック秒の引き算をして経過時間を計算する
  • KeyboardInterrupt例外を処理してCTRL-Cが押されたら終了するようにする

新しいファイルエディタタブを開き、stopwatch.pyという名前で保存してください。

ステップ1:プログラムのセットアップ

ストップウォッチプログラムは現在時刻を利用するので、timeモジュールをインポートします。簡単な説明を表示してから、ユーザーがENTERを押したときにタイマーが開始するようにinput()を呼び出します。コードはユーザーがENTERを押すたびにラップタイムを計算し、CTRL-Cを押すと終了します。

以下のコードをファイルエディタに書いて、コードの残りの部分についてはTODOコメントを書きます。

# 単純なストップウォッチプログラム
import time

# プログラムの説明を表示
print('Press ENTER to begin and to mark laps. Ctrl-C quits.')
input()  # Enterを押して開始
print('Started.')
start_time = time.time()  # 最初のラップの開始時刻を取得
last_time = start_time
lap_number = 1

# TODO:ラップタイムの測定開始

説明を表示して、最初のラップを開始して時刻を保存し、lap_numberを1に設定するコードを書きました。

ステップ2:ラップタイムの測定と表示

新しいラップを開始して、前のラップの時間を計算し、ストップウォッチを開始してからの総経過時間を計算するコードを書きましょう。新しいラップを開始するごとに、ラップタイムとトータルタイムを表示して、ラップカウントを1増やします。以下のコードを追記してください。

# 単純なストップウォッチプログラム
import time

--snip--

# ラップタイムの測定開始
❶ try:
  ❷ while True:
        input()
      ❸ lap_time = round(time.time() – last_time, 2)
      ❹ total_time = round(time.time() – start_time, 2)
      ❺ print('Lap #{lap_number}: {total_time}({lap_time})', end='')
        lap_number += 1
        last_time = time.time() # 直近のラップタイムをリセット
❻ except KeyboardInterrupt:
    # Ctrl-C例外を処理してエラーメッセージを表示させない
    print('\nDone.')

ストップウォッチを止めるためにユーザーがCTRL-Cを押すと、KeyboardInterrupt例外が送出されてプログラムはクラッシュします。クラッシュしないように、プログラムをtry文で囲みます(❶)。except節で例外を処理します(❻)。例外が送出されたときに、KeyboardInterruptエラーメッセージではなくDoneと表示します。それまでは無限ループ内での実行が続きます(❷)。input()を呼び出してユーザーがラップを終わらせるためにENTERを押すまで待ちます。ラップが終われば、time.time()の現在時刻からlast_timeのラップ開始時刻を引いて、そのラップの時間を計算します(❸)。現在時刻からstart_timeのストップウォッチ開始時刻を引いて、総経過時間を計算します(❹)。

この計算結果には、4.766272783279419のように小数点以下の数字がたくさんあるので、round()関数で小数第二位に丸めます(❸❹)。

❺では、ラップ番号とトータルタイム(総経過時間)とラップタイムを表示します。input()呼び出しに対してユーザーがENTERを押すと画面が改行されるので、print()関数にend=''を渡して出力が二重に改行されてしまわないようにしています。ラップ情報を表示し、lap_numberに1を加え、last_timeに現在時刻をセットして、次のラップの準備を行います。これが次のラップの開始時刻です。

似たようなプログラムのアイデア

時間を測定できるようになれば、プログラムはさまざまな可能性に開かれます。時間を測定するアプリをダウンロードできますが、自分でプログラムを書けばフリー(自由/無料)ですし、広告や不必要な機能に悩まされることもありません。以下のようなプログラムを作成できます。

  • 従業員の名前を入力すると出退勤時刻が入力される単純な勤怠管理アプリ
  • 第13章で紹介したrequestsモジュールを使ってダウンロードするアプリへの経過時間の表示の追加
  • プログラムの実行時間をチェックして、あまりにも長く時間がかかっている作業をキャンセルできるようにする機能

datetimeモジュール

timeモジュールを使えばエポック秒を取得できます。しかし、日付をもっと見やすく表示したかったり、日付の計算をしたかったりしたら(例えば今日から205日前や123日後を知りたい場合など)、datetimeモジュールを使ったほうがよいです。

datetimeモジュールにはdatetimeデータ型があります。datetime値は特定の時点を表しています。以下の式を対話型シェルに入力してみてください。

>>> import datetime
❶ >>> datetime.datetime.now()
❷ datetime.datetime(2026, 2, 27, 11, 10, 49, 727297)
❸ >>> dt = datetime.datetime(2026, 10, 21, 16, 29, 0)
❹ >>> dt.year, dt.month, dt.day
(2026, 10, 21)
❺ >>> dt.hour, dt.minute, dt.second
(16, 29, 0)

datetime.datetime.now()を呼び出すと(❶)、コンピュータの時計に基づいた現在時刻のdatetimeオブジェクトが返されます(❷)。このオブジェクトには、年、月、日、時、分、秒、マイクロ秒が含まれています。datetime.datetime()関数を使えば(❸)、指定した時点のdatetimeオブジェクトを取得できます。年、月、日、時、分、秒を整数で渡します。この整数は、datetimeオブジェクトの属性year、month、day(❹)、hour、minute、second(❺)に格納されます。

datetime.datetime.fromtimestamp()関数でエポック秒をdatetimeオブジェクトに変換できます。datetimeオブジェクトの日時はローカルタイムゾーンに変換されます。以下の式を対話型シェルに入力してみてください。

>>> import datetime, time
>>> datetime.datetime.fromtimestamp(1000000)
datetime.datetime(1970, 1, 12, 5, 46, 40)
>>> datetime.datetime.fromtimestamp(time.time())
datetime.datetime(2026, 10, 21, 16, 30, 0, 604980)

1000000を渡してdatetime.datetime.fromtimestamp()を呼び出すと、Unix時間から1,000,000秒経過した瞬間のdatetimeオブジェクトが返されます。time.time()は、現在時刻のエポック秒を返しますから、これを渡すと現在時刻のdatetimeオブジェクトが返されます。つまり、datetime.datetime.now()とdatetime.datetime.fromtimestamp(time.time())は同じことをしています。どちらでも現在時刻のdatetimeオブジェクトを取得できます。

比較演算子でdatetimeオブジェクトを比較して、日時の先後を判定できます。遅いdatetimeオブジェクトが「大きい」値です。以下の式を対話型シェルに入力してみてください。

>>> import datetime
❶ >>> halloween_2026 = datetime.datetime(2026, 10, 31, 0, 0, 0)
❷ >>> new_years_2027 = datetime.datetime(2027, 1, 1, 0, 0, 0)
>>> oct_31_2026 = datetime.datetime(2026, 10, 31, 0, 0, 0)
❸ >>> halloween_2026 == oct_31_2026
True
❹ >>> halloween_2026 > new_years_2027
False
❺ >>> new_years_2027 > halloween_2026
True
>>> new_years_2027 != oct_31_2026
True

このコードでは、2026年10月31日午前0時0分0秒のdatetimeオブジェクトを作成してhalloween_2026に格納しています(❶)。次に、2027年1月1日午前0時0分0秒のdatetimeオブジェクトを作成してnew_years_2027に格納しています(❷)。さらに、2026年10月31日午前0時0分0秒のdatetimeオブジェクトをもう一つ作成してoct_31_2026に格納しています。halloween_2026とoct_31_2026を比較すると、等しいと判定されます(❸)。new_years_2027とhalloween_2026を比較すると、new_years_2027のほうがhalloween_2026よりも大きい(遅い)と判定されます(❹❺)。

期間

datetimeモジュールにはtimedeltaというデータ型もあります。これは瞬間的な時刻ではなく、幅のある期間を表します。以下の式を対話型シェルに入力してみてください。

>>> import datetime
❶ >>> delta = datetime.timedelta(days=11, hours=10, minutes=9, seconds=8)
❷ >>> delta.days, delta.seconds, delta.microseconds
(11, 36548, 0)
>>> delta.total_seconds()
986948.0
>>> str(delta)
'11 days, 10:09:08'

datetime.timedelta()関数でtimedeltaオブジェクトを作成します。datetime.timedelta()関数は、weeks、days、hours、minutes、seconds、milliseconds、microsecondsのキーワード引数を取ります。monthとyearのキーワード引数はありません。なぜなら月や年の期間は一定ではないからです。timedeltaオブジェクトは、日、秒、マイクロ秒の合計で期間を表します。それぞれ、days、seconds、microseconds属性に格納されています。total_seconds()メソッドは秒単位での期間を返します。timedeltaオブジェクトをstr()に渡すと、人間にとって読みやすい形式で表された文字列が返されます。

この例では、datetime.delta()に11日10時間9分8秒間の期間を指定するキーワード引数を渡し、返り値のtimedeltaオブジェクトをdeltaに格納しています(❶)。このtimedeltaオブジェクトのdays属性には11が格納され、seconds属性には36548(10時間9分8秒を秒で表したもの)が格納されます(❷)。total_seconds()を呼び出すと、11日10時間9分8秒間は986,948秒間であることがわかります。最後に、timedeltaオブジェクトをstr()に渡して、その期間をわかりやすい文字列にしています。

算術演算子を使ってdatetime値について日付計算を行うことができます。例えば、今から1,000日後を計算するには、対話型シェルに以下のように入力します。

>>> import datetime
>>> now = datetime.datetime.now()
>>> now
datetime.datetime(2026, 12, 2, 18, 38, 50, 636181)
>>> thousand_days = datetime.timedelta(days=1000)
>>> now + thousand_days
datetime.datetime(2029, 8, 28, 18, 38, 50, 636181)

まず、現在時刻のdatetimeオブジェクトを作成してnowに格納します。次に、1,000日間を表すtimedeltaオブジェクトを作成してthousand_daysに格納します。nowとthousand_daysを足し合わせ、nowの現在時刻から1,000日後のdatetimeオブジェクトを取得します。Pythonが計算をしてくれて、2026年12月2日の1,000日後は2029年8月28日だとわかります。1,000日後を計算するには、各月の日数に基づきうるう年を考慮するなどしなければなりませんが、datetimeモジュールはうまく処理してくれます。

timedeltaオブジェクトを使えば、+と-の演算子で、datetimeオブジェクトや別のtimedeltaオブジェクトと足し引きできます。timedeltaオブジェクトは*と/の演算子で整数や小数のかけ算や割り算ができます。以下の式を対話型シェルに入力してみてください。

>>> import datetime
❶ >>> oct_21st = datetime.datetime(2026, 10, 21, 16, 29, 0)
❷ >>> about_thirty_years = datetime.timedelta(days=365 * 30)
>>> oct_21st
datetime.datetime(2026, 10, 21, 16, 29)
>>> oct_21st – about_thirty_years
datetime.datetime(1996, 10, 28, 16, 29)
>>> oct_21st - (2 * about_thirty_years)
datetime.datetime(1966, 11, 5, 16, 29)

2026年10月21日を表すdatetimeオブジェクトを作成し(❶)、約30年間の期間を表すtimedeltaオブジェクトを作成しています(❷)。(毎年365日と考えてうるう年は考慮に入れていません。)oct_21stからabout_thirty_yearsを引くと、2026年10月21日から約30年前のdatetimeオブジェクトを取得できます。oct_21stから2 * about_thirty_yearsを引くと、1966年11月5日の夕方という、約60年前のdatetimeオブジェクトを取得できます。

指定した日時まで停止させる

time.sleep()メソッドは指定した秒数だけプログラムを停止させます。whileループを活用すると、プログラムを指定した日時まで停止させられます。例えば、以下のコードは、2039年のハロウィンまでループし続けます。

import datetime
import time
halloween_2039 = datetime.datetime(2039, 10, 31, 0, 0, 0)
while datetime.datetime.now() < halloween_2039:
    time.sleep(1)  # ループしてもう一度判定するまでに1秒待つ

time.sleep(1)を呼び出すとPythonプログラムを停止するので、最速でループしてコンピュータがCPUの処理サイクルを無駄にするということはありません。whileループは1秒に1回だけ条件を判定して、2039年のハロウィンになればプログラムの残りの部分に進みます(ここでは2039年のハロウィンにしましたが、プログラムをいつまで停止させるかは自由に決められます)。

datetimeオブジェクトから文字列への変換

エポック秒とdatetimeオブジェクトは人間の目に優しくありません。strftime()メソッドを使えばdatetimeオブジェクトを文字列として表示できます(strftime()関数の名前の中にあるfはformatのfです。)

strftime()メソッドはPythonの文字列補間と似た指示子を使います。表19-1はstrftime()指示子の全一覧です。この情報はhttps://strftime.orgにも載っています。

表 19-1:strftime()の指示子

strftime()の指示子

意味

%Y

'2026'のような4桁の年

%y

'00'から'99'までの2桁の年(1970から2069)

%m

'01'から'12'までの月

%B

'November'のような月名

%b

'Nov'のような省略形の月名

%d

'01'から'31'までの日付

%j

'001'から'366'までの一年のうちの日

%w

'0'(日曜日)から'6'(土曜日)までの曜日

%A

'Monday'のような曜日名

%a

'Mon'のよう省略形の曜日名

%H

'00'から'23'までの24時間表記の時間

%I

'01'から'12'までの12時間表記の時間

%M

'00'から'59'までの分

%S

'00'から'59'までの秒

%p

'AM'(午前)または'PM'(午後)

%%

リテラルな'%'記号(%文字そのものを表したいときに使う)

strftime()にフォーマット指示子を含めてスラッシュやコロンなどでカスタムしたフォーマット文字列を渡します。そうするとstrftime()は指定したフォーマットの文字列でdatetimeオブジェクトの情報を返します。以下の式を対話型シェルに入力してみてください。

>>> oct_21st = datetime.datetime(2026, 10, 21, 16, 29, 0)
>>> oct_21st.strftime('%Y/%m/%d %H:%M:%S')
'2026/10/21 16:29:00'
>>> oct_21st.strftime('%I:%M %p')
'04:29 PM'
>>> oct_21st.strftime("%B of '%y")
"October of '26"

2026年10月21日午後4時29分を表すdatetimeオブジェクトがあり、oct_21stに格納されています。カスタムしたフォーマット文字列'%Y/%m/%d %H:%M:%S'をstrftime()に渡すと、2026と10と21がスラッシュで区切られ、16と29と00がコロンで区切られた文字列が返されます。'%I:%M %p'を渡すと'04:29 PM'が、"%B of '%y"を渡すと"October of '26"が返されます。

文字列からdatetimeオブジェクトへの変換

'2026/10/21 16:29:00'や'October 21, 2026'のような日時情報の文字列があり、その文字列をdatetimeオブジェクトに変換したければ、datetime.datetime.strptime()関数が使えます。strptime()関数はstrftime()メソッドの逆です。strftime()と同じ指示子でカスタムしたフォーマット文字列を渡すと、文字列を解析してくれます(strptime()関数の名前の中にあるpはparseのpです)。

以下の式を対話型シェルに入力してみてください。

❶ >>> datetime.datetime.strptime('October 21, 2026', '%B %d, %Y')
datetime.datetime(2026, 10, 21, 0, 0)
>>> datetime.datetime.strptime('2026/10/21 16:29:00', '%Y/%m/%d %H:%M:%S')
datetime.datetime(2026, 10, 21, 16, 29)
>>> datetime.datetime.strptime("October of '26", "%B of '%y")
datetime.datetime(2026, 10, 1, 0, 0)
>>> datetime.datetime.strptime("November of '63", "%B of '%y")
❷ datetime.datetime(2063, 11, 1, 0, 0)
>>> datetime.datetime.strptime("November of '73", "%B of '%y")
❸ datetime.datetime(1973, 11, 1, 0, 0)

'October 21, 2026'という文字列からdatetimeオブジェクトを取得するために、文字列をstrptime()の第一引数に渡し、'October 21, 2026'に対応するカスタムしたフォーマット文字列を第二引数に渡しています(❶)。日時情報の文字列はカスタムしたフォーマット文字列と正確にマッチしなければなりません。そうでないとPythonはValueError例外を送出します。%y指示子は1970から2069の間であるため、"November of '63"は2063と解釈されるのに対し(❷)、"November of '73"は1973と解釈される(❸)ことに注意してください。

Pythonの時間関数のおさらい

Pythonの日付と時間にはさまざまなデータ型と関数があります。時間を表す3種類の型の値をおさらいします。

  • (timeモジュールで使用される)エポック秒は、UTCでの1970年1月1日午前0時0分0秒からの経過秒数を表す浮動小数点数値または整数値です。
  • (datetimeモジュールの)datetimeオブジェクトは、属性year、month、day、hour、minute、second、 microsecondに整数が格納されています。
  • (datetimeモジュールの)timedeltaオブジェクトは時刻ではなく期間を表します。

時間関数とそのパラメータおよび返り値についておさらいします。

time.time() この関数は、現在のエポック秒を浮動小数点数値で返します。

time.sleep(seconds) この関数は、プログラムを seconds引数で指定した秒数だけ停止させます。

datetime.datetime(year, month, day, hour, minute, second) この関数は、引数で指定した時刻の datetimeオブジェクトを返します。hour、minute、second の引数は、指定されなければデフォルトで0になります。

datetime.datetime.now() この関数は、現在時刻のdatetimeオブジェクトを返します。

datetime.datetime.fromtimestamp(epoch) この関数は、epoch引数で指定したエポック秒のdatetimeオブジェクトを返します。

datetime.timedelta(weeks, days, hours, minutes, seconds, milliseconds, microseconds) この関数は、期間を表すtimedeltaオブジェクトを返します。この関数のキーワード引数はすべてオプションで、monthとyearはありません。

total_seconds() このtimedeltaメソッドは、そのtimedeltaオブジェクトが表す秒数を返します。

strftime(format) このdatetimeメソッドは、formatに基づきカスタムしたフォーマットの時刻の文字列を返します。フォーマットの詳細については表19-1を参照してください。

datetime.datetime.strptime(time_string, format) この関数は、time_stringで指定した時刻を表すdatetimeオブジェクトを返します。 format文字列引数に基づいて解釈します。フォーマットの詳細については表19-1を参照してください。

Pythonから別のプログラムを起動する

組み込みのsubprocessモジュールのrun()関数を使えば、Pythonのプログラムからそのコンピュータ上の別のプログラムを開始できます。同じアプリケーションを複数開くと、それぞれ別のプロセスになります。例えば、図19-1は、別々のプロセスで電卓アプリを開いたウィンドウを示しています。

Screenshot of a Windows computer desktop showing six overlapping calculator programs

図 19-1:同じ電卓プログラムの6つの実行プロセス

Pythonスクリプトから外部のプログラムを開始するには、subprocess.run()に起動したいプログラムのファイル名を渡します。(Windowsでは、アプリケーションのスタートメニューを右クリックしてプロパティを選べばそのアプリケーションのファイル名を確認できます。macOSでは、アプリケーションをCTRL-クリックして、パッケージの内容を表示を選べば実行ファイルのパスを確認できます。)run()関数は起動したプログラムが閉じられるまでコードの実行をブロックします。起動するプログラムの実行ファイルのパスをリスト内に文字列で書いて渡します。起動したプログラムは、Pythonプログラムと同じプロセスではなく、別々のプロセスで実行されます。

Windowsでは、以下の対話型シェルの内容を実行してください。

>>> import subprocess
>>> subprocess.run(['C:\\Windows\\System32\\calc.exe'])
CompletedProcess(args=['C:\\Windows\\System32\\calc.exe'], returncode=0)

Ubuntu Linuxでは、以下を実行してください。

>>> import subprocess
>>> subprocess.run(['/usr/bin/gnome-calculator'])
CompletedProcess(args=['/usr/bin/gnome-calculator'], returncode=0)

macOSでは、以下を実行してください。

>>> import subprocess
>>> subprocess.run(['open', '/System/Applications/Calculator.app'])
CompletedProcess(args=['open', '/System/Applications/Calculator.app'], returncode=0)

macOSではopenプログラムに起動したいプログラムをコマンドライン引数で渡さなければなりません。

これらの例では、Pythonのコードが指定したプログラムを起動し、そのプログラムが閉じられるまで待ってから、コードの残りの部分を実行します。プログラムを起動してそのプログラムが閉じられるのを待たずにすぐコードの実行を続けたければ、subprocess.Popen() 関数(Popenはprocess openの略)を呼び出します。

>>> import subprocess
>>> calc_proc = subprocess.Popen(['C:\\Windows\\System32\\calc.exe'])

返り値はPopenオブジェクトで、poll()とwait()という2つの便利なメソッドがあります。

poll()メソッドは運転手に「目的地にもう着きましたか?」と到着するまで何度も聞くようなものです。poll()が呼び出されたときにプロセスがまだ実行中であれば、poll()メソッドはNoneを返します。プログラムが終了していたら、そのプロセスの終了コードを整数で返します。終了コードはプロセスが正常に終了したかどうかを示します。正常終了であれば終了コードは0で、エラーによりプロセスが終了すれば0以外です(1のことが多いですがプログラムによって異なります)。

wait()メソッドは、運転手が目的地に到着するのを待つようなものです。起動したプロセスが終了するまでブロックします。ユーザーが別のプログラムでの処理を終えるまでプログラムを停止させたいときに便利です。wait()の返り値は起動したプロセスの終了コードの整数です。

Windowsでは、対話型シェルで以下の内容を入力してください。wait()を呼び出すと起動した電卓プログラムが終了するまでコード実行をブロックします。

>>> import subprocess
❶ >>> calc_proc = subprocess.Popen(['c:\\Windows\\System32\\calc.exe'])
❷ >>> calc_proc.poll() == None
True
❸ >>> calc_proc.wait() # 電卓が閉じられるまで返ってこない
0
>>> calc_proc.poll()
0

電卓プロセスを開始します(❶)。Windowsの古いバージョンでは、起動したプロセスが実行中であればpoll()がNoneを返します(❷)。電卓アプリケーションのウィンドウを閉じてから対話型シェルに戻り、終了したプロセスについてwait()を呼び出します(❸)。wait()とpoll()は、プロセスが正常に終了したことを示す0を返します。

Windows10以降でsubprocess.Popen()によりcalc.exeを実行すると、電卓アプリが実行中であってもwait()がすぐに返ってきます。calc.exeが電卓アプリを起動してすぐに終了するからです。Windowsの電卓プログラムは「信頼済みMicrosoft Storeアプリ」で、その詳細は本書の範囲外です。プログラムはアプリケーションやOSによって特有の実行のされ方をすることを覚えておいてください。

subprocess.Popen()で起動したプロセスを閉じたければ、その関数が返すPopenオブジェクトのkill()メソッドを呼び出します。WindowsのMS Paintをお持ちでしたら、対話型シェルで以下の内容を入力してみてください。

>>> import subprocess
>>> paint_proc = subprocess.Popen('c:\\Windows\\System32\\mspaint.exe')
>>> paint_proc.kill()

kill()メソッドは、「本当にプログラムを終了しますか?」のような確認を経ずに即座にプログラムを終了させることに注意してください。そのプログラムでの保存していない作業はすべて失われます。

プロセスにコマンドライン引数を渡す

run()で作成したプロセスにコマンドライン引数を渡すことができます。run()の唯一の引数であるリストでコマンドライン引数を渡します。このリストの最初の文字列は起動するプログラムの実行ファイルの名前で、それ以降の文字列は起動するプログラムのコマンドライン引数になります。起動したプログラムのsys.argvの値になります。

GUIを持つアプリケーションはターミナルベースのプログラムほどコマンドライン引数を使いませんが、そのアプリケーションが起動したときに開くファイル名を引数に取るGUIアプリケーションは多くあります。例えば、Windowsで、C:\Users\Al\hello.txtという名前のテキストファイルを作成し、対話型シェルで以下の内容を入力してください。

>>> subprocess.run(['C:\\Windows\\notepad.exe', 'C:\\Users\Al\\hello.txt'])
CompletedProcess(args=['C:\\Windows\\notepad.exe', 'C:\\Users\\Al\\hello.txt'], returncode=0)

メモ帳アプリケーションがC:\Users\Al\hello.txtファイルを開きます。プログラムごとにコマンドライン引数が定められており、特にGUIアプリケーションではコマンドライン引数を一切取らないことも珍しくありません。

subprocess.Popen()関数もコマンドライン引数を同じように扱います。関数に渡すリストの末尾にコマンドライン引数を入れます。

起動したコマンドから出力テキストを受け取る

subprocess.run()とsubprocess.Popen()ではターミナルコマンドを起動することもできます。Pythonのコードで、起動したターミナルコマンドについて、出力テキストを受け取ったりキーボード入力を送信したりしたいことがあるかもしれません。例えば、Pythonからpingコマンドを起動して、その出力テキストを受け取ってみましょう(pingコマンドの詳細については本書の範囲外です)。Windowsでは、-n 4引数をつけて、Nostarch.comのサーバーがオンラインであるかを確認するためにpingリクエストを4回送信します。macOSとLinuxでは、-nではなく-cとしてください。このコマンドは実行に数秒を要します。

>>> import subprocess
>>> proc = subprocess.run(['ping', '-n', '4', 'nostarch.com'], capture_output=True, text=True)
>>> print(proc.stdout)
Pinging nostarch.com [104.20.120.46] with 32 bytes of data:
Reply from 104.20.120.46: bytes=32 time=19ms TTL=59
Reply from 104.20.120.46: bytes=32 time=17ms TTL=59
Reply from 104.20.120.46: bytes=32 time=97ms TTL=59
Reply from 104.20.120.46: bytes=32 time=217ms TTL=59

Ping statistics for 104.20.120.46:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 17ms, Maximum = 217ms, Average = 87ms

subprocess.run()にcapture_output=Trueとtext=Trueの引数を渡すと、起動したプログラムの出力テキストがCompletedProcessオブジェクトのstdout(標準出力)属性に文字列として格納されます。Pythonスクリプトで他のプログラムの機能を活用してその出力テキストを文字列として利用できます。

タスクスケジューラ、launchd、cronの実行

コンピュータに詳しい人なら、Windowsのタスクスケジューラ、macOSのlaunchd、Linuxのcronスケジューラのことをご存知かもしれません。資料が豊富で信頼できるこれらのツールを用いれば、指定した時間にアプリケーションが起動するようにスケジューリングできます。

OSに内蔵されているスケジューラを使えば、自分で時計を確認してスケジューリングするコードを書かなくてもすみます。ただし、プログラムを少し停止させたいだけなら、ループでtime.sleep(1)を呼び出して待機させたほうが簡単です。

デフォルトアプリケーションでファイルを開く

お使いのコンピュータで.txtファイルをダブルクリックすると、.txtファイル拡張子と関連付けられたアプリケーションが自動的に起動します。各OSにはダブルクリックするのと同等のプログラムがあります。Windowsではstartコマンドで、macOSとLinuxではopenコマンドです。対話型シェルに以下の内容を入力し、お使いのOSに応じてrun()に'start'または'open'を渡してください。また、Windowsでは、以下のようにshell=Trueキーワード引数を渡します。

>>> file_obj = open('hello.txt', 'w')  # hello.txtファイルの作成
>>> file_obj.write('Hello, world!')
13
>>> file_obj.close()
>>> import subprocess
>>> subprocess.run(['start', 'hello.txt'], shell=True)

Hello, world!を新しいhello.txtファイルに書き込んでいます。それから、run()を呼び出し、プログラム名(上の例ではWindowsの'start')とファイル名を含むリストを渡します。OSはそのファイルとの関連付けがわかるので、例えば、Windowsでは、hello.txtファイルを扱うNotepad.exeを起動します。

プロジェクト15:単純なカウントダウン

単純なストップウォッチアプリケーションを見つけるのが難しいのと同じように、単純なカウントダウンアプリケーションを見つけるのも難しいです。カウントダウンが終了したらアラームを鳴らすプログラムを書いてみましょう。

このプログラムには以下の内容が必要です。

  • カウントダウンの数字を表示する間にtime.sleep()で1秒間停止する
  • subprocess.run()でalarm.wavの音声ファイルをデフォルトアプリケーションで開く

新しいファイルエディタタブを開いてsimplecountdown.pyという名前で保存してください。

ステップ1:カウントダウン

このプログラムには、time.sleep()関数のためのtimeモジュールと、subprocess.run()関数のためのsubprocessモジュールが必要です。以下のコードをsimplecountdown.pyファイルに保存してください。

# https://autbor.com/simplecountdown.py - 単純なカウントダウンスクリプト

import time, subprocess

❶ time_left = 60
while time_left > 0:
  ❷ print(time_left)
  ❸ time.sleep(1)
  ❹ time_left = time_left - 1

# TODO:カウントダウンが終了したら音声ファイルを再生する

timeとsubprocessをインポートしてから、カウントダウンで残り秒数を保持するtime_leftという名前の変数を作成します(❶)。ここでは60を設定していますが、好きな値に変更してください。コマンドライン引数から設定することもできます。

whileループでは、残り秒数を表示し(❷)、1秒間停止して(❸)、time_left変数を1減らし(❹)、ループを続けます。time_leftが0より大きい間はループし続けます。0になればカウントダウンが終わります。次に、音声ファイルを再生するTODOコメントの部分のコードを書きます。

ステップ2:音声ファイルを再生する

第12章でさまざまなフォーマットの音声を再生するplaysound3モジュールを紹介しましたが、ユーザーが音声を再生するのにすでに使っているアプリケーションを起動するのが一番簡単です。.wavファイル拡張子からどのアプリケーションを起動して再生するかをOSに委ねます。.wavファイルを.mp3や.oggファイル形式に差し替えることも簡単にできます。カウントダウンが終わったときに再生する音声ファイルは何でも構いません。https://autbor.com/alarm.wavからalarm.wavをダウンロードしてもよいです。

以下のコードを追加します。

# https://autbor.com/simplecountdown.py - 単純なカウントダウンスクリプト

--snip--

# カウントダウンが終了したら音声ファイルを再生する
#subprocess.run(['start', 'alarm.wav'], shell=True)  # Windows
#subprocess.run(['open', 'alarm.wav'])  # macOSとLinux

whileループが終われば、alarm.wav(あるいはお選びの音声ファイル)が再生され、ユーザーにカウントダウンが終了したことを伝えます。お使いのOSに応じてsubprocess.run()の呼び出しのコメントを外してください。Windowsでは、run()に渡すリストに'start'を含めてください。macOSとLinuxでは、'start'ではなく'open'を渡し、shell=Trueを取り除いてください。

音声ファイルを再生する代わりにBreak time!のようなメッセージが書かれたテキストファイルをどこかに保存してカウントダウンが終了したときにsubprocess.run()で開くこともできます。これによりポップアップウィンドウでメッセージが表示されます。あるいは、カウントダウンが終了したときに、webbrowser.open()関数で指定したウェブサイトを開くこともできます。オンラインで入手できる無料のカウントダウンアプリケーションとは異なり、自作のカウントダウンプログラムでは好きなようにカスタマイズできます。

似たようなプログラムのアイデア

カウントダウンとは、プログラム実行を継続する前に遅延を生じさせることです。以下のようなアプリケーションや機能を同じアプローチで作成できます。

  • time.sleep()を使用した、ユーザーがCTRL-Cを押してファイル削除などの実行をキャンセルできる機能。“Press CTRL-C to cancel”のようなメッセージを表示して、try文とexcept文でKeyboardInterrupt例外を処理します。
  • 長い時間のカウントダウンには、timedeltaオブジェクトを利用できます。将来のある時点(誕生日や記念日)までの日数、時間数、分数、秒数を設定できます。

まとめ

Unix時間(UTCでの1970年1月1日午前0時0分0秒)はPythonを含む多くのプログラミング言語で参照する標準時間です。time.time()関数はエポック秒(Unix時間からの経過秒数を表す浮動小数点数値)を返します。日時の計算や表示の調整や文字列の解析には、datetimeモジュールを使ったほうが便利です。

time.sleep()関数は指定秒数だけコード実行をブロックします。プログラムを停止させるのに使えます。ただし、プログラムを指定日時に実行させるなら、https://nostarch.com/automate-boring-stuff-python-3rd-editionの説明を読み、お使いのOSに内蔵されているスケジューラを使ってください。

自分が書いたPythonプログラムからsubprocess.run()関数で別のアプリケーションを起動できます。コマンドライン引数をrun()呼び出しに渡してそのアプリケーションで開くファイルを指定できます。あるいは、run()でstartまたはopenプログラムを使って、ファイルの関連付けによりファイルを開くアプリケーションを自動的に起動することもできます。コンピュータ上の別のアプリケーションを利用することで、Pythonプログラムは自動化に必要な機能を拡張することができます。

練習問題

  1. Unix時間とは何ですか?

  2. Unix時間からの経過秒数を返す関数は何ですか?

  3. 現在時刻について、'Mon Jun 15 14:00:38 2026'のような人間が読みやすい文字列を返すtimeモジュールの関数は何ですか?

  4. 5秒間プログラムを停止させるにはどうしますか?

  5. round()関数は何を返しますか?

  6. datetimeオブジェクトとtimedeltaオブジェクトの違いは何ですか?

  7. datetimeモジュールを使って、2019年1月7日の曜日を調べてください。

練習プログラム

以下の練習プログラムを書いてください。

ストップウォッチの表示調整

本章のストップウォッチプロジェクトの拡張で、rjust()メソッドとljust()メソッドを使って出力をきれいにしてください(これらのメソッドは第8章で紹介しました)。以下のような出力から

Lap #1: 3.56 (3.56)
Lap #2: 8.63 (5.07)
Lap #3: 17.68 (9.05)
Lap #4: 19.11 (1.43)

以下のような出力へと調整します。

Lap # 1:   3.56 (  3.56)
Lap # 2:   8.63 (  5.07)
Lap # 3:  17.68 (  9.05)
Lap # 4:  19.11 (  1.43)

次に、第8章で紹介したpyperclipモジュールを使い、出力テキストをクリップボードにコピーして、テキストファイルやメールに素早く貼り付けられるようにしてください。

13日の金曜日発見器

13日の金曜日は不吉だと考えられています(私は個人的に吉兆だと考えています)。うるう年や月の日数の変動から、次の13日の金曜日がいつになるかを判断するのは難しいです。

2つのプログラムを書きます。1つ目のプログラムでは、現在の日付のdatetimeオブジェクトと、1日間のtimedeltaオブジェクトを作成します。現在の日付が13日の金曜日なら、その月と年を表示します。次に、datetimeオブジェクトにtimedeltaオブジェクトを足して次の日を設定し、13日の金曜日かどうかまたチェックします。次の13日の金曜日を見つけるまでこれを繰り返します。

2つ目のプログラムは、timedeltaオブジェクトを足すのではなく引くという点を除き、1つ目のプログラムと同じです。このプログラムは、西暦1年まで遡り、過去の13日の金曜日をすべて発見します。