ゼロから始めるPython(4)

ゼロから始めるPythonの4回目になります。
[三回目はこちら](https://qiita.com/wind0411/items/1e406fab05e681054814)

#おさらい

前回まで、チュートリアルを読みつつPythonの持つ特性や機能をまとめていました。
長くなりましたので分けて投稿します。


#チュートリアル

[Python3.7 チュートリアル](https://docs.python.org/ja/3.7/tutorial/index.html)

##5. 制御

この辺は、大まかに説明するのと
Python上での特殊な仕様などについて説明します。

###5.1. if

if文で特徴的なところは、「else if」の記述ですね。
Pythonでは、「`elif`」と書きます。

```python:
>>> def value():
...     x = int(input("整数を入力してね："))
...     if x < 0:
...         x = 0
...         print("0に変換しました。")
...     elif x == 0:
...         print("0ですね。")
...     elif x == 1:
...         print("1ですか。")
...     else :
...         print("2以上ですよ。")
...
>>> value()
整数を入力してね：4
2以上ですよ。
```

また、シーケンス内に特定の値が含まれているかどうかを検証したい場合は
「`in`」キーワードを用いることで簡単に検証できます。

```python
>>> def check(value):
...     values = [1, 2, 3, 4, 5]
...     if value in values: # ここで検証している
...             print("含まれています。")
...     else :
...             print("含まれていません。")
...
>>> check(1)
含まれています。
>>> check(6)
含まれていません。

```

###5.2. for

C言語などで言う「`foreach`」文に相当します。

```python:for
>>>animals = ['dog', 'cat', 'bird']
>>>for animal in animals:
...     print(animal)
...
dog
cat
bird
```

この時注意しなければならないのは、**for文で得られた結果はコピーを作らないことです。**
例えば、for文内で対象のシーケンス型に要素を追加したいといった場合、意図した結果にならないことがあります。

```python:for_bad
>>>values = [1, 2, 3]
>>>for value in values:
...     values.insert(0, value+1) # 0番目にvalue+1の値を追加
...
# ほぼ永続的にループ
```

シーケンス型:`values`に要素を追加すると、その要素に対しても処理が行われるためです。
この場合、事前にシーケンス型のコピーを生成しておきます。
以下の例では、スライスによってコピーを行っています。

```python:for_safe
>>>values = [1, 2, 3]
>>>for value in values[:]:
...     values.insert(0, value+1)
...
>>> values
[4, 3, 2, 1, 2, 3]
```

####5.2.1. range関数

C言語などのfor文のようにインデックスを取得したい場合は、組み込み関数「`range`」を使用します。

>range(開始値, 終了値, ステップ数)

```python:range
# 終了値のみ指定
>>> for i in range(3):
...     print(i)
...
0
1
2
# 開始値, 終了値指定
>>> for i in range(1, 3):
...     print(i)
...
1
2
# 開始値, 終了値, ステップ数指定
>>> for i in range(1, 5, 2):
...     print(i)
...
1
3

```

###5.3. while

while文では、式の値がTrueである間実行を繰り返します。
次の例では、初期値「1」を2倍指していき100を超えるまで繰り返します。

```python
>>> def example() :
...     num = 1
...     while 100 > num :
...         print(num)
...         num *= 2
...
>>> example()
1
2
4
8
16
32
64
```

###5.4. break

for や while などのループを中断します。
この時、**最も内側のループだけ**が中断されることに注意してください。

```python:break
>>> values = [[1, 2, 3], [4, 5, 6]]
>>> for value in values:
...     for v in value:
...         print('test')
...         if v > 2:
...             break # 2を超える値の場合はvalueのループを終了する
...         print(v)
...
test
1
test
2
test
test
```

###5.5. continue

continueが実行されると次のループのイテレーションを実行します。

```python:continue
>>> values = [[1, 2, 3], [4, 5, 6]]
>>> for value in values:
...     for v in value:
...             print('test')
...             if v >2:
...                     continue #2を超える値の場合は次の要素へ
...             print(v)
...
test
1
test
2
test
test
test
test
```

###5.6. pass

pass文は何も動作しません。
構文上文を書くことが要求されていますが、**プログラム上何も動作する必要がない場合**に使用します。

```python:pass
>>> while True:
...     pass # 永遠にループ(ctrl+Cで停止)
...
```

###5.7. ループのelse節
 
ループ文に対してelse節を使用できます。
これは、反復処理がリスト、またはwhileでFalseを返されたときに実行されます。
ただし、**break文によって終了した場合は実行されない**ことに注意しましょう。

```python:loop_else
# リストの最後まで走査
>>> values = [1, 2, 3, 4, 5]
>>> for value in values:
...     print(value)
... else:
...     print('done')
...
1
2
3
4
5
done
# 途中で終了
>>> for value in values:
...     if value == 5 :
...             break
...     print(value)
... else:
...     print('done')
...
1
2
3
4
```

##6. 関数

Pythonで関数を定義する場合、「`def`」キーワードを使用します。
>def 関数名 ( 引数 ) :

関数の定義の次の行から実行文を記述します。
実行文はインデントされていなければなりません。
※チュートリアルによれば、関数にドキュメンテーション文字列を入れることを推奨しています。

```python:def
def gleeting(name):
    """ 挨拶します """
    print('Hello ' + name)

```

###6.1. シンボルテーブル

変数の参照の仕組みとして、`シンボルテーブル`が使われています。

>シンボルテーブル：変数と値の組み合わせや、関数情報などを保持するテーブル。

関数内のローカル変数の情報は、シンボルテーブルで保持しています。
変数への参照が行われた時に、ローカルなシンボルテーブル内に参照先がないか検索し、
存在しなければグローバルなシンボルテーブルを検索、最終的にはPythonの組込名前テーブルを検索しに行きます。

関数内ローカルシンボルテーブル
→上位関数内ローカルシンボルテーブル
→上位・・・
→グローバルシンボルテーブル
→組込名前テーブル

```Python
# グローバルなシンボルテーブル
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class'_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}

#関数内のローカルなシンボルテーブル
>>> def test():
...     values = [1, 2, 3]
...     print(locals())
...
>>> test()
{'values': [1, 2, 3]}
```

**※これらシンボルテーブルの内容は外部から変更しても意味がありませんので変更しないでください。**

###6.2. 引数(仮引数)

■[参考サイト1:[Python]常識ですよ？と言われないための引数入門](http://note.crohaco.net/2014/python-argument-intro/)
■[参考サイト2:Python 3の美しい関数引数システム](https://qiita.com/raviqqe/items/ee2bcb6bef86502f8cc6)




####6.2.1. 位置引数

関数の呼び出し時に必ず指定しなければならない引数です。
引数名だけを記述して定義します。

>def 関数名(位置引数名1, 位置引数名2,...)

下記の例では、引数を指定しない場合と定義した引数以上の数を与えた場合で
意図的にTypeErrorを発生させます。

```python:
>>> def func(arg1, arg2):
...     pass
...
>>> func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() missing 2 required positional arguments: 'arg1' and 'arg2'
>>>
>>> func(1,2,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() takes 2 positional arguments but 3 were given
```



####6.2.2. キーワード引数

既定値を付与した引数です。
関数の呼び出し時に省略が可能になります。

>def 関数(引数名1=既定値, 引数名2=既定値...)

下記の例では既定値が適用されています。

```python
>>> def func(arg1=1,arg2=2):
...     print(arg1,arg2)
...
>>> func()
1 2
```

次に、下記の例では指定していないキーワード引数を与え、意図的にTypeErrorを発生させてみます。

```python
>>> def example(arg1=1, arg2=2):
...     pass
...
>>> example(arg1=1,arg3=3)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: example() got an unexpected keyword argument 'arg3'
```

キーワード引数を使用する上で**注意しなければならないポイント**があります。
まずは下記の例を見てください。

```python
>>> def add_value(value, values=[], threshold=5):
...     if threshold > value:
...             values.append(value)
...     return values
...
>>> add_value(1)
[1]
>>> add_value(2)
[1, 2]
>>> add_value(3)
[1, 2, 3]
```

引数「`values`」は既定値に空のリストが定義されています。
valuesが指定されていない場合、空のリストに対して「`value`」を追加した結果が返却されると思いましたが違うようです。

公式ドキュメントには次のように記述があります。
>**重要な警告**: デフォルト値は 1 度だけしか評価されません。デフォルト値がリストや辞書のような変更可能なオブジェクトの時にはその影響がでます。例えば以下の関数は、後に続く関数呼び出しで関数に渡されている引数を累積します

つまり、
**関数を定義した段階で規定値が評価されてしまい、
関数の何度使おうと既定値は初期化されないということです。**
変更可能(ミュータブル)な引数を指定する場合は注意しましょう。

対策としては次のようにすると回避できます。

```python
>>> def new_example(value, values = None, threshold = 3):
...     if values is None:
...         values = []
...     if threshold > value :
...         values.append(value)
...     return values
...
>>> new_example(1)
[1]
>>> new_example(2)
[2]
>>> new_example(3)
[]
```

同じように、既定値に関数が定義された側の変数を設定した場合は、
定義された時点で関数を定義しているスコープで評価します。

```python
>>> value = 1
>>> def func(arg = value):
...     print(arg)
...
>>> value = 2 #この段階でfuncはvalue=1で評価される。また1度しか評価されないことに注意。
>>> func()
1
```

####6.2.3. 可変長位置引数

可変長位置引数では、位置引数が不定な場合に利用します。
0個以上の引数を指定できます。
引数名の前には「`*(アスタリスク)`」を付与して下さい。
なお、可変長位置引数は、**1つの関数に付き１つしか**指定できません。
2つあるとどこからどこまでなのかわかりませんよね。

>def 関数名(*可変長引数名)

次の例では、可変長引数に指定された引数を順にprintしています。

```python
>>> def variable_example(*message):
...     for m in message:
...             print(m)
...
>>> variable_example('こんにちは','今日も','いい天気','ですね')
こんにちは
今日も
いい天気
ですね
```

####6.2.4. 可変長キーワード引数

可変長位置引数と同様、キーワード引数を0個以上付与して実行します。
このとき、引数は「*(アスタリスク)」を2つ続けて入力して引数名を指定します。

>def 関数名(**可変長キーワード引数)

可変長キーワード引数も可変長位置引数と同様、
**1つの関数に付き1つしか指定できません。**

次の例では、可変長キーワード引数を指定して入力文字を出力します。

```python
>>> def keyword_example(**kwarg):
...             for key, value in kwarg.items():
...                     print([key, value])
...
>>> keyword_example(arg1='こんばんは', arg2='今夜も', arg3='いい天気ですね')
['arg1', 'こんばんは']
['arg2', '今夜も']
['arg3', 'いい天気ですね']
```

さて、結果を見て分かる通り、可変長キーワード引数では、
引数名とその値をkey,valueの値で保持しています。
dict型で持っているということですね。

####6.2.5. キーワード専用引数

Python3から取り入れられた構文だそうです。

キーワード専用引数を用いた場合、引数には
キーワード引数を必ず指定しなければなりません。

> def 関数名(*, 引数1, 引数2, 引数3 ...) 

次の例では、関数に対してキーワード引数を指定した場合と指定しない場合を検証します。

```python
>>> def force_keyword_example(*,arg1,arg2,arg3):
...     print(arg1,arg2,arg3)
...
>>> force_keyword_example(arg1='a', arg2='r','g')
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument ## キーワード引数がないよ
>>> force_keyword_example(arg1='a', arg2='r',arg3='g')
a r g
```

ちなみに触れていませんでしたが、
位置引数では引数の順序を無視した指定ができません。
(引数の順序によって定義が決まるので当たり前ですが）

一方で、キーワード引数は順序を無視した指定ができます。
関数のキーワード引数は**dictionary型で保持している**と考えるとしっくり来るかと思います。

```python
>>> def force_keyword_example(*,arg1,arg2,arg3):
...     print(arg1,arg2,arg3)
...
>>> force_keyword_example(arg2='r', arg1='a', arg3='g')
a r g
```

###6.3. アノテーション（注釈）

こちらもPython3から実装された機能になりますが、
ユーザが定義した関数で使用される「型」について付与するメタ情報です。
[PEP3107](https://www.python.org/dev/peps/pep-3107/)では、関数アノテーションの構文に関する既定があります。
また、[PEP484](https://www.python.org/dev/peps/pep-0484/)では関数アノテーションに型情報を書く場合の記述方法に関する規定があります。

####6.3.1. 関数アノテーション

関数アノテーションは、引数に対するアノテーションと戻り値に対するアノテーションがあります。
引数に対するアノテーションの場合、引数名の後に「`：(コロン)`」を指定します。
戻り値に対するアノテーションの場合、関数名末尾に「`->`」を指定します。
もちろん、アノテーションを記述するものと記述しないものが混在していても問題ありません。

>def 関数名(引数:'引数アノテーション'(=既定値), 引数2)-> '戻り値アノテーション' :

既定値を付与する場合には、**アノテーションの後**に定義します。

注意しなければならないのは、**あくまでも注釈でしかない**ということです。
アノテーションを付与したからと言って、強制的に何らかの処理が行われるということはありません。
（IDEやエディタによっては静的解析をして警告を出したりしてくれるものもある）

####6.3.2. __annotations__属性

前項6.3.1.の関数アノテーションは、
「__annotations__」属性にdict型で格納されています。
下記の例では、実際に定義されたアノテーションを出力しています。
(~~意味のわからない処理に関しては突っ込まないでください~~)

```python
>>> def annotations(arg1:'argument is integer', arg2:'argment is string'='example')->str :
...     return str(arg1 + 1) + arg2
...
>>> annotations(1)
'2example'
>>> annotations.__annotations__
{'arg1': 'argument is integer', 'arg2': 'argment is string', 'return': <class'str'>}
```

####6.3.3. 型ヒント、typingモジュール

[PEP3107](https://www.python.org/dev/peps/pep-3107/)では具体的にどんなことを書くか、ということは定義されていません。
これに対し、[PEP484](https://www.python.org/dev/peps/pep-0484/)ではアノテーションに型を記述する場合の記述方法が新しく規定されました。
それに伴い、Python3.5以降、`typing`モジュールが標準ライブラリに追加されています。
しかし、6.3.1.でも記載したとおり、
注釈はあくまでも**注釈でしかありません。**
型ヒントを入力したからと言って型チェックが行われることもないです。
(繰り返しますが、IDE等ではサポートしているものもあります)

では、typingモジュールを使用し、型アノテーションを記述します。
さらにその上で「mypy」を使用して静的解析を行ってみましょう。

```:mypyの取得
pip install mypy
```

1.関数 func
typingモジュールを使用して、引数[x]の型ヒントに
「**List型で、要素にはint型かfloat型が指定されている**」という情報を付与しています。

2.関数 error_func
直接アノテーションを付与した関数です。
前述の`mypy`を使用して静的解析を行った結果、違反していると解釈させました。

```python:target.py
from typing import Union, List

def func (x:List[Union[int, float]]) -> float:
    return sum(x) ** 0.5

print(func([0.1, 0.2, 5])) # Listの要素は全てint,float

def error_func(x:str, y:str) -> str :
    return x + y    

error_func('a', 'b') # str
error_func(1, 2) # int
error_func('c', 3) # str , int
```

```:チェック
mypy target.py
target.py:12: error: Argument 1 to "error_func" has incompatible type "int"; expected "str"
target.py:12: error: Argument 2 to "error_func" has incompatible type "int"; expected "str"
target.py:13: error: Argument 2 to "error_func" has incompatible type "int"; expected "str"
```

###6.4. ラムダ式

ラムダ式は、無名関数を定義する際の記法です。

>lambda 引数 : 処理

例えば、2つの引数を合算した結果を返す「sum」関数があるとしましょう。

```python
>>> def sum(a, b):
...     return a + b
...
>>> sum(1,2)
3
```

これをlambda式を用いて記述します。

```python:lambda
>>> sum = lambda a, b : a + b
>>> sum(1,2)
3
```
ラムダ式は、組み込み関数`map()`や`sort()`といった高階関数で利用するために利用する場合に真価を発揮し、「わざわざ一度きりの関数のために関数を定義しておく必要もないよね」を解消してくれます。

注意しなければならないのは、※[PEP8](https://pep8-ja.readthedocs.io/ja/latest/index.html?highlight=lambda)によるところで
>ラムダ式を直接識別子に結びつける代入文を書くのではなくて、常に def 文を使いましょう。

とあります。

ラムダ式を識別子に結びつけるのであれば、はじめからdef文で関数を定義しましょうねというお作法の話です。規約に違反しているかと言ってエラーにはなりませんが、IDEやエディタによっては警告がでるかもしれませんね。

#まとめ

本稿からすこしずつPEPについても触れるようになってきました。
ご指摘等ありましたらコメント、編集リクエストお待ちしております。