関数覚えてPython RPAの面倒を大幅削減!

FacebooktwitterredditpinterestlinkedinmailFacebooktwitterredditpinterestlinkedinmail

RPA に限らず、 Python でプログラミングをしていると、「同じような処理を何度も書かないといけなくて、面倒だなぁ」と思うこと、ありますよね? 楽をするために RPA を書いているのに、それが面倒では困ってしまいますよね。前回の「Python RPA でウィンドウや画像が出るのを待つ」のサンプルでは名前を付けて保存の確認ダイアログに、迅速かつ正確に対応できていましたが、その分似たような処理が多くて面倒でした。

でも大丈夫です! プログラミング言語には、「関数」という同じ処理をなんども使い回せる仕組みが用意されています。しかも!全く同じではなくて、「引数」を与えることで、その動きを変化させることができます。

関数を自分で、ある程度適切に作れるようになってくると、「自分、プログラミングできますよ!」ってある程度言えるようなレベルになります。今回も頑張っていきましょう。

今回の内容
  • 関数って、なんだ?
  • 引数のない簡単な関数
  • 引数を使用した関数
  • 返り値のある関数
  • RPA を関数で整理する

この記事は、 RPA を作りながら Python を学習する欲張り企画の1部です。 Python と RPA を最初から学習したい、という方はシリーズをご覧下さい。

この連載の目次

関数って、なんだ?

まず、 Python, プログラミングにおける関数を見て行きましょう。普通、関数といったら、 y = 3x + b のような、数学でグラフを書くような数式を意味します。

プログラミングにおける関数は、これとは少し異なっていて、「プログラムで行う、色々な処理を再利用可能な形でまとめておく」のが関数となります。

Python においては、変数と同様に名前を付けて、丸括弧()演算子で実行します。

たとえば、いつも使っている print 関数では、名前がprint, 実行するために()演算子を使っているということです。

そして、print(“文字列”)の「文字列」の部分が引数と呼ばれています。これは本連載でも何度か出てきているのでなんとなく覚えている人も多いと思います。

この引数は、y = 3x + b の式で言えば、 x と bの部分に当たります。 y(5, 2)としたら、 3 * 5 + 2 = 17 で、yが17と求まるため、プログラミングにおいても関数と呼ばれるのですね。

なお、ここでの 17 は、プログラミングにおいては返り値(戻り値)と呼ばれます。関数の結果とも言うことができますね(詳しくは後で解説します)。

 

引数のない簡単な関数

では、引数のない簡単な関数を作ってみましょう。chapter-10-1.py として Python ファイルを作成して、以下のような Python プログラムを入力します。

import time

def countDown():
    for n in range(10):
        m = 10 - n
        print(m, '秒前')
        time.sleep(1)
    print('0')

countDown()

Python における関数の定義は

def 関数名():

が基本形です。上のPython プログラムでは、countDown という関数を定義しています(def は英語のdefine, 定義という意味です)。

関数の内容を示すブロックは、字下げ・インデントで行います。for 文、 if文と同じですね。

もちろん、関数の中にも for 文や if 文を入れることができます。その場合は、for 文や if 文の中のブロックは、字下げ・インデントをさらに1段階深くする必要があります。上のPython では、for 文の中はタブ文字を2つになっていますので、確認して下さい(もちろん、スペースでも大丈夫です)。

また、もう一つ大事なこととして、関数の中で代入された変数はローカル変数と呼ばれ、その関数の中でしかアクセスできない変数となります。

例として、countDown()として、countDown関数を実行した後に、print(m)としてみましょう。

すると、このように「NameError」というエラーが発生してしまいます。これは、countDown 関数の外では、mという変数に何の値も代入されたことがないため、エラーとなっています。

では、countDown関数の最後でprint(‘0’)として0を出力しています。これの次の行で(字下げを変えずに)print(m)とするとどうでしょうか。

こちらは、0の次にmの最後の値である 1 がちゃんと出力されていることが分かります。for 文の中とブロック(字下げの量)が異なりますが、同じ関数の中なので問題無く変数にアクセスできていますね。

 

今回作成したcountDown関数は、10秒前からカウントダウンするプログラムです。

引数も返り値もない関数はプログラミングではあまり多くありませんが、 RPA の場合は「PCの操作」という出力があるので、意外に出番があります。例えば、「名前を付けて保存の確認ダイアログ」が出たら、yキーを押す RPA なんかは、関数化できそうですね。

この後、詳しく解説しますが、自信がある方は読み進める前にご自分で挑戦してみてください。

 

引数を利用した関数

引数を利用した関数の定義は、関数名の後の()の中に、引数の名前を宣言することで行います。

def 関数名(引数1, 引数2….):

宣言した引数の名前は、関数の中だけで使えるローカル変数として機能します。この引数を関数の外で呼びだそうとした場合、前項でみたローカル変数の例のように、NameErrorが出ます。

chapter-10-1.py を以下のように書き換えて、カウントダウンする秒数を変えられるようにしてみましょう。

import time

def countDown(time):
    for n in range(time):
        m = time - n
        print(m, '秒前')
        time.sleep(1)
    print('0')

countDown(5)

5秒カウントダウンされるプログラムになると思います。そして、 countDown関数を呼び出している 5という数字を変えれば、それでカウントダウンされる秒数が変わることも確認できますね。

デフォルト引数のある関数

デフォルト引数とは、引数が省略されても関数が動作するように、引数が与えられなかったときの値をあらかじめ決めておく書き方です。

def 関数名(引数名1, 引数名2 = デフォルト引数の値):

という形式になります。文字で説明するより、動作を見た方が早いのでchapter-10-1.py をまずは以下のように書き換えて実行してみましょう。

import time

def countDown(sec, message='0'):
    for n in range(sec):
        m = sec - n
        print(m, '秒前')
        time.sleep(1)
    print(message)

countDown(5)
countDown(5, '時間切れ!')

今回は、2回のカウントダウンが行われます(countDown関数が2回呼ばれていますね)。

1回目は、今まで通り、最後に0と表示されていますが、2回目は、「時間切れ!」という文章が表示されました。これは、2回目は2番目の引数に’時間切れ!’という文字列を指定しているからです。

1回目の方は、何も指定していないので、関数の定義で、message=’0’とデフォルト引数の値が設定されているため、0となっています。

また、デフォルト値が指定ある引数に値を与える方法はもう1種類方法があります。

countDown(5, message=’時間切れ!’)

こういった形式です。引数の名前を指定して、そこに代入するように = 演算子を使っていますね。

これがどういう時に役に立つかというと、デフォルト値のある引数が複数あるときに一部にだけ引数を渡したいときや、引数の順序を忘れてしまった場合などです。例えば、以下のように書き換えます。

import time

def countDown(sec=5, message='0'):
    for n in range(sec):
        m = sec - n
        print(m, '秒前')
        time.sleep(1)
    print(message)

countDown(message='time up!')
countDown(message='時間切れ!',sec=3)

2回countDownを呼び出していますが、どちらも動作します。何秒待つかを決めるsec引数にもデフォルト値が割り当てられたことで、省略可能になったためですね。2回目の実行では、引数の定義の順番と逆ですが、問題なく動作しています。これを

駄目な例

countDown(‘時間切れ!’, 3)

とするとエラーで動作しません。

デフォルト -default-について

近年、口語で「デフォルト・デフォ」なんて言葉を聞くことが増えました。インターネットの影響か、あるいはゲームの影響かもしれません。

意味としては、標準値・初期値といった意味でデフォルト値という言葉使われ、そこから「標準である」といったような意味合いで使われています。一方で、経済のニュースなんかでたまに聞く債務不履行、デフォルト。なんだか意味が全然違いますよね? これはどういうことでしょうか。

実は元となった英単語のdefault は「怠慢」や「不履行」、「やるべきことをやらない」「あるはずのものがない」といった意味合いです。これが、プログラミングやサーバーの設定ファイルで、省略というか、「ないとどうしても困るのに、設定を忘れられてしまう」、つまりプログラマーやエンジニアが怠慢をしたときようにデフォルト値というものが用意されるようになりました。

そこから、GUIの発達やゲームなどで、怠慢なときに入れられる既定値がユーザーにも表示されるようになり、初期値といった意味合いから「標準的な設定」という意味に変化していったと考えられます。

言葉って面白いですね。

 

返り値(戻り値)のある関数

返り値(かえりち)、戻り値(もどりち)とは、関数を実行した結果を、関数を呼び出したところに返す値のことです。y = 3x + bの例で言えば、右辺を計算した結果を返り値として、yに代入するというイメージになります。

返り値を返すには

return 返り値

とします。返り値には、変数も定数も両方指定できます。

また、この return には実行された時点で、関数を終了するという、forループにおけるbreakと似たような性質も持っています。例えば、for 文の中でreturn を実行した場合は、forループも関数も終了して、関数の呼び出し元に戻る効果があります。

それでは、実験してみましょう。

chapter-10-1.pyを以下のように書き換えます。

import time

def countDown(sec=5, message='0'):
    if sec < 1:
        return '1以上の数字を入力してください。'
    for n in range(sec):
        m = sec - n
        if n > 9:
            return '長すぎます!'
        print(m, '秒前')
        time.sleep(1)
    print(message)
    return 'カウントダウンが終了しました。'

result = countDown(message='time up!')
print(result)
result = countDown(12)
print(result)
print(countDown(message='時間切れ!',sec=-1))

return を3つ追加していますが、最後の1つ以外if文のブロックの中であることに注意して、実行結果を見て下さい。

1度目のcountDown 関数の呼び出しでは、これまで通りループが完了します。そして、countDown関数の最後のreturn によって、’カウントダウンが終了しました。’という文字列が返り値として呼び出し元に返されます。返り値は、 result という変数に格納されて、その次の行のprint文で出力されます。

2度目のcountDown 関数の呼び出しでは、第一引数に12が渡されています。これによって、前項までであれば12秒のカウントダウンが行われるはずでした。しかし、for文の中に追加された if文の条件、 n > 9 によって、nに代入される数が10以上(9より大きい)ときにif文の中が実行されます。結果として、’長すぎます!’という文字列を返り値として、関数が終了しています。break文はないですが、以降のcountDown 関数の内容は一切実行されていないことを確認してくださいね。

最後の countDown 関数の呼び出しでは、 引数secに-1を指定しています。これは、countDown 関数の先頭に追加された if文の n < 1 の条件を満たして、すぐに関数が終了します。また、前2つのcountDown 関数の実行と違って、countDown関数をprint関数の引数に直接書いています。

このように、わざわざ変数に格納するまでもない返り値は、他の関数の引数として直接入力することも多くあります。ただし、やり過ぎると読みづらくなってしまうので、読みやすいように適宜変数に代入することも重要です。

色々な比較演算子

今回出て来た <, > のように、数値同士を比較する演算子は他にもあります。また、これらの演算子は、True(真), False(偽)という真偽値・bool型の数値を返します。if 文では、True のときに、ifブロックの中の内容を実行します。

  • == 数字が同じときにTrue
  • != 数字が違うときにTrue
  • < 前の数字が後ろの数字より小さいときにTrue
  • > 前の数字が後ろの数字より大きいときにTrue
  • <= 前の数字が後ろの数字以下のときにTrue
  • >= 前の数字が後ろの数字以上のときにTrue
  • not 後に続く真偽値を逆転する(TrueはFalse, FalseはTrue)。()と組み合わせないと意図しない結果になることがよくある

また、今回のcountDown関数のように、正しい数値を入力しないときちんと動作しない関数では先頭にif文を設定してちゃんとした値が渡されているかどうか確認するという方法が、実際のプログラミングの現場でもよく行われています。RPAでは定型動作が多いので必要ない場合も多いですが、ユーザーからの入力や、ファイルやブラウザの操作……と高度になっていくと必要になる場面も出て来ます。

定番のやり方として覚えておくと便利でしょう。

 

RPAを関数で整理する

それでは、前回作成した chapter-9-3.py を関数を作成して読みやすく・拡張しやすくしましょう。

chapter-9-3.pyを作成していない人は、前回の内容を復習しましょう。

関数化しやすいポイントに目を付ける

まず、関数化しやすそうなのは「指定のウィンドウがアクティブになるのを待つ」「指定のウィンドウがアクティブでなくなるのを待つ」です。何より、他の RPA ツールでひとつの機能化しているのですから、自分でも使いやすくするのはよさそうです。

まずは、chapter-9-3.py で作成した「指定のウィンドウがアクティブになるのを待つ」部分のソースコードを見てみます。

ag.hotkey('ctrl', 'shift', 's')
for n in range(10):
    a_win = gw.getActiveWindow()
    if a_win == '名前を付けて保存':
        break
    time.sleep(0.1)

引数として受け取りたい部分を考えてみます。まずは何より、「名前を付けて保存」と固定で入力されている、判定するウィンドウのタイトルですね。

それから、最大何秒くらい待機するかも設定できるといいかもしれません。ただ、これはデフォルト値を設定して面倒なとき・それほど重要でないときは省略できるようにしましょう。将来の自分の怠惰のために、丁寧にやりましょう。

では、chapter-10-2.pyとして、Python ファイルを作成しましょう。

import pyautogui as ag
import pygetwindow as gw
import time
import pyperclip as cb
import sys

def waitWindowActive(title, sec=10):
    if sec < 1:
        sec = 10
    
    for n in range(sec * 10):
        if gw.getActiveWindow().title == title:
            return
        time.sleep(0.1)

ここまでの復習と応用も兼ねて、少し新しい書き方も含めています。

まずは、関数の先頭の if ですが、待機時間が1秒未満だった際は、デフォルト値の10秒を設定しています。お好みで、小さい値が設定されているのだから、最小値の1にするなど、変えて見てください。

続く、for文のrange 関数ですが sec * 10という見慣れない書き方がされています。

これは、×(かける・乗算)に相当する * (アスタリスク、場合によってはスター)乗算演算子です。つまり、range関数に secが10倍された数字を渡しているということになります。何故ここで10倍しているかというと、time.sleepで待機している時間が0.1秒だからです。関数を使う人からしたら、何回チェックするか? と考えるより、何秒待つか? とだけ考えた方が楽なため、こうしています。

for 文の中の、ウィンドウが見つかったか判定するif文も、これまでは getActiveWindowの結果(返り値)を一旦変数に保存していました。しかし、ここではprint関数の引数に他の関数の実行結果をそのまま入れたように、直接 if文の判定式に入れています。

さらに、getActiveWindow().title という書き方になっている点も覚えておいてください。少し直感に反しますが(関数の実行が文だと考えていると、余計に)、これは関数の返り値に対して . (ドット)演算子を使い、中のメンバ変数にアクセスしている……という形になっています。

アクティブウィンドウのタイトルと、引数のtitleが一致したらbreak ではなく、 return で関数を終了しています。

ではこれで完成でしょうか?

 

ちょっと待って。もうちょっと便利にしましょう

例えば、前回メモ帳のアクティブ・非アクティブ状態を取得するには == 演算子ではなく、 in 演算子を使いました。理由は、ウィンドウタイトルが固定ではないからですね。

かといって、この関数で in 関数にしてしまうと、「名前を付けて保存」がアクティブになったか確認したいのに「名前を付けて保存の確認」ダイアログが引っかかってしまうかもしれません。

これを区別できるように引数を設定しましょう。もちろん、デフォルト値も設定しましょう。さぼったときに設定される値ですから、判定の緩い in 演算子をデフォルト値にしましょう。

それから、今のままだとウィンドウが見つかって関数が終了したのか、時間切れで終了したのか分かりません。これを返り値として呼び出し元に返して、 RPA の挙動を変えられるようにしてあげましょう。

さて、こういった On / Off や、上手く行った / 上手く行かなかった という区別には真偽値が便利です。値が2種類しかない上に、if 文にそのまま入れられて便利です。

ということで、関数を以下のように書き換えましょう(import文は省略しています)。

def waitWindowActive(title, sec=10, exact=False):
    if sec < 1:
        sec = 10
    print(title)
    for n in range(sec * 10):
        if exact:
            if gw.getActiveWindow().title == title:
                return True
        else:
            if title in gw.getActiveWindow().title:
                return True
        time.sleep(0.1)
    return False

変更点は、関数の末尾に return False としたことです。これで、関数の呼び出し元でif文を使って、ウィンドウが見つかったのか見つからないで時間が経過したのか分かる様になっています。

そして、引数にexactを追加して、デフォルト値をFalseとしています。これにTrueを設定したら完全一致(exact match)で判定する、という意味です。

for文の中に移動すると、if ~ else 文が追加され、入れ子構造になっていますね。引数のexact を if文で判断して、True(真)だったら == の完全一致で判定。Flase(偽)だったら in の部分一致(含まれているか)で判定しています。

これで、ウィンドウがアクティブになるのを待つ RPA を書くときは、 waitWindowActive 関数を使えば1行で済むようになりました。

 

ウィンドウが非アクティブになるのを待つ

この調子で、ウィンドウが非アクティブになるのを待つ関数も作りましょう。

判定する条件が反転だけなので、コピー&ペーストで作ると楽です。ただし、そうすると条件の変更を忘れたり、変数名を変え忘れたり、予期せぬ不具合(バグ)が紛れ込むことがとてもよくあります。注意しましょう。

def waitWindowNoneActive(title, sec=10, exact=False):
    if sec < 1:
        sec = 10
    for n in range(sec * 10):
        if exact:
            if gw.getActiveWindow().title != title:
                return True
        else:
            if title not in gw.getActiveWindow().title:
                return True
        time.sleep(0.1)
    return False

変更点は、getActiveWindow().title を評価する演算子をそれぞれ、 != と not in に変更している点です。

!= は数値の比較と一緒です。左右の値が異なれば True となります。

not in は in 演算子を真逆にしたものですね。title引数が アクティブなウィンドウのタイトルに一切含まれていなければ Trueとなります。

 

名前を付けて保存が閉じられるのを待つ関数を作る

RPA の複雑な処理を学ぶのには便利な「名前を付けて保存」ダイアログと、「名前を付けて保存の確認」ダイアログですが、毎回書くのはちょっと嫌ですね。

ということで、こちらも関数化してみましょう。その過程でwaitWindowNoneActive 関数も使って見ましょう。もちろん、自分でつくった関数を自分で作った関数の中で呼び出すことができますよ!

def waitClosingNamingDialog(forceSave=True, sec=3):
    if sec < 1:
        sec = 3
    if not(waitWindowNoneActive('名前を付けて保存', sec, True)):
        return False
        #名前を付けて保存ダイアログが、なんらかの理由で閉じなかった
    time.sleep(0.1)
    if gw.getActiveWindow().title == '名前を付けて保存の確認':
        if forceSave:
            ag.press('y')
        else:
            ag.press('n')
        if not(waitWindowNoneActive('名前を付けて保存の確認'), sec, True):
            return False
            #名前を付けて保存ダイアログが、なんらかの理由で閉じなかった
    return True

これが「名前を付けて保存」ダイアログが閉じられるのを待ち、さらに「名前を付けて保存の確認」ダイアログが表示されたらy キーを押すか、n キーを押して閉じる関数です。

引数 sec の処理の仕方はここまでと同じです。ファイルの保存を待つため、3秒をデフォルト値としていますが、10として他の関数と揃えるのもいいでしょう。

まずは、「名前を付けて保存」ダイアログを閉じる処理(enterキーの送信やマウスクリックなど)は既に行われているとして名前を付けて保存の確認ダイアログが閉じられるのを待ちます。また、ここでは exact 引数には必ず True を入力しましょう。何故なら、「名前を付けて保存の確認」ダイアログには「名前を付けて保存」という文字が含まれているため、 not in 演算子では「含まれている」と判定されてしまうからです。

続いて、「名前を付けて保存の確認」ダイアログがアクティブになっているかチェックしています。もし名前を付けて保存の確認ダイアログが表示されなければ、if文は入れ子になっている部分も含めて全てスキップされ、True を返します。このifの直前に0.1秒のsleepを入れているのは、ほんのちょっとの処理の遅れなどで表示が遅れることを考慮してのものです。なくても動作します。

if文の中では、引数のforceSave(強制保存)をチェックしています。Trueであれば上書きのためにyキーでダイアログを閉じ、そうでなければnキーでダイアログを閉じています。

そして、名前を付けて保存の確認ダイアログが閉じなければ、やはりFalseを返り値として、異常が起きたことを通知しています。

Falseで異常を示すのはよくない?

今回作成した関数では、時間切れなど、何かおかしな現象が発生したときに、 return Falseをするという処理にしています。これでも一応動作はします。

しかし、名前を付けて保存が閉じるのを待つ関数では、「名前を付けて保存の確認ダイアログが出ないで正常終了したのか、名前を付けて保存の確認ダイアログが出たのか(特にnで保存しない場合)、あるいは何か問題が起きたのか」という3パターンの結果を知りたい場合があります。

プログラミングではこのように、「何か問題が起きたよ」ということを通知する仕組みに、例外というものがあります。これはプログラムの作成を間違えたときに出るErrorのようなものを、プログラマーが作成したり、制御してエラーではないとしたりする仕組みです。

ただ、少し概念が難しいのでここでは触れません。将来的にはもっと便利な関数を自在に作れるんだということと、正常な動作の完了と、何か異常が起きたことを通知する仕組みは本当なら分けた方がスマートだということを覚えておきましょう。

[/type]

おわりに

ここまで頑張った方、お疲れ様でした。関数の基本を紹介しました。関数は、Python で RPA を作成するときも便利でよく使います。むしろ、普通のプログラミングよりも、引数も返り値もない関数をよく作ると言っていいかもしれません。それくらい、PCの操作というのは定型的な繰り返しに満ちています。もちろん、引数や返り値を利用すれば、何度も書くのが面倒な処理を、ちょっとの手間だけで書けるようになる場合も多くあります。

そうなってくると、GUIの RPA ツールなんて面倒くさい! Python で書く方が楽だ! と段々なってきます。とくに、ソフトウェアのバージョンアップなどで動作が変わった場合などは、GUIでいじるのがちょっと億劫でも、プログラムなら簡単……という場合も多々あります。

また、今回は、「実際に処理を関数にするときの考え方」も基本的な部分ですがご紹介しました。「どこを関数にしたらいいかなんて、自分じゃ分からない!」と思う方も安心してください。楽介も、今回のように「一回書いてしまってから、再利用するために関数化する」ということがよくあります。

実際には再利用しなくても、if文やfor文が長くなり、複雑化していくと読みづらく、保守(バグの修正やバージョンアップ)作業が大変になります。そのためにも関数化しておくことは有効です。最初はできるところからで構わないので、慣れていきましょう。

今回のまとめ
  • 関数の宣言は def
  • 関数も字下げでブロックにする
  • return で関数を終了し、返り値を呼び出し元に返せる
  • デフォルト値は緩い方、よく使われる方を設定する
  • 関数の先頭で if文を使って引数のチェックは、とてもよくやる!

この連載を読んで、「プログラマーになるのも、面白そうかも」と思った方は、転職活動をスタートしてみるのもひとつの手段です。未経験とはいえ、かなり「プログラマー的」知識がついています。

何より、こういった情報でポジティブな気分になれる方はプログラマーに向いていると言えます。そうしたら、一人で頑張って勉強するのも一つですが、いっそのことプロの世界に飛び込んでがんがんスキルを学ぶと成長も早いですし、何より楽しい仕事ができますよ。

GeekJobでは、未経験からのエンジニアへの転職を支援しています。詳しくは以下の記事を参考にしてください。

最短でプログラマーに転職したい?なら最初に転職活動をするべし!

FacebooktwitterredditpinterestlinkedinmailFacebooktwitterredditpinterestlinkedinmail

「関数覚えてPython RPAの面倒を大幅削減!」への1件のフィードバック

  1. ピンバック: Python RPA でウィンドウや画像が出るのを待つ | 楽しい生産性ブログ

コメントする

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください