はじめに
今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #01
次の記事:Python初心者の備忘録 #03
今回の記事は関数(function)についてまとめてあります。
■学習に使用している資料
Udemy:米国AI開発者がゼロから教えるPython入門講座
■関数(function)
▶関数の基本
関数とは処理を格納しているfunction型のオブジェクトである。(type()
で型を確認することができる)
変数は値を格納していたが、関数はそれが処理になっている。
何度も実行する必要がある処理などを関数として定義して、再利用できるようにしたり、処理を修正する際に複数個所を修正しなくてもいいようにするのが目的。
関数には外から値を与えてあげて関数内で使用することができ、処理の結果を関数の外に取り出すことができる。それぞれ外から与える値を引数、外に取り出す値を戻り値という。
関数の後は2行開けるのが通例で、命名規則は変数と同じ。
fahrenheit = 72
# 華氏から摂氏に変換する関数
def fahrenheit_to_celsius(fahrenheit): # 受け取った引数をfahrenheitという変数に格納して関数内で使用できるようにしている。
celsius = (fahrenheit - 32) * 5 / 9
return celsius # returnで戻り値を指定することができる
# 関数の戻り値を変数に格納している
celsius = fahrenheit_to_celsius(fahrenheit) # fahrenheitという変数を引数として関数に渡している
print(celsius)
# 関数はfunction型のオブジェクト(この時、関数名の後ろに「()」は付けない)
print(type(fahrenheit_to_celsius))
▶return(戻り値)
関数を呼び出して実行した処理の結果を受け取ることができ、その受け取る値やオブジェクトを戻り値という。
関数内でreturn
の後に書いた値やオブジェクトがその関数の戻り値となる。
もちろんreturn
を定義しなくてもよく、その時はNone
を戻り値として返却している。
戻り値はreturn
の後にカンマ区切りで複数定義することができ、その際の戻り値はtuple型で返却されるので、unpackで受け取ることもできる。
# returnしない関数を作ることもできる
def print_dict(input_dict):
for k, v in input_dict.items():
print(f"key: {k}, value: {v}")
a = {"one": 1, "two": 2}
print(a)
print_dict(a)
# returnしない関数は,Noneをreturnしている.
return_value = print_dict(a)
print(return_value) # None
# 複数returnする場合は,(カンマ)で区切って渡す
def get_first_last_word(text):
text = text.replace(",", "")
words = text.split()
return words[0], words[-1]
text = "Hello, My name is Mike"
# 複数戻り値があると,tuppleで渡されるので,unpackで受け取る
first, last = get_first_last_word(text)
print(f'first word is {first}, last word is {last}')
▶引数(parameter)
関数内で使用する値を関数の外から与えることができ、その与える値を引数という。
関数を定義する際に受け取る引数の数やデフォルトの値も定義することができ、関数を呼び出す際は定義した受け取る引数の数と同じ数の値を与えなければ、その関数を呼び出すことができないので注意。
しかし、引数のデフォルトを定義している場合は、与える引数の数が同じでなくても呼び出すことができる。
def func(first, second, third):
print(f"first: {first}, second: {second}, third: {third}")
# 順番通りに入れる
func("1", "2", "3") # -> first: 1, second: 2, third: 3
# 順番通りに入れない場合は引数を指定する
func("1", third="3", second="2") # -> first: 1, second: 2, third: 3
# デフォルトの値を指定する場合は,位置引数(positional parameter)より後に書く
def func(first, second, third="3"):
print(f"first: {first}, second: {second}, third: {third}")
# 与える引数の数が定義した引数の数と違うが、デフォルトで定義している分は与えなくても大丈夫
func("1", "2") # -> first: 1, second: 2, third: 3
▶ *argsと**kwargs
関数を定義する際の受け取る引数の数を動的にすることができ、その際に*args
や**kwargs
を使用する。
・*args:与えられた引数をargsというリストオブジェクトに格納して使用できる。
・**kwargs:*argsのdictionary版、引数を渡す際にkey=value
という形で渡すことができる。
# *args: 不特定多数の引数を渡すことが可能
def get_average(*args):
num = len(args)
if num == 0:
return 0
total = sum(args)
return total / num
average = get_average(1, 2, 3)
print(average)
# **kwargs: *argsのdictionary版(キーワード付き引数)
def kwargs_func(**kwargs):
param1 = kwargs.get('param1', 1)
param2 = kwargs.get('param2', 2)
param3 = kwargs.get('param3', 3)
print({f'param1: {param1}, param1: {param2}, param3: {param3}'})
# param4は関数内で使われていないが,引数として渡すことが可能
kwargs_func(param1=10, param2=6, param4=4)
# 定義する際は位置引数, キーワード引数, *args, **kwargsの順
def func(positional, keyword='default', *args, **kwargs):
print(positional, keyword, args, kwargs)
# *と**の正体はunpacking operator
numbers = (1, 2, 3)
print(numbers) # -> (1, 2, 3) # print((1, 2, 3))と同じ
print(*numbers) # -> 1 2 3 # print(1, 2, 3)と同じ
a = {'a': 1, 'b': 2}
b = {'c': 3, 'd': 4}
c = {**a, **b} # -> {'a': 1, 'b': 2, 'c': 3, 'd': 4}
print(c)
▶値渡しと参照渡し
引数を渡す際にその引数がmutableかimmutableかに注意しなければならない。
まず、よく勘違いされるがPythonに参照渡しはない。
Pythonは参照渡しのような挙動をするが、実際は参照の値渡し(メモリのアドレスを渡している)を行っている。
同じメモリのアドレスを参照しているので、関数内で引数を更新してしまうと引数として与えたオブジェクトも同時に更新してしまい、場合によってはシステムが壊れてしまう可能性がある。
immutableのオブジェクトは更新ができず、新しい値を代入した際にその値をオブジェクトとしてメモリ内に新しく登録し、その新しいオブジェクトを参照するという挙動をとるので、関数内で値を更新しても外にまで影響を与えず、見た目は値渡しのような挙動をとる。
※参照サイト⇒Pythonの引数における参照渡しと値渡しについて
# Pythonは参照の値渡し
def add_nums(a, b):
print(f"第一引数のID:{id(a)}")
print(f"第二引数のID:{id(b)}")
return a + b
one = 1
two = 2
# oneは1, twoは2というオブジェクトを参照している
print(id(one))
print(id(two))
# add_nums内でも1, 2というオブジェクトを参照しているのでidは同じ
add_nums(one, two)
# immutableでは変数を更新できないので値渡しのような挙動をする
def add_one(num):
print(f"変更前のID:{id(num)}")
num += 1
print(f"変更後のID:{id(num)}")
return num
one = 1
print(id(one))
print(f'関数呼び出し前のone:{one}')
add_one(one)
print(f'関数呼び出し後のone:{one}')
# mutableでは変数を更新できるので(そのまま)参照渡しのような挙動となる
def add_fruit(fruits, fruit):
print(f"変更前のID:{id(fruits)}")
fruits.append(fruit)
print(f"変更後のID:{id(fruits)}")
return fruits
myfruits = ['apple', 'banana', 'peach']
myfruit = 'lemon'
print(f'関数呼び出し前のmyfruits{myfruits}')
add_fruit(myfruits, myfruit)
print(f'関数呼び出し後のmyfruits{myfruits}') # -> 関数内で更新したのに、外側のmyfruitsも更新されている
▶Type anntation(Type hint)
関数を定義する際に引数は「:」の後に、戻り値は「->」の後に型を定義することでどの型の引数を与えればいい関数なのか、戻り値はどんな型で返却されるのかをヒントとして定義することができる。
しかし、これはあくまでコメントのようなものなので、システム的な制限力はなく、annotationで定義した型以外の引数を渡すこともできる。
※そもそもあまり意味がないので、よほどの理由がない限りは後述のdocstringを使用する
※mypyなどのライブラリを活用することで静的型チェック(annotationで付与した型以外はエラーとなる)を実装することもできる
# 変数に対してもType hintをつけることができる
num: int
string: str = "Type Annotation"
# 引数と戻り値の型のヒントをつけることができる.(annotation)
def add_nums(num1: int, num2: int) -> int:
return num1 + num2
print(add_nums(1, 2)) # -> 3
# int型ではなく、str型でも引数として渡すことができる
print(add_nums("1", "2")) # -> 12
▶変数のスコープ(scope)
scopeとは定義した変数や関数、その他オブジェクトがどの範囲で有効か(使用できるか)ということを考える際に使用する箱、範囲のようなもの。
基本的に関数(def)やif文、for文などインデントが下がるたびにscopeが変わるというように考えておけばいい。
上のscopeのオブジェクトは下のscopeから呼び出すことはできるが、下のscopeのオブジェクトを上のscopeから呼び出すことはできない。
もし下のscopeのオブジェクトを使用したい場合はreturnする必要がある。
特にインデントが下がっていないscopeはglobal scopeと呼ばれ、インデントが下がっているscopeはlocal scopeと呼ばれる。
globalとlocalで同じ名前のオブジェクトを使用している場合は、localのオブジェクトが優先される。
local scopeでglobal scopeのオブジェクトを明示的に使用したい場合はオブジェクトを定義するときに名前の前にglobal
とつければよい。
# =================global scope=================
age = 30
# =================local scope=================
def print_name_local():
first_name = 'Taro'
last_name = 'Yamada'
print(f"I'm {first_name} {last_name}")
# =================local scope=================
print_name_local()
def print_age():
# この時ageはlocal scopeとなる
age = 20
print(f"I'm {age} years old")
print_age()
# global scopeのageは関数内で変更されていない
print(age)
def print_global_age():
# global scopeのageを参照することができる
global age
age = 20
print(f"I'm {age} years old")
print_global_age()
# global scopeのageの値が更新されている
print(age)
# =================global scope=================
▶関数のネスト(nested)
ネストとは関数の中にさらに関数を定義したり、if文の中にまたif文を作成したりする書き方で、入れ子構造になっているもののこと。
# 関数の中で関数を定義(nested function)
def outer():
def inner():
print("this is inner function")
# outer関数内でinner関数を呼び出している
inner()
outer()
# scopeの考え方と同じで、外から中のオブジェクトを呼び出すことはできない
inner() # -> エラーになる
# inner関数はouter関数の変数にアクセス可能
def outer(outer_param):
def inner():
print(f"this is inner function, and I can access to {outer_param}")
inner()
outer("arg")
# outer関数はinner関数のローカル変数にアクセスはできない
def outer():
def inner():
inner_variable = "inner var"
print("this is inner function")
inner()
print(inner_variable) # -> エラーなる
# ネストされているときに上の階層のオブジェクトを明示的に使用したい場合は名前の前にnonlocalとつける
msg = "I am global"
def outer():
msg = "I am outer"
def inner():
nonlocal msg # msgはnonlocal変数となる
msg = "I am inner"
print(msg)
inner()
print(msg)
print(msg)
outer()
▶カプセル化(encapsulation)
ネストを使用して、外から直接アクセスできないような構造にすることで、不必要なアクセスによるデータの破損やシステムの崩壊を防ぐことができる。
例えば、必ずinputされたデータのvalidationを行ってからDB登録の関数を呼び出せる形をとることで、不正なデータがDBにinsertされないようにするといったことができる。
他にも、ネストで関数ごとに役割を分離することによって、それぞれの関数が1つの機能、責任に集中することができるので保守性が向上するという考え方もある(レスポンシビリティの分離)
# カプセル化(encapsulation): 外からアクセスできないようにする
# outerではvalidationを書き, innerには実際のプログラムを書くことで,役割を分けることができる
def casino_entrance(age, min_age=21):
if age < min_age:
print(f"{min_age}歳未満お断り")
return
def inner_casino_entrance():
print("ようこそカジノへ")
inner_casino_entrance()
casino_entrance(27)
▶クロージャ(closure)
ネスト関数を使用して、状態を静的、動的に保持した状態の関数を変数に代入することができる。
return
でネストの内側の関数を戻り値として返却することで、引数Aというオブジェクトを持ったネスト関数に引数Bを与えたり、引数Cを与えたり状況に応じて柔軟に処理を行うことができる。
# 関数(function)もオブジェクトなので、変数やlist、dictionaryの要素として入れることが可能
def compute_square(num):
return num * num
f = compute_square
function_list = ["1", 1, True, f]
print(function_list[-1](10))
# 関数を引数に渡したりもできる
def execute_func(func, param):
return func(param)
print(execute_func(f, 10))
# 関数をreturnすることもできる
def return_func():
def inner_func():
print('This is an inner function')
# ()で実行していないことに注意.この場合,関数のオブジェクトが返される
return inner_func
f = return_func()
print(f)
print(type(f))
f()
# Closure: 状態をキープした関数を作ることができる
# 状態が静的の例
def power(exponent):
def inner_power(base):
return base ** exponent
return inner_power
power_four = power(4)
print(power_four)
print(power_four(2)) # -> 16
print(power_four(3)) # -> 81
# 状態が動的の例
def average():
nums = []
def inner_average(num):
nums.append(num)
return sum(nums) / len(nums)
return inner_average
average_nums = average()
# 関数を呼び出すたびにnumsリストに引数が格納されていく
print(average_nums(5)) # -> 5.0
print(average_nums(15)) # -> 10.0
print(average_nums(4)) # -> 8.0
print(average_nums(10)) # -> 8.5
▶デコレータ(decorator)
ある関数に対してなにかちょっとした処理を付け足した関数を定義したいという場合に使用する。
処理を足したい関数の関数名を@
の後に続けて書き、その下に足したい処理を新しい関数として定義することで元の関数の処理にちょっとした処理を付け足した関数を新しく定義することができる。
# デコレート元はfunc、*args、**kwargsを使用して下記のようにコードするのがテンプレとなっている
def greeting(func):
def inner(*args, **kwargs):
print("Hello!")
# ここにデコレートされた処理が入る
func(*args, **kwargs)
print("Nice to meet you!")
return inner
@greeting # Decorator
def say_name(name):
print(f"I'm {name}")
say_name("Jiro")
@greeting # Decorator
def say_name_and_origin(name, origin):
print(f"I'm {name}, I'm from {origin}")
say_name("Taro")
say_name_and_origin(name="Jiro", origin="Tokyo")
▶再帰関数(recursive function)
関数内で自分自身を呼ぶような形で定義する関数で、何度も同じような処理を前回の処理結果をもとに繰り返したい時などによく使われる。
しかし、単純に処理に膨大な時間がかかったりメモリなどのリソース不足によるエラーの原因になったりするのであまり推奨されていない。
しかし、普通に書けば面倒な処理も簡単に書けたりするのでその点はメリットともいえる。
# 一見大きく見える問題も
# n! = n * (n-1) * (n-2) * ... * 1
# 小さい問題を繰り返すことで解決できる
# n! = n * (n-1)!
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
print(factorial(100))
Challenge
- fibonacci数列のn番目の数字を出力する処理を再帰関数で実装してみよう。
fibonacci数列:ひとつ前とふたつ前の数字を足したものを並べる(0,1,1,2,3,5,8,13,21,...)
例)fibonacci_recursive(6) -> 8 - 再帰関数を使用せずに上記の関数を定義し、それぞれの処理スピードを比べる
※それぞれの引数に50とかを渡すと顕著にわかる
1の解答例
# challenge1
def fibonacci_recursive(n):
if n < 2:
return n
else:
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
2の解答例
# challenge2
def fibonacci(n):
if n < 2:
return n
else:
n_1 = 1
n_2 = 0
for _ in range(n-1):
result = n_2 + n_1
n_2 = n_1
n_1 = result
return result
# 再帰の方が時間がかかる
for i in range(50):
# print(i, fibonacci_recursive(i))
# print(i, fibonacci(i))
▶ラムダ関数(lambda)
無名関数とも呼ばれ、わざわざ名前を付ける必要もないほど簡単で短い処理に対して使用される書き方。
不必要な部分(def
、関数名、return
など)を排除して、必要最低限(引数や処理)を残す。
ラムダ関数を定義する際はdef
の代わりにlambda
と書く。
# 通常の関数
def power(x):
return x * x
# lambda関数 ※普通はlambdaを変数には格納しない
p = lambda x: x * x
print(power(4)) # -> 16
print(p(4)) # -> 16
# lambda関数をreturnするケース
def power(exponent):
return lambda base: base ** exponent
third_power = power(3)
print(third_power(2))
# 引数にlambda関数を入れるケース
numbers = [6, 2, 5, 43, 5, 36, 67, 2]
filtered_nums = filter(lambda x: x % 2, numbers)
for num in filtered_nums:
print(num)
▶docstring
関数を定義する際にリファレンスをつけることができる。
リファレンスとは関数にカーソルを合わせた際に出てくるその関数の説明のようなもの。
※使用しているIDEやテキストエディタでは表示されないこともある
基本的に書き方は会社や現場に合わせて書けばいいが、自分が定義した関数には必ず書くようにする。
関数の処理部分の先頭に"""
で囲んで記述する。
def multiply(num1, num2):
"""
multiply num1 with num2 and return the result
:param num1: first number that you want to multiply
:param num2: second number that you want to multiply
:return: num1 * num2
"""
return num1 * num2
# .__doc__に文字列が格納されている
print(multiply.__doc__)
# help()でもDocstringを表示することができる
help(multiply)
multiply(3, 5)
# Google styleの例
# preference -> tools -> python integrated tools -> Docstringsで変更可能(PyCharmの場合)
def dividend(num1, num2):
"""
num1 is divided by num2 and return the result
Args:
num1: number that you want to divide
num2: number that num1 is divided by
Returns:
num1 / num2
"""
return num1 / num2