数弱の文系大学生によるブログです。

Python、Webデザイン関連の記事を投稿していけたらなと思っています。

備忘録的なブログなので、読みにくい文章だと思います。ご了承ください。

Python3の復習(関数)

こんにちは。

かにたまごです。
今回も復習記事です。
Python3の復習以外にも上げたい記事はあるのですが、復習意欲があるうちはコッチを優先して書いていきたいと思っています。


さて、今回は

  • 関数

について、復習していきたいと思います。


今までのPython3 復習履歴は以下の通り。

  1. 数値・文字列
  2. リスト・タプル
  3. 辞書・集合
  4. if文・while文・for文
  5. 内包表記

関数

まとまった処理ができる箱のようなもの(すごい曖昧)。
よく使うようなコードであれば関数としてまとめておくことで、いちいちその都度書かなくて済む。

  • 関数の定義

関数を定義するにはdef文を先頭に記述する必要がある(define: 定義する の略ってことらしい)。
javascriptでいうところのfunction、C#だとアクセス修飾子+戻り値の型みたいな関数を定義する際のお決まり事。


おなじみの はろーわーるど。

def hello():
    print("Hello World!!!")

関数名() で関数を呼び出す。

hello()

出力 ->
Hello World!!!

return文で処理結果を呼び出し元に返す。

def hello():
    return_text = "Hello World!!!"
    return return_text

returned_text = hello()
print(returned_text)

出力 ->
Hello World!!!

引数を指定することで、渡された値を関数内で処理することができる。

def introduce(name, gender):
    text = "私は"+name+"です。\n"+"性別は"+gender+"です。"
    return text

result = introduce("かにたまご", "男")
print(result)

出力 ->
私はかにたまごです。
性別は男です。


補足説明
引数には2種類あるようで、呼び出し元で渡す引数を実引数、関数に渡される引数を仮引数と呼ぶらしい。流れとしては、関数を呼び出すときに渡される実引数が関数内の仮引数にコピーされ、処理された結果が返ってくるという感じ。

該当箇所は以下。

実引数 -> introduce("かにたまご", "男") の "かにたまご"と"男"
仮引数 -> def introduce(name, gender) の "name"と"gender"

  • 引数のあれこれ


位置引数:
先頭から順に、対応する位置の仮引数にコピーされるタイプ。

def login(id, name, password):
    id = "@"+id
    name = name+"さん"
    password = "*"*len(password)
    return {"id":id, "name":name, "password":password}

result = login("aiueoookaki", "かにたまご", "8375937927")
print(result)

出力 ->
{'id': '@aiueoookaki', 'name': 'かにたまごさん', 'password': '**********'}

ただ、この場合引数の位置を覚えておかないと全く違った結果になってしまうので気を付けなければならない。

result = login("aiueoookaki", "8375937927", "かにたまご")
print(result)

出力 ->
{'id': '@aiueoookaki', 'name': '8375937927さん', 'password': '*****'}


キ-ワード引数:
位置引数の混乱を避けるために、「どの仮引数に何を渡すのか」を明示的に指定するタイプ。

result = login(id="aiueoookaki", password="8375937927", name="かにたまご")
print(result)

出力 ->
{'id': '@aiueoookaki', 'name': 'かにたまごさん', 'password': '**********'}

これで渡す順番が違っても安心。

デフォルト引数値の指定:
呼び出し時に、引数に値が渡されなかった場合に使われるデフォルト値。

def login(id, password, name="名無し"):
    id = "@"+id
    name = name+"さん"
    password = "*"*len(password)
    return {"id":id, "name":name, "password":password}

result = login(id="aiueoookaki", password="8375937927")
print(result)

出力 ->
{'id': '@aiueoookaki', 'name': '名無しさん', 'password': '**********'}

全ての仮引数にデフォルト値を設定しない場合、デフォルト引数値は最後に記述しなければならない

def login(id, name="名無し", password):
    id = "@"+id
    name = name+"さん"
    password = "*"*len(password)
    return {"id":id, "name":name, "password":password}

result = login(id="aiueoookaki", password="8375937927")
print(result)
  File "C:\Users\***\Desktop\Python\Test\blog_test.py", line 1
    def login(id, name="名無し", password):
             ^
SyntaxError: non-default argument follows default argument


ん??ってなったところ。

デフォルト値で毎回空のリストを定義したい場合:

def error_function(word, word_list=[]):
    word_list.append(word)
    print(word_list)

error_function("word1")
error_function("word2")

出力 ->
['word1']
['word1', 'word2']

本来の意図としては、渡されたwordを毎回空のリストにぶっこむ(繰り返し)ことだけど、一回目の引数が入ったリストに二回目の呼び出しで渡された引数も入ってしまっている。この理由は、デフォルト値の値が決定するのは呼び出された時ではなく、関数が定義されたときだからだ。

ん~???頭がパニック。
深く考えすぎないのもアリだけど、せっかく復習してるんだからちゃんと理解したい。

ここからは自分なりの考えを書いておく。
・関数が定義された時点で、デフォルト値が決定する
・この現象は、リストや辞書などのデータ型に限定される
リストや辞書などは参照型である

これらを踏まえると
・関数が定義された時点で、リストや辞書の参照する場所が決定する
・参照する場所が毎回同じなので、後から追加される値も含まれてしまう
という風に解釈できないかな・・・?

とりあえず自分の中ではこれで落ち着いた(;^ω^)

以下のようにすれば、正しく動作する。

#呼び出すたびに新しいリストを生成する
def correct_function(word):
    word_list = []
    word_list.append(word)
    print(word_list)

correct_function("word1")
correct_function("word2")

出力 ->
['word1']
['word2']

あるいは

#Noneをデフォルト値として渡しておく
def correct_function(word, word_list=None):
    if word_list == None:
        word_list = []
    word_list.append(word)
    print(word_list)

correct_function("word1")
correct_function("word2")

出力 ->
['word1']
['word2']

位置引数のタプル化:
今まで見てきた仮引数は、記述した分のみ受け取ることしかできない。
しかし、*(アスタリスク)をつけた仮引数を定義すると、可変個の引数をタプルとして受け取ることができる。いわゆる可変長引数ってやつ。

def hikii(*args):
    print("args contain: ", args)

hikii("埋", "大平", "シルフィン", "海老名", "切絵")

出力 ->
args contain:  ('埋', '大平', 'シルフィン', '海老名', '切絵')

他の仮引数と併用する場合は最後に記述しなければならない。

def wrong_hikii(name, *anything, gender):
    print("名前: ", name)
    print("性別: ", gender)
    [print("その他"+str(idx+1)+": ", thing) for idx, thing in enumerate(anything)]

wrong_hikii("かにたまご", "男", "アニメ見たい", "ゲームしたい", "旅行に行きたい")
(main) C:\Users\***\Desktop\Python\Test>python blog_test.py
Traceback (most recent call last):
  File "blog_test.py", line 1, in <module>
    wrong_hikii("かにたまご", "男", "アニメ見たい", "ゲームしたい", "旅行に行き たい")
TypeError: wrong_hikii() missing 1 required keyword-only argument: 'gender'
def correct_hikii(name, gender, *anything):
    print("名前: ", name)
    print("性別: ", gender)
    [print("その他"+str(idx+1)+": ", thing) for idx, thing in enumerate(anything)]

correct_hikii("かにたまご", "男", "アニメ見たい", "ゲームしたい", "旅行に行きたい")

出力 -> 
名前:  かにたまご
性別:  男
その他1:  アニメ見たい
その他2:  ゲームしたい
その他3:  旅行に行きたい

キーワード引数の辞書化:
**(アスタリスク二つ)をつけた仮引数を定義すると、引数の名前(キー):引数の値(辞書の値)として関数に辞書を渡すことができる。

def keyword_hikii(**kwargs):
    print("kwargs contain: ", kwargs)

keyword_hikii(name="かにたまご", gender="男")

出力 ->
kwargs contain:  {'name': 'かにたまご', 'gender': '男'}


*argsと**kwargsを同時に定義する場合、 (*args, **kwargs)の順で並べなければならない。

def kwargs_args(**kwargs, *args):
    print("可変長引数: ", args)
    print("キーワード可変長引数: ", kwargs)
(main) C:\Users\***\Desktop\Python\Test>python blog_test.py
  File "blog_test.py", line 1
    def kwargs_args(**kwargs, *args):
                            ^
SyntaxError: invalid syntax
def args_kwargs(*args, **kwargs):
    print("可変長引数: ", args)
    print("キーワード可変長引数: ", kwargs)

args_kwargs("かにたまご", "男", age=22, blood_type="A")

出力 ->
可変長引数:  ('かにたまご', '男')
キーワード可変長引数:  {'age': 22, 'blood_type': 'A'}
  • 関数にドキュメントをつける

可読性を高めるには、コードをシンプルに書くこと(=読みやすいコード)だけでなく、内容を素早く理解させること(=把握しやすいコード)が重要。そのためにコメントを記述したり、分かりやすい変数名をつけたりする。
関数においては「ドキュメント」を定義すること(docstring)で、その関数が何を処理してくれるものなのかをより把握しやすくしてくれる。

def type_is(something):
    """
    仮引数に与えられた値の型をチェックする。
    戻り値はデータ型
    """
    result = type(something)
    return result
result = type_is(1)
print(result)

出力 ->
<class 'int'>

ドキュメントを確認するには、help関数もしくは__doc__を使用する。

#help関数を使用した場合:
#整形されたdocstringが返ってくる
help(type_is)

出力 ->
Help on function type_is in module __main__:

type_is(something)
    仮引数に与えられた値の型をチェックする。
    戻り値はデータ型


#__doc__を使用した場合:
#素のdocstringが返ってくる
print(__doc__)

出力 ->
    仮引数に与えられた値の型をチェックする。
    戻り値はデータ型
  • 関数もオブジェクト

変数に関数を代入したり、引数に関数をとったり、戻り値として関数を返したりすることができる。

変数に関数を代入:

def hello():
    print("Hello World")

function = hello
function()

出力 ->
Hello World

引数に関数をとる:

def hello(name):
    print("こんにちは ", name, " さん")

def hello_plus(func, name):
    func(name)

hello_plus(hello, "かにたまご")

出力 ->
こんにちは  かにたまご  さん

戻り値として関数を返す:

def squaring_nums(*args):
    result = sum([num**2 for num in args])
    return result

def return_squaring_nums(func, *args):
    return func(*args)

result = return_squaring_nums(squaring_nums, 10, 100, 1000)
print(result)

出力 ->
1010100
  • 関数内関数

なんだこのゲシュタルト崩壊起こしそうな単元。
関数の中に関数を定義できるってことらしい。
頭がこんがらがる~\(~o~)/

def login_check(func, name, password):
    def check(c_name, c_password):
        result = None
        if c_name == "" or c_password == "":
            result = "どちらかが入力されていません"
        else:
            result = func(c_name, c_password)
        return result
    return check(name, password)

def login(name, password):
    text = """
        こんにちは、{}さん。
        ログインが完了しました。
    """.format(name)
    return text

result = login_check(login, "かにたまご", "8475279")
print(result)

出力 ->

        こんにちは、かにたまごさん。
        ログインが完了しました。


result = login_check(login, "かにたまご", "")
print(result)

出力 ->
どちらかが入力されていません

他の関数によって動的に生成される関数らしい。
関数内関数の外で受け取った引数の値を覚えておいたり、変更したりできるってのが特徴らしい。
そうらしい。

( ^ω^)・・・

先ほどのコードを以下のようにするとクロージャにできる、のかな?

def login_check(func, name, password):
    def check():
        if name == "" or password == "":
            print("どちらかが入力されていません")
        else:
            func(name, password)
    return check

def login(name, password):
    text = """
        こんにちは、{}さん。
        ログインが完了しました。
    """.format(name)
    print(text)

success_login = login_check(login, "かにたまご", "7943709")
error_login = login_check(login, "", "98794327")

確認してみる。

print(type(success_login))
#<class 'function'>
print(type(error_login))
#<class 'function'>

print(success_login)
#<function login_check.<locals>.check at 0x02B61C90>
print(error_login)
#<function login_check.<locals>.check at 0x02B61C48>

呼び出してみる。

success_login()

        こんにちは、かにたまごさん。
        ログインが完了しました。

error_login()
どちらかが入力されていません


使い方が違う気がする・・・。

  • 無名関数:ラムダ関数

一つの文で表現される無名関数。
複雑な処理でなければ、ラムダで関数定義したほうが簡潔。

lambda 引数: 処理

という形式で定義する。

例えば、リストを受け取りそれらの文字数をカウントするという処理をしたい場合、通常の関数を使用すると以下のようになる。

#リストを受け取り、それらの文字数をカウントする
word_list = ["apple", "orange", "strawberry", "lemon", "pineapple"]

def count_word(word_list, method):
    result = {word:method(word) for word in word_list}
    return result

def length_word(word):
    return len(word)

result = count_word(word_list, length_word)
print(result)

出力 ->
{'apple': 5, 'orange': 6, 'strawberry': 10, 'lemon': 5, 'pineapple': 9}

リストの値をカウントするだけならこれでいいような気もする。
しかし、大文字にしたり小文字にしたり...etc 様々な処理を加えたい場合、以下のように全体のコードが長ったらしくなる。

#リストを受け取り、それらの文字数をカウントする
word_list = ["apple", "orange", "strawberry", "lemon", "pineapple"]

def count_word(word_list, method):
    result = {word:method(word) for word in word_list}
    return result

def length_word(word):
    return len(word)

def upper_word(word):
    return word.upper()

def lower_word(word):
    return word.lower()

def join_space(word):
    return " ".join(word)

result1 = count_word(word_list, length_word)
result2 = count_word(word_list, upper_word)
result3 = count_word(word_list, lower_word)
result4 = count_word(word_list, join_space)

print(result1)
print(result2)
print(result3)
print(result4)

出力 ->
{'apple': 5, 'orange': 6, 'strawberry': 10, 'lemon': 5, 'pineapple': 9}
{'apple': 'APPLE', 'orange': 'ORANGE', 'strawberry': 'STRAWBERRY', 'lemon': 'LEMON', 'pineapple': 'PINEAPPLE'}
{'apple': 'apple', 'orange': 'orange', 'strawberry': 'strawberry', 'lemon': 'lemon', 'pineapple': 'pineapple'}
{'apple': 'a p p l e', 'orange': 'o r a n g e', 'strawberry': 's t r a w b e r r y', 'lemon': 'l e m o n', 'pineapple': 'p i n e a p p l e'}

これをラムダで記述すると、多少ではあるが簡潔になる。

result1 = count_word(word_list, lambda word: len(word))
result2 = count_word(word_list, lambda word: word.upper())
result3 = count_word(word_list, lambda word: word.lower())
result4 = count_word(word_list, lambda word: " ".join(word))

print(result1)
print(result2)
print(result3)
print(result4)

出力 ->
{'apple': 5, 'orange': 6, 'strawberry': 10, 'lemon': 5, 'pineapple': 9}
{'apple': 'APPLE', 'orange': 'ORANGE', 'strawberry': 'STRAWBERRY', 'lemon': 'LEMON', 'pineapple': 'PINEAPPLE'}
{'apple': 'apple', 'orange': 'orange', 'strawberry': 'strawberry', 'lemon': 'lemon', 'pineapple': 'pineapple'}
{'apple': 'a p p l e', 'orange': 'o r a n g e', 'strawberry': 's t r a w b e r r y', 'lemon': 'l e m o n', 'pineapple': 'p i n e a p p l e'}

要は、その場限りの処理(=反復して使用しないコード)はラムダで書いた方がシンプルで素敵・・・ってことかな。あと、GUIでコールバック関数を定義する際にラムダが役立つらしい・・・。ふーん。



関数一つとっても知らないことだらけだし、その上それを使いこなすレベルに持ってくとなると相当時間がかかるよね・・・。
地道に、コツコツと頑張ってこ。


以上です。