Python3 エンジニア認定実践試験 チートシート

* 自分の記憶が曖昧なところを中心に作成しています
* 章番号は公式テキスト「Python実践レシピ」と合わせてあります
* 本記事内のコードは覚えるためのものであり、動作しません

# 1.Pythonの環境(出題数1：2.5%)
## 1.1 pip
* `pip install --upgrade package` 最新バージョンに更新する
* `pip list -o`または`pip list --outdated` 最新ではないパッケージを表示する
* `pip uninstall package` アンインストール
* `pip install -r requirements.txt -c constraints.txt`
  * constraints.txtにバージョン制限をかけたいパッケージを記述する
  * 開発プロジェクトで作るモジュールが複数あるケースで、バージョン制限をかけたいときに使う

## 1.2 venv
`python -m venv --upgrade-deps`すると仮想環境用のpipが最新になる

### 仮想環境の有効化
| OS | シェル | コマンド |
|:-:|:-|:-|
| Unix  | bash\zsh | source .venv/bin/activate  |
| Unix  | fish  | source .venv/bin/activate.fish  |
| Unix  | csh/tcsh  | source .venv/bin/activate.csh  |
| Windows | コマンドプロンプト | .venv\Sctipts\activate.bat  |
| Windows/Unix | PowerShell | .vnev\bin or Scripts\Activate.ps1 | 


# 2.コーディング規約(出題数2：5%)
## 2.1 PEP8
* インデントはスペース4文字
* 1行は79文字
* 関数、クラス間は2行空ける
* クラスメソッドは1行空ける
* importは`標準ライブラリ`→`サードパーティ`→`ローカルライブラリ`の順番
* importの後は2行空ける
* コメントは"#"の後にスペース1文字空ける
* docstringが複数行の場合、2行目は空ける
```python
def func():
    """関数の短い説明を書く

    1行空けて、
    説明を書く
    """
```
### 命名規則
| タイプ | ルール | 例 |
|:-|:-|:-|
| パッケージ/モジュール名  | lowercase  | `system` `datetime`  |
| クラス名  | CamelCase  | `TestCase`  |
| 変数/関数名  | lowercase_with_underscores  | `is_connected`  |
| 定数  | UPPEERCASE_WITH_UNDERSCORES  | `MAX_OVERFLOW`  |
| 例外  | CamelCase+Error  | `ClientApiError`  |

## 2.2 Flake8
静的コード解析
| タイプ | 内容 |
|:-|:-|
| E または W  | PEP8由来のエラー  |
| F  | 不具合の元になりそうな書き方  |
| C  | 複雑さのエラー |

* 特定のエラーを無視する `flake8 --ignore=E203,W503`
* 特定の行だけエラーを無視する
```python:インラインコメントで書く
aaa = lambda x: 2 + x # noqa: E731
```
* 特定のファイル等を無視する `flake8 --exclude=.venv,build`
* 1行の最大文字数を変更する `flake8 --max-line-length=88`
* 複雑度チェック `flake8 --max-complexity=10`
* 設定ファイルは `setup.cfg` `tox.ini` `.flake8`のいずれか

## 2.3 Black
妥協のないコードフォーマッターで自動整形ツール
import順序の修正は`isort`を別途使う
| オプション | 説明 |
|:-|:-|
| -l, --line-length  | 1行の最大文字数(デフォルト88文字)  |
| --check  | チェック結果(全行)を出力、修正はしない  |
| --diff  | チェック結果(差分)を出力、修正はしない  |
| --include  | 対象ファイル/ディレクトリ指定  |
| --exclude  | 除外ファイル/ディレクトリ指定  |

* 設定ファイルは `pyproject.toml` `~/.config/black`(Unix) `~/.black`(Windows)

| ファイル種別 | Black | Flake8 | mypy |
|:-|:-:|:-:|:-:|
| pyproject.toml  | 〇  | ×  | 〇  |
| setup.cfg  | ×  | 〇  | 〇  |
| その他  | `.config/black`、`.black`  | `.flake8`、`tox.ini`  | `mypy.ini`、`.mypy.ini`  |

# 3.Pythonの言語仕様(出題数7：17.5%)
## 3.1 例外
```
BaseException
├─ SystemExit
├─ KeyboardInterrupt
├─ GeneratorExit
└─ Exception
   ├─ ArithmeticError 算術エラーの基底クラス
   │  ├─ OverflowError
   │  └─ ZeroDivisionError
   │
   ├─ AttributeError 存在しない属性を参照
   ├─ LookupError
   │  ├─ IndexError list
   │  └─ KeyError   dict
   │
   ├─ OSError
   │  └─ FileNotFoundError
   │
   ├─ SyntaxError
   │  └─ IndentationError 空白の不備
   │     └─ TabError
   │
   ├─ TypeError 演算・操作で不適切な型
   ├─ NameError 未定義の変数を使ったとき
   └─ ValueError 演算・操作で不適切な値
```

## 3.2 with文
### ContextManager
```python
class sample:
    def __init__(self, file_path):
        self.file_path = file_path
        
    def __enter__(self):
        """ファイルを開く"""
        self.file_obj = open(self.file_path)
        return self.file_obj

    def __exit__(self,
                 type,  # 例外の型
                 value, # 例外のメッセージ
                 traceback # スタックトレース
                 ):
        """ファイル閉じる"""
        print(type(e))
        print(e)
        print(スタックトレースは長いので省略)
        self.file_obj.close()

with sample('./sample.txt') as f:
    print(f.read())
    raise ValueError('my context managaer error')
```

### ContextManagerデコレーター
```python
import contextlib

@contextlib.contextmanager
def sample(file_path):
    file_obj = open(file_path)
    try:
        # __enter__
        yield file_obj
    except Exception as e:
        # __exit__例外
        print(type(e))
        print(e)
        print(スタックトレースは長いので省略)
        raise
    finally:
        # __exit__通常
        file_obj.close()

with sample('./sample.txt') as f:
    print(f.read())
    raise ValueError('my context managaer error')
```

### その他のcontextlib機能
```python
import contextlib
import os
with contextlib.suppress(FileNotFoundError):
    os.remove('dummy.txt') # ファイルがなくても例外にならない

with open('sample.log', 'w') as f:
    with contextlib.redirect_stdout(f):
        print('log_write') # ファイルに出力される
```

## 3.3 関数の引数

### 位置引数
`func(param1, param2, param3)`

### 位置専用引数
```python
def func(x, y, /):
    pass

func(1, 2)     # OK
func(x=1, y=2) # NG
```
### キーワード引数
`func(param1=1, param2=2, param3=3)`
* この呼び出しはエラー `func(param3=3, 1, 2)`
### デフォルト付き引数
* これはエラー`def func(param1=1, param2, param=3):`
### 可変長引数
```python
def func(*args):
    for arg in args
        print(arg)

func(1, 2, 3, 4, 5)

# listとtupleは*を付けて呼び出す
nums = [4, 5, 6, 7]
func(*nums)
```
### 可変長キーワード引数
```python
def func(**kwargs):
    for k, v in kwargs.items() # dict型になる
        print(f'{k} {v}')

func('name':'john', 'age':30)
```
### キーワード専用引数
```python
def func(param1, *, param2):
    pass

func(1, param2=2) # *以降はキーワード指定が必須
```
## 3.4 アンパック
```python
nums = (1, 2, 3)
a, b, c = nums
# a=1, b=2, c=3

nums = (1, 2, (3, 4, 5))
a, b, c = nums
# a=1, b=2, c=(3, 4, 5)

a, b, (c, d, e) = nums
# a=1, b=2, c=3, d=4, e=5
```

```python:アスタリスクを使ったアンパック
nums = (1, 2, 3, 4)
a, b, *c = nums
# a=1, b=2, c=[3, 4]まとめて入る

a, *b, *c = nums
# 2つ以上はエラーになる
```

## 3.6 ジェネレーター

`return`ではなく`yield`にするとジェネレーターになる
```python
def func(values):
    for i in values:
        yield i * 2

ret = func([0, 1, 2, 3])
print(type(ret)) # <class 'generator'>

# 長さを取得
len(ret) # NG
len(list(ret)) # リストに変換する必要がある

next(ret) # 0
next(ret) # 1
next(ret) # 2
next(ret) # 3
next(ret) # StopIteration例外
```

## 3.7 デコレーター
```python
@my_decorator1
@my_decorator2
def func():
    psss

# 以下と同じ
func = my_decorator1(my_decorator2(func))
```

`__name__`や`__doc__`(docstring)がデコレーターに上書きされてしまうが、`functools.wraps`を使うと元関数の情報を返すことができる。
```python
from functools import wraps
def my_decorator(func):
    @wraps(func):
    def wrap_func(a):
        """wrap_funcのdocstringです"""
        func(a)
    return wrap_func

@my_decorator
def func(name):
    """funcのdocstringです"""
    print(name)

print(func.__doc__) " funcのdocstringです
```

# 4.Pythonのクラス(出題数3：7.5%)
## 4.1 class構文
* Pythonはマルチパラダイムプログラミング言語
  * オブジェクト指向、手続き型、関数型いずれも書ける

## 4.2 特殊メソッド

| メソッド | 説明 |
|:-|:-|
| `__init__` | コンストラクタ |
| `__call__` | メソッドのように( )で呼び出せるようにする |
| `__repr__` | print出力を変更できる |
| `__str__` | 文字列型への変換処理 |
| `__len__` | len()関数の処理 |
| `__lt__` | <演算子 (lはlower→小なり) |
| `__le__` | <=演算子 (eはイコール) |
| `__eq__` | ==演算子 |
| `__ne__` | !=演算子 |
| `__gt__` | >演算子 (gはgrator→大なり) |
| `__ge__` | >=演算子 |
| `__add__` | +演算子 |
| `__sub__` | -演算子 |
| `__mul__` | *演算子 |
| `__truediv__` | / 演算子 |

## 4.3 継承
* `super()`関数で親クラスのメソッドを呼び出せる
* 多重継承は`class Custom(Base1, Base2)`
  * 継承順位をMROという仕組みで、C3アルゴリズムで探索する 

## 4.4 dataclass
* `__init__`の定義を省略できる
```python
@dataclass(frozen=true) # データ変更するとFrozenInstanceErrorになる
class User:
    name: str
    age: int
    active: bool = True # デフォルト値は最後に定義する

user = User('Taro', 30)
dic = asdict(user) # 辞書型に変換できる
tup = astuple(user) # タプルに変換できる
```

## 4.5 オブジェクト関連関数
| 関数名 | 説明 |
|:-|:-|
| `id(object)` | objectの識別値を返す |
| `type(object)` | 型オブジェクトを返す |
| `isinstance(object, class_info)` | objectがclass_infoインスタンスであるか判定する |
| `issubclass(class, class_info)` | classがclass_infoのサブクラスであるか判定する |
| `help(object)` | docstringを返す |
| `dir(object)` | objectが持つ属性・メソッドのリストを返す |

```python
isinstance(True, bool) # True
isinstance(True, int) # bool値はintを継承しているのでTrue
```
## デコレーター
### @classmethod
メソッド内でクラス自身を参照できるようにするデコレーター

### @staticmethod
インスタンス化せずにメソッドを使えるようにするデコレーター

### @property 
```python
class Hoge:
    @property
    def length(self):
        cnt = ・・・ # データ数を数える処理
        return cnt

# ()なしで呼び出せる
hoge_length = hoge.length
```

# 5.型ヒント(出題数2：5%)
5.2 mypyは出題無し

## 5.1 typingモジュールを利用した型ヒント
### Union
```python
from typing import Union

def func(number: Union[int, str]):
    pass

func(100)
func("100")
```

### Optional
```python
from typing import Optional

price: Optional[int] # 数値またはNone
```

Python3.10から`typing`を`import`せずに`Union`と`Optional`が書ける
```python
def func(number: int | str) # Union
    pass

price: int | None # Optional
```

### Literal
特定の値のみ許可する
```python
from typing import Literal

FILETYPE = Literal["csv", "json"]
def func(path: str, file_type: FILETYPE):
    pass

func("sample.csv", "csv")   # OK
func("sample.html", "html") # NG
```

### TypeVar
C++のTemplateのイメージ
```python
from typing import TypeVar

T = TypeVar("T", int, float)

def add_one_to_t_arg(arg: T) -> T:
    return arg + 1
```

### TypedDict

dict型よりも厳密に型を付ける
```python
from typing import TypedDict

class BookDict(TypedDict):
    name: str
    author: str
    price: int

book: BookDict = {
    "name": "Python実践レシピ",
    "author": "鈴木たかのり",
    "price": 2700
}
 ```

 # 6.テキストの処理(出題数4：10%)
 ## 6.1 str, string
 ### チェックメソッド
 | メソッド | 解説 |
|:-|:-|
| `string.istitle()` | 先頭大文字、残り小文字ならTrue  |
| `string.isdecimal()`  | 数値(全角、半角)ならTrue  |
| `string.isdigit()`  | True:①、False:ローマ数字/漢数字  |
| `string.isnumeric()`  | ① ローマ数字/漢数字もTrue  |

### 変換メソッド
| メソッド | 解説 |
|:-|:-|
| `string.capitalize()`  | 先頭を大文字に、それ以外を小文字にする  |
| `string.title()`  | 単語ごとに大文字1文字＋小文字にする  |
| `string.zfill(width)`  | 長さがwidthになるよう左に0を埋める  |
| `string.removeprefix(prefix, /)`  | 先頭からprefixを削除する<br>※位置専用引数なので`string.removeprefix(prefix='hoge')`では呼び出せない  |
| `string.removesuffix(suffix, /)`  | 末尾からsuffixを削除する  |

### その他メソッド

| メソッド | 解説 |
|:-|:-|
| `find(sub, start[, end])->int` | subが出現する位置を返す。なければ-1。<br>※search( )は正規表現のreモジュールなので引っ掛け注意  |
| `encode(encording='utf-8', errors='strict', replace, ignore)->str`  | `errors='strict'`(デフォルト)は変換できなければUnicodeEncodeError例外<br>`replace`：変換できない文字は?になる<br>`ignore`：無視する(その文字は変換されない)  |

### 文字列定数

| 定数 | 内容 |
|:-|:-|
| `string.ascii_lowercase`  | 英小文字  |
| `string.ascii_uppercase`  | 英大文字  |
| `string.ascii_letters`  | 英大文字＋小文字  |
| `string.digits`  | 0～9  |
| `string.hexdigits`  | 0～9+ABCDEF  |
| `string.octdigits`  | 0～7  |
| `string.punctuation`  | 記号  |
| `string.whitespace` | 空白 \t \n \t \r \x0b \x0c  |
| `string.printable`  | ascii_letters + digits + punctuation + whitespace  |

```python:使用例
import string

print('a' in string.ascii_lowercase)
```

## 6.2 f-string

| 書式 | 解説 |
|:-|:-|
| `{aaa:<10}`  | 左寄せ10文字埋め  |
| `{aaa:^10}`  | 中央ぞろえ10文字埋め  |
| `{aaa:>10}`  | 右寄せ10文字埋め  |
| `{aaa:b}`  | 2進数  |
| `{aaa:X}`  | 16進数(大文字) 小文字もある  |
| `{aaa:%}`  | 百分率+%  |
| `{aaa:,}`  | 3桁カンマ区切り |
| `{aaa:6.2f}` | 全体が6桁で、小数点は2桁 |
| `{aaa:%Y-%m-%d %H:%M:%S}` | YYYY-mm-dd HH:MM:SS |

`print(f'{name=}')` 変数名＋値の形式で出力される（デバッグで使う)


## 6.3 正規表現(reモジュール)
### search関数
```python
import re

mt = re.search('a.c', 'abcde')
print(mt)
# re.Match object: span(0, 3), match='abc'
# マッチしなければNoneが返る
```

| フラグ | 解説 |
|:-:|:-:|
| `re.A`  | ASCII文字  |
| `re.I`  | 大文字・小文字を区別しない  |
| `re.M`  | 複数行の先頭と末尾  |
| `re.S`  | .が改行文字含めてマッチする  |

```python:使用例
re.search('a.c', 'a\nc', re.S) # .と\nがマッチする
```

### 正規表現の特殊文字一覧
| 特殊文字 | 説明 |
|:-|:-|
| `\d`  | 数字  |
| `\D` | 数字以外  |
| `\s`  | 空白文字  |
| `\S`  | 空白文字以外  |
| `\w`  | 任意の英数字  |
| `\W`  | 英数字以外  |
| `.`  | 任意の1文字  |
| `^`  | 先頭  |
| `$`  | 末尾  |
| `*`  | 0文字以上の繰り返し  |
| `+` | 1文字以上の繰り返し |
| `?` | 0回か1回の繰り返し |
| `{m}` | m回の繰り返し |
| `{m, n}` | m回以上、n回以下の繰り返し |
| `[abc]` | 指定した いずれかの文字(この場合`a` or `b` or `c`) |
| `[^abc]` | 指定した いずれかの文字以外(この場合、a/b/c以外) |
| `(x\|y)` | `x`か`y`の選択 |

### compile(正規表現オブジェクト)
`search()`の第一引数をオブジェクト化するイメージ。
```python
import re

pattern = re.compile('a.c')
mt = pattern.seach('abcde')
print(mt)
# Match object: span(0, 3), match='abc'
```

### compileオブジェクトのメソッド
| メソッド名 | 解説 | 戻り値 |
|:-|:-|:-|
| `search(string)`  | stringが正規表現にマッチするか返す  | Matchオブジェクト or None  |
| `match(string)`  | 先頭から正規表現にマッチするか返す  | Matchオブジェクト or None  |
| `fullmatch(string)`  | string全体が正規表現にマッチするか返す  | Matchオブジェクト or None  |
| `split(string)`  | 正規表現にマッチした文字列で分割する  | list[str]  |
| `sub(replace, string)`  | マッチした文字列をreplaceに置き換える  | str  |
| `findall(string)` | マッチした文字列をリストで返す | list[str] |
| `finditer(string)` | マッチした文字列をIteratorで返す | MatchのIterator |

```python:使用例
import re

regex1 = re.compile('[-+()]')

print(regex1.split('+81(80)1234-5678')
# ['080', '1234', '5678']

print(regex1.sub('_', '+81(80)1234-5678')
# _81_80_1234_5678
```
### Matchオブジェクト
```python
import re

mt = re.match(r'(\d+)-(\d+)-(\d+)', '080-1234-5678')
print(mt.group(0)) # 全体
# '080-1234-5678'

print(mt.group(1)) # 1番目のグループ
# '080'

print(mt.group[2]) # 2番目のグループ(添え字でもOK)
# '1234'
```

名前付きサブグループは`(?P<name>)`という形式で記述する。
```python:
import re

mt = re.match(r'(?P<family_name>\w+) (?P<first_name>\w+)', '鈴木 太郎')
print(mt.group('family_name')
# '鈴木'

print(mt.group['first_name']) # dictみたいにもアクセスできる
# '太郎'
```

```python:その他の出力
print(mt.groupdict())
# 'family_name': '鈴木', 'first_name': '太郎'

print(mt.expand(r'名字:\g<family_name> 名前:\g<first_name>'))
# '名字:鈴木 名前:太郎'
```

## 6.4 unicodedataモジュール
```python
import unicodedata

print(unicodedata.lookup('BEER MUG'))
# '🍺'

print(unicodedata.name('🍣'))
# 'SUSHI'
```

### normalize(正規化)
```python
import unicodedata

print(unicodedata.normalize('NFC', '１２３ａｂｃｱｲｳｴｵ①②③¹²³')) # そのまま出力
# １２３ａｂｃｱｲｳｴｵ①②③¹²³

print(unicodedata.normalize('NFKC', 'ｱｶﾞ')) # 半角ｶﾅが全角カナになる
# 'アガ'

print(list(unicodedata.normalize('NFKD', 'ｱガ'))) # 濁点が分割される
# ['ア', 'カ', '゛']
```

# 7.数値の処理(出題数0)
pass

# 8.日付と時刻の処理(出題数2：5%)
8.4 dateutilは出題なし

## 8.1 datetimeモジュール
### dateオブジェクト
`from datetime import date`が必要。

| メソッド | 備考 |
|:-|:-|
| `date.today()`  | `f'{date.today():%Y年 %m月 %d日}'`  |
| `date.weekday()` | 月曜日が0～日曜日が6  |
| `date.isoweekday()`  | 月曜日が1～日曜日が7  |
| `date.isoformat()`  | YYYY-MM-DD形式で返す  |
| `date.fromisoformat('2024-12-31')`  | YYYY-MM-DD文字列からdateオブジェクトを作る  |
| `date.strftime('%Y/%m/%d')` | 任意のフォーマットで文字列を返す |

### timeオブジェクト
`from datetime import time`が必要。

| メソッド | 備考 |
|:-|:-|
| `time.isoformat(timespec='auto')`  | '12:59:59.123456'が返る  |
| `time.fromisoformat('12:59:59.123456')`  | 文字列からオブジェクトを作る  |
| `time.strftime('%H時 %M分 %S秒')`  | 任意のフォーマットで文字列を返す  |

```python
from datetime import time

t1 = time(12, 25, 55, 999999) # 12時25分55秒 123456マイクロ秒
t2 = time.fromisoformat('12:25:55.123456')
```

`time.fold`はサマータイムが終わって時刻が巻き戻った期間の時に「1」にする

### datetimeオブジェクト
`from datetime import datetime`が必要。
`date`と`time`を合わせたもの。

| メソッド | 備考 |
|:-|:-|
| `datetime.now(tz=None)`  | タイムゾーンを渡せる  |
| `datetime.utcnow()`  | UTC日時を返す  |
| `datetime.strftime('%Y/%m/%d')`  | 任意のフォーマットの文字列を返す  |
| `datetime.strptime('2021/12/31', '%Y/%m/%d')`  | 任意のフォーマットからdatetimeオブジェクトを作る<br>`strftime`と`strptime`は動きが逆なので注意 |
| `datetime.fromisoformat('2021-12-31') ` | ISOフォーマットからdatetimeオブジェクトを作る  |

* `date` `time` `datetime`の`fromisoformat()`にパースできない文字列を渡すと`ValueError`が発生する
* タイムゾーン情報を含む：Awareオブジェクト
* タイムゾーン情報を含まない：Nativeオブジェクト
  * 比較や差分計算をする場合は種類を合わせる必要がある
  * 種類が違うと`TypeError`が発生する 

### strftime()で使える指定子

| 指定子 | 備考 |
|:- |:- |
| %Y  | 4桁の西暦  |
| %y  | 下2桁の西暦(2002の場合は02)  |
| %m  | 月(0埋め2桁)  |
| %d  | 日(0埋め2桁)　%Dは無い |
| %H  | 時(24時間表記)　%hは無い  |
| %M  | 分(0埋め2桁) |
| %S  | 秒(0埋め2桁)　%sは無い  |

## 8.2 timeモジュール

`import time`が必要。
1970年1月1日からの経過時間を扱う。
(`datetime`モジュールの`time`とは別なので注意)

| 関数名 | 説明 |
|:- |:- |
| `time.time()`  | 1970年1月1日から現在の経過時間(秒)をfloatで返す  |
| `time.gmtime([経過時間(秒)])`  | 1970年1月1日から指定秒経過後のUTC時刻(time.struct_time構造体)を返す<br>(UTCは日本時間の9時間前)<br>引数未指定なら現在時刻が返る  |
| `time.localtime([経過時間(秒)])`  | gmtime( )の日本時間が返る版  |
| `time.strftime('%Y-%m-%d', time.localtime())`  | 経過時間から日付文字列を返す  |

`time.time()`で簡易的に実行時間を計測できるが、`timeit`モジュールを使うほうが正確(?)

### time.sleep関数

```python
import time

time.sleep(0.5) # 0.5秒止める(全スレッド)
```

自スレッドだけ止める場合は`import asyncio`して`asyncio.sleep(0.5)`を使う。

### 8.3 zoneinfoモジュール
```python
from zoneinfo import ZoneInfo
from datetime import datetime

tokyo = ZoneInfo('Asia/Tokyo')
dt = datetime(2021, 12, 31, tzinfo=tokyo)
```

サードパーティに`pytz`がある。
`pytz`は`fold`(サマータイム終了後の巻き戻り期間中を表す属性)には対応していない。

# 9.データ型とアルゴリズム(出題数5：12.5%)
9.3 bisect と 9.5 pprint は出題無し

## 9.1 ソート(sorted, sort, operator)

`sorted`: 入力データはそのままで、ソート済みの新しいlistを返す(非破壊的操作)
`sort`: 入力データ自体をソートする(破壊的操作)

```python
print(sorted(['B', 'D', 'a', 'c'], key=str.lower)) # 小文字に変換して比較する
# ['a', 'B', 'c', 'D']
```

### operatorモジュール
```python:itemgetterの使用例
from operator import itemgetter

# 複数キーの指定
data = [(2, 'c', 100), (1, 'b', 200), (2, 'a', 300)]
print(sorted(data, key=itemgetter(0, 1))) # tupleの0番目、1番目をキーにソートする
# [(1, 'b', 200), (2, 'a', 300), (2, 'c', 100)]

# 辞書のValueでソート
user_dict = [{'name': 'terada', 'age': 35},
             {'name': 'suzuki', 'age': 25},
             {'name': 'sugita', 'age': 30}]
print(sorted(user_dict, key=itemgetter('age'))
# suzuki(25) → sugita(30) → terada(35)の順番になる
```

`date.month`、`date.day`など `.`で属性にアクセスする物をソートする場合は`attrgetter`を使う。
```python:attrgetterの使用例
from operator import attrgetter
from datetime import date

dates = [date(1989, 1, 4),
         date(1970, 11, 28),
         date(1984, 3, 4)]
print(sorted(dates, key=attrgetter('month', 'day))
# 1989/1/4 → 1984/3/4 → 1970/11/28の順になる
```

## 9.2 collectionsモジュール
### Counterクラス
件数をカウントするクラス。

```python
from collections import Counter

cnt = Counter('aaabbc')
print(cnt)
# {'a':3, 'b':2, 'c':1}
```

```python:Counter.elements( )の使用例
from collections import Counter

cnt = Counter('cbbaaa')

print(list(cnt.elements())) # カウントキーのイテレーター
# ['c', 'b', 'b', 'a', 'a', 'a']
```

```python:Counter.most_common( )の使用例
from collections import Counter

cnt = Counter('cbbaaa')

print(cnt.most_common(2)) # 大きい順に2つ取り出す
# [('a', 3), ('b', 2)]
```

```python:Counter.update( )の使用例
from collections import Counter

# update()でCounter同士を足す
cnt = Counter('cbbaaa')
cnt2 = Counter('cba')
cnt.update(cnt2)
print(cnt)
# {'c':2, 'b':3, 'a':4 }
```

```python:Counter.subtract( )の使用例
from collections import Counter

#subtract()でCounter同士を引く
cnt = Counter('cbbaaa')
cnt2 = Counter('cba')
cnt.subtract(cnt2)
print(cnt)
# {'b':1, 'a':2}
```
### defaultdictクラス
存在しないキーにアクセスしても`KeyError`にならない辞書。
```python
from collections import defaultdict

def default_func():
    return 'default_value'

d = defaultdict(default_func)
print(d['apple'])
# KeyErrorにならず'default_value'になる

d2 = defaultdict(int)
print(d2['apple'])
# intを指定すると0になる
```

### OrderdDictクラス
Python3.7からdict型が挿入順を保持できるようになったので、最近は使われない。
```python
from collections import OrderdDict

d = OrderdDict(one=1, two=2, three=3)

# 順番を移動する
d.move_to_end('two')
print(d)
# [('one', 1), ('three', 3), ('two', 2)]

# last=Falseにすると先頭に移動する
d.move_to_end('two', last=False)
print(d)
# [('two', 2), ('one', 1), ('three', 3)]

# 末尾を取り出す
print(d.popitem()) 
# ('three', 3)

# 先頭を取り出す
print(d.popitem(last=False))
# ('two', 2)
```

### namedtuple
名前付きタプル
```python
from collections import namedtuple

Pet = namedtuple(typename='Pet' # 型の名前
                 field_names='animal, name, age')

pet = Pet('dog', 'ポチ', 3)
print(pet)
# Pet(animal='dog', name='ポチ', age=3)
```
## 9.4 enumモジュール
`enum.auto()`を使うと自動で連番が振られる
```python
import enum

clann Nengo(enum.Enum):
    SHOWA = enum.auto()
    HEISEI = enum.auto()
    REIWA = enum.auto()
```

### uniqueデコレーター
```python
import enum

@enum.unique
class Spam(enum.Enum):
    HAM = 1
    BACON = 1
# 実行するとValueError例外になる
```

### 定数の参照
```python
import enum

class Nengo(enum.Enum):
    SHOWA = enum.auto()
    HEISEI = enum.auto()
    REIWA = enum.auto()

print(Nengo.REIWA) # <Nengo.REIWA: 3>
print(Nengo['REIWA']) # <Nengo.REIWA: 3>
print(Nengo(3)) # <Nengo.REIWA: 3>

nengo = Nengo.REIWA
print(nengo.name) # 'REIWA'
print(nengo.value) # 3
```

### Enumの比較
```python
import enum

class Spam(enum.Enum):
   HAM = 1
   EGG = 2
   BACON = 2

print(isinstance(Spam.HAM, Spam) 
# True (HAMはSpam型インスタンスだから)

print(Spam.EGG == Spam.BACON)
# True (別名でも同じclassで値が同じだから)
```

```python:異なるEnumとの比較
import enum

class Spam(enum.Enum):
   HAM = 1
   EGG = 2
   BACON = 2
   
class Spam2(enum.Enum):
   HAM = 1
   EGG = 2
   BACON = 2

print(Spam.HAM == Spam2.HAM)
# False (別のクラスだから)

print(Spam.HAM == 1)
# False (マジックナンバーとの比較しても一致しない)
```

引数の型ヒント以外を入力してもエラーにならないので、mypyなどの静的解析をすると良い
```python
import enum

class Spam(enum.Enum):
   HAM = 1
   EGG = 2

def func(spam: Spam)
   print(spam)

func('BECON') # エラーにならない
# 'BECON'

func(Spam['BECON']) # これはKeyErrorになる
```

## 9.6 itertoolsモジュール
### chain関数
複数のイテラブルオブジェクトを連結する
```python
import itertools

l = ['A', 'B']
s = 'ab'
t = (1, 2)
itr = itertools.chain(l, s, t)

for m in itr:
    print(m)
# 'A' → 'B' → 'a' → 'b' → 1 → 2
```

### groupby関数
```python
import itertools

for value, group in itertools.groupby('aaabbcdddaabb'):
    print(f'{value}: {list(group)}')
# a: ['a', 'a', 'a']
# b: ['b', 'b']
# c: ['c']
# d: ['d', 'd', 'd']
# a: ['a', 'a']
# b: ['b', 'b']
```

```python:Keyに比較関数を指定する
import itertools

def is_odd(num):
    if num % 2 == 1:
        return '奇数'
    else:
        return '偶数'

numbers = [10, 20, 31, 11, 3, 4]

for value, group in itertools.groupby(nembers, key=is_odd):
    print(f'{value}: {list(group)}')
# '偶数': [10, 20]
# '奇数': [31, 11, 3]
# '偶数': [4]
```

### islice関数

指定したイテラブルオブジェクトから、指定した範囲のイテレーターを作成する。
```python
import itertools

l = [0, 1, 2, 3, 4]
l2 = itertools.islice(l, 3) # 先頭から3つを指定
print(l2)
# [0, 1, 2]
```
`islice(iterable, start, stop, step=1)`の指定もできる。
iterableの長さが不明な場合、start, stop, stepにマイナス値を指定すると`ValueError`になる

### zip関数
組み込み関数のため、`itertools`の`import`は不要。
短いほうに合わせる。
```python
l1 = [0, 1, 2, 3]
l2 = ['a', 'b', 'c']

for m in zip(l1, l2):
    print(m)
# (0, 'a')
# (1, 'b')
# (2, 'c')
```
python3.10で`strict=True`引数が追加されて、長さが異なると`ValueError`になる。

### zip_longest関数
`itertools`の`import`が必要。
```python
import itertools

l1 = [0, 1, 2, 3]
l2 = ['a', 'b', 'c']

for m in itertools.zip_longest(l1, l2, fillvalue='-'):
    print(m)
# (0, 'a')
# (1, 'b')
# (2, 'c')
# (3, '-') # fillvalue未指定の場合はNoneになる
```

### 組み合わせイテレーターを取得する関数

| 関数 | 説明 | 結果例 |
|:-|:-|:-|
| `itertools.product(*itr, repeat=1)`  | 各itrの全要素の組み合わせ  | AA, AB, AC, BA, BB, BC, CA, CB, CC  |
| `itertools.permutations(itr, r=None)`  | 順列  | AB, AC, BA, BC, CA, CB<br>(AA, BB, CCがない)  |
| `itertools.combinations(itr, r)`  | 重複なしの組み合わせ  | AB, AC, BC |
| `itertools.combinations_with_replacement(itr, r)`  | 重複ありの組み合わせ  | AA, AB, AC, BB, BC, CC  |

## 9.7 copyモジュール
### copy関数
1層までは新しいオブジェクトになるが、多次元リストなど2層以上は参照コピーになる。
```python
import copy

values = [1, 2, 3, 4]
values2 = copy.copy(values)
```

### deepcopy関数
多次元リストなど2層以上も新しいオブジェクトになる。

# 10.汎用OS・ランタイムサービス(出題数2：5%)
## 10.1 osモジュール
### ファイルとディレクトリ操作
後者は中間のディレクトリも作成する。
* `mkdir`と`mkdirs`
* `rename`と`renames`
中間ディレクトリのパーミッションは`os.umask(8進数)`で設定できる。

### ファイルパスの定数

| 定数名 | 説明 |
|:-|:-|
| `os.curdir`  | カレントディレクトリ  |
| `os.pardir`  | 親ディレクトリ  |
| `os.sep`  | ディレクトリ区切り文字  |
| `os.extsep`  | 拡張子区切り文字  |
| `os.linesep`  | 行の終端  |

### システム情報の関数
利用できるのはUnixのみ
| 関数名 | 説明 |
|:-|:-|
| `os.confstr(str)`  | システム設定値を文字列で返す。<br>渡す文字列は`os.confstr_names`で取得できる。  |
| `os.sysconf(str)`  | システム設定値を数値で返す。<br>渡す文字列は`os.sysconf_names`で取得できる。  |
| `os.getloadavg()`  | ロードアベレージを返す (過去1分間、5分間、15分間)  |
| `os.cpu_count()`  | CPU数を取得する。取得できなければNoneを返す。  |

`multiprocessing.cpu_count()`は取得できないと`NotImplementedError`が発生する。

`psutil`モジュール(サードパーティ)もある。
| 関数名 | 説明 |
|:-|:-|
| `psutil.cpu_times()`  | CPU時間を求める  |
| `psutil.cpu_times_percent(interval=1, percpu=False)`  | 特定のCPU時間ごとの使用率  |
| `psutil.virtual_memory()`  | メモリの使用率  |
| `psutil.disk_partitions()`  | ディスクパーティション情報  |
| `psutil.net_io_counters()`  | ネットワークI/O  |

### ランダムな文字列を生成
```python:10バイトのランダムなbytesオブジェクトを生成する
print(os.urandom(10))
# b'＜10バイト分のデータ＞'
```
`randam`モジュールは簡易的なものなので、セキュリティを考慮する必要がある場合は`os.urandom()`もしくは`secrets`モジュールを使う。

## 10.2 ioモジュール

| ストリーム | 説明 |
|:-|:-|
| `io.StringIO`クラス  | メモリー上でテキストファイルのように扱う  |
| `io.BytesIO`クラス  | メモリー上でバイナリファイルのように扱う  |

* ストリームを`close()`した後に書き込み等を行うと`ValueError`が発生する
* `io.BytesIO`の`getbuffer()`は値変更すると元データも変更される
  * `getvalue()`は値変更できない
* ストリームを他関数に渡すときは`seek(0)`で先頭に戻さないと意図する動きにならないかもしれない

## 10.3 sysモジュール
### sys.path
`sys.path`はパッケージ・モジュールの探索するときに参照される。
以下の順番でパスが入っている。
* pyファイルと同じディレクトリ、カレントディレクトリ
* `PYTHONPATH`
* Pythonのインストール先

### sys.breakpointhook関数
```python
import sys

def func():
    print('hello')

sys.breakpointhook = func

print('start')
breakpoint() # ここでfunc()が呼び出される
print('end')
```

## 10.4 argparseモジュール
```python
import argparse

parser = argparse.ArgumentParser(
    prog=sys.argv[0], # プログラム名
    usage='利用方法',
    description='ヘルプ文の前に表示される説明',
    epilog='ヘルプ文の後に表示される説明',
    parents=[ArgumentParserオブジェクトのリスト],
    formatter_class=argparse.HelpFormatter, #フォーマットをカスタマイズするクラス
    prefix_chars='-', # 引数の先頭文字 ハイフン以外に変更できる
    fromfile_prefix_chars=None, # 引数にファイルを指定する際のプレフィックス @file.txtとか
    argument_default=None, # パーサー全体の引数のデフォルト値
    conflict_handler='error', # 複数オプション指定された時の挙動
    add_help=True, # -hオプションを追加するかどうか
    allow_abbrev=True, # 長いオプションを先頭の1文字に短縮できるようにするかどうか
    exit_on_error=True # Trueはプロセス終了、Falseはargparse.ArgumentError例外
)

# required=Trueにすると-s/--stringの指定が必須になる
parser.add_argument('-s', '--string', type=str, help='data of string', required=True)

parser.add_argument('--input', type=argparse.FileType('r'))

args = parser.parse_args() # 実行時に指定されたオプションをパースする

print(args.string) # add_argmentで追加したものが属性でアクセスできる
input_file_data = args.input.read() # ファイルが読み込める
```

`Click`というサードパーティパッケージは、デコレーターで同じことができる。
```python
import click

@click.command()
@click.option('-s', '--string', help='data of string', requred=True)

print(string)
```

# 11.ファイルとディレクトリへのアクセス(出題数2：5%)
## 11.1 pathlibモジュール
```python:継承関係イメージ
class PurePath()
    """純粋パス 基底クラス"""
    drive: str
    root: str
    def is_absolute(): # 絶対パスか
    def is_relative_to(*other): # otherに対して相対パスか

class PurePosixPath(PurePath) # Unix向け純粋パス
class PureWindowsPath(PurePath) # Windows向け純粋パス

class Path(PurePath)
    """具象パス 基底クラス"""
    def cwd()->str: # 現在のディレクトリを返す
    def stat()->os.stat_result: # ファイルのプロパティ情報を返す
    def rmdir(): # ディレクトリが空であること

class PosixPath(PurePosixPath, Path) # Unix向け具象パス
class Windowspath(PureWindowsPath, Path) # Windows向け具象パス
```
純粋パスは実際にファイルがなくても扱える。
`Path`は`path-like`オブジェクトの一種。

```python:exist_okの例
from pathlib import Path

p = Path('sample_dir')
p.mkdir()
p.mkdir() # FileExistsError例外になる
p.mkdir(exist_ok=True) # 例外にならないので、いちいち存在チェックしなくてよくなる
```

* ファイル削除は`Path.unlink(missing_ok=True)`にすると、存在しなくても例外にならない

## 11.2 tempfileモジュール
| オブジェクト | 説明 | 出力先 |
|:-|:-|:-|
| `tempfile.TemporaryFile()`  | ファイル名のない一時ファイルを作成<br>`exist()`で存在確認できない  | ファイルシステム |
| `tempfile.NamedTemporaryFile()`  | ファイル名がある一時ファイルを作成  | ファイルシステム |
| `tempfile.SpooledTemporaryFile()`  | 一定サイズまでメモリーで、超えるとディスクに書き出される<br>`exist()`で存在確認できない  | メモリ→ファイルシステム |
| `tempfile.TemporaryDirectory()`  | 一時ディレクトリを作成  | ファイルシステム |

## 11.3 shutilモジュール
高水準なファイル操作

### ファイルコピー
| 関数 | 説明 |
|:-|:-|
| `shutil.copyfile(src, dst)`  | srcファイル dstファイルにコピー<br>※dstにディレクトリは指定不可  |
| `shutil.copy(src, dst)`  | ファイルのパーミッションもコピー  |
| `shutil.copy2(src, dst)`  | すべてのメタデータ(変更時刻等)もコピー  |
| `shutil.copymode(src, dst)`  | パーミッションをコピー  |
| `shutil.copystat(src, dst)`| メタデータをコピー |

コピー元・先がシンボリックリンクで同じものを指している場合は、`SameFileError`例外が発生する。

### 再帰的に操作する

| 関数 | 説明 |
|:-|:-|
| `shutil.copytree(src, dst, ignore)`  | srcディレクトリ以下を再帰的にコピーする  |
| `shutil.move(src, dst)`  | srcディレクトリ以下を再帰的に移動する  |
| `shutil.rmtree(path, ignore_errors=False, onerror=None)`  | pathで指定したディレクトリ以下を削除する  |

```python:copytree( )にignoreを指定するサンプル
import shutil

ignore_pattern = shutil.ignore_patterns('*.pyc', '*.swp')
shutil.copytree('from', 'to', ignore=ignore_pattern) # *.pycと*.swpはコピーされない
```

# 12. データ圧縮、アーカイブと永続化(出題数0)
pass

# 13.特定のデータフォーマットを扱う(出題数2：5%)
以下は出題なし
* 13.3 configparser
* 13.4 PyYAML
* 13.5 openpyxl
* 13.6 Pillow

## 13.1 CSV
### csv.reader() / csv.writer()
```python
import csv

# 読み込み
with open('sample.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

# 書き込み
with open('sample2.csv', 'w', encoding='utf-8') as f:
    writer = csv.writer(f,
                        delimiter='\t', # 区切り文字
                        quotechar='#', # 引用符
                        skipinitialspace=True, # 区切り文字直後のスペースを削除
                        lineterminator='\r\n' # 改行文字)
    writer.writerow(row) # 1行書き込み
    writer.writerows(rows) # 複数行書き込み
```

### csv.DictReader() / csv.DictWriter()
```python
import csv

dict_data = [
    {'都道府県': '東京都', '人口密度': 6335},
    {'都道府県': '神奈川', '人口密度': 3807},
    {'都道府県': '千葉', '人口密度': 1207}
]

# 書き込み
with open('result.csv', newline='', mode='w', encoding='utf-8') as f:
    filed_names = ['都道府県', '人口密度']
    writer = csv.DictWriter(f, filednames=filed_names)
    writer.writeheader()
    writer.writerows(dict_data)

# 読み込み
with open('result.csv', 'r', encoding='utf-8') as f:
    # csv.Snifferを使って、先頭1024バイトからCSVの形式を推測して読み込む
    data_sample = f.read(1024)
    f.seek(0)
    
    print(csv.Sniffer().has_header(data_sample)) # ヘッダ行があればTrue
    dialect = csv.Sniffer().sniff(data_sample)
    
    for row in csv.DictReader(f, dialect):
        print(row)
```

## 13.2 jsonモジュール

| 関数 | 説明 |
|:-|:-|
| `json.dumps(obj)->str`  | obj(dict等)をJSON文字列にする<br>JSONにできないデータ型があると`TypeError`例外になる  |
| `json.dump(obj, file)`  | obj(dict等)をfile保存する  |
| `json.loads(json_string)->obj`  | JSON文字列からobj(dict等)に変換する  |
| `json.load(file)->obj`  | ファイルからobj(dict等)に変換する  |

"s"が付けば"string(文字列)"と覚える。

# 14.インターネット上のデータを扱う(出題数2：5%)
以下は出題なし
* 14.3 Requests
* 14.5 email

## 14.1 urllib.parseモジュール
### parse.urlparse関数
```python
from urllib import parse

parse_result =  parse.urlparse('https://www.expample.com/test/;parameter?q=example#hoge')
print(parse_result.scheme) # 'https'
print(parse_result.hostname) # 'www.expample.com'
print(parse_result.path) # '/test'
print(parse_result.query) # ?q=example
print(parse_result.fragment) # #hoge
```

### parse.parse_qs関数
クエリ文字列を解析して、パース結果をdictにする
```python
from urllib import parse

query_dict = parse.parse_qs('q=python&oq=python&sourceid=chrome')
# {'q': ['python'],
#  'oq': ['python'], 
#  'sourceid': ['chrome']}

# 同じキーがある場合
query_dict = parse.parse_qs('param=aaa&param=bbb')
# {'param': ['aaa', 'bbb']}

# keep_blank_values=Trueの場合
query_dict = parse.parse_qs('param1=&param2=bbb', keep_blank_values=True)
# {'param1': [''], ※デフォルト(False)だとparam1は出力されない
#  'param2': ['bbb']}
```

### parse.parse_qsl関数
パース結果がtupleになる
```python
from urllib import parse

query_tuple = parse.parse_qsl('q=python&oq=python&sourceid=chrome')
# [('q', 'python'), ('oq', 'python'), ('sourceid', 'chrome')]

# 同じキーがある場合、query_qsと違って別々になる
query_tuple = parse.parse_qsl('param=aaa&param=bbb')
# [('param', 'aaa'), ('param', 'bbb')]
```

### parse.urlencode関数
dict等からクエリ文字列を作成する
```python
from urllib import parse

query_dict = {
    'key1': 'hoge',
    'key2': ['foo', 'bar']
}

query_str = parse.urlencode(query_dict)
print(query_str)
# 'key1=hoge&key2=foo+bar'

# doseq=Trueの場合
query_str = parse.urlencode(query_dict, doseq=True)
print(query_str)
# 'key1=hoge&key2=foo&key2=bar' ※key2が２つある

# quote_via引数の指定
query_str = parse.urlencode({'key1': ' '})
# 'key1=+' ※デフォルトではスペースは "+" に変換される

query_str = parse.urlencode({'key1': ' '}, quote_via=parse.quote)
# 'key1=%20' ※スペースが%20に変換される
```

### parse.quote関数 / quote_plus関数
特殊文字や2バイト文字をパーセントエンコードする。

| 関数 | スペース文字 | スラッシュ(/) |
|:-|:-|:-|
| `parse.quote()`  | %20  | パーセントエンコードされない |
| `parse.quote_plus()`  | + | パーセントエンコードされる |

* `_` `.` `-` `~`はデフォルトではパーセントエンコードされない
* `parse.quote('_.-~', safe='_.-~')`とすればパーセントエンコードされる

### parse.urljoin関数
URLを結合する
```python
from urllib import parse

url_str = parse.urljoin('https://ja.wikipedia.org/wiki/Python', '#ライブラリ')
# 'https://ja.wikipedia.org/wiki/Python#ライブラリ'

# 相対パスの場合
url_str = parse.urljoin('https://ja.wikipedia.org/wiki/Python', '../../wiki/Python')
# 'https://ja.wikipedia.org/wiki/Python'

# URL同士の場合、左側が返される
url_str = parse.urljoin('https://www.python.org', 'https://www.expample.com')
# 'https://www.python.org'
```

## 14.2 urllib.requestモジュール

| HTTPメソッド | `urlopen()`の呼び出し方 |
|:-|:-|
| GET  | `urlopen(url=<url>)`または<br>`urlopen(url=Request(url=<url>))`  |
| POST  | `urlopen(url=<url>, data=string.encode())`または<br>`urlopen(url=Request(url=<url>, data=string.encode()))`  |
| HEAD  | `urlopen(url=Request(url=<url>, method='HEAD'))`  |
| PATCH  | `urlopen(url=Request(url=<url>, method='PATCH'))`  |
| PUT  | `urlopen(url=Request(url=<url>, method='PUT'))`  |
| DELETE  | `urlopen(url=Request(url=<url>, method='DELETE'))`  |
| OPTIONS  | `urlopen(url=Request(url=<url>, method='OPTIONS'))`  |

```python:ファイルのダウンロード(GET)
from urllib import request

# JPEGをダウンロードしてローカル保存する
response = request.urlopen('https://expample.com/image/jpeg')

file_data = response.read()
with open('result.jpg', 'wb') as f:
    f.write(file_data)

print(response.url) # 'https://expample.com/image/jpeg'
print(response.status) # 200
print(response.headers)
# Date: Fri, 07 May 2021 13:55:48 GMT
# Content-Type: image/jpeg
# Content-Length: 1024
# ・・・
```

```python:カスタムヘッダー
from urllib import request

costom_header = {'Accept': 'application/json'}
reqest.urlopen(url=reqest.Request('http://expample.com/', headers=costom_header))
```

## 14.3 base64モジュール
### Base64エンコード
```python
import base64

s = 'Pythonは簡単に習得でき、それでいて強力な言語の1つです。'

base64.b64encode(s.encode(), altchars=b'@*') # 文字列をバイト列にして渡す
# altcharsの1つ目で'+'を'@'に置き換える
#           2つ目で'/'を'*'に置き換える

# URL向けエンコード処理
base64.urlsafe_b64encode(s.encode())
# デフォルトで'+'は'-'に、
#            '/'は'_'に置き換わる

# JSON等に画像を含めたい場合に利用する
with open('./sample.png', 'rb') as png:
    b64_img = base64.b64encode(png.read())
data = {'img': b64_img}
json_str = json.dumps(data)
```

### Base64デコード
```python
import base64

s = 'Pythonは簡単に習得でき、それでいて強力な言語の1つです。'
base64_s = base64.b64encode(s.encode())

decode_s = base64.b64decode(base64_s).decode() # バイト列から文字列へのデコードも必要

# urlsafe_b64encode()をデコードする関数もある
decode_s = base64.urlsafe_b64decode(base64_s).decode()
```

# 15.HTML/XMLを扱う(出題数0)
pass

# 16.テスト(出題数3：7.5%)
以下は出題なし
* 16.4 pytest
* 16.5 pydoc

## 16.1 doctest
* docstringに「>>>実行コード」、次の行に期待結果を記載して、`doctest.testmod()`でテストを動かして検証する
  * 対話形式でPythonを実行するイメージで書く
* docstringのサンプルコードが実際に動くことを保証する
* CIでdoctestを動かすことで、docstringの更新忘れを検出する

```python: doctest_sample.py
"""
与えられた引数を使って割り算を行う。

>>> div(5, 2)
2.5
"""

def div(a, b):
    """
    割り算の答えを少数で返す

    >>> [div(i, 2) for i in range(5)]
    [0.0, 0.5, 1.0, 1.5, 2.0]
    """
    return a / b

def foo()
    """
    複数行の書き方

    >>> foo() = {'Hermione', 'Harry'}
    True
    >>> d = sorted(foo()) #集合は順番が保証されないため、ソートしないと失敗する可能性がある
    >>> d
    ['Harry', 'Hermione']
    """
if __name__ == '__main__':
    import doctest
    doctest.testmod()
```

```shell:実行例
$ python doctest_sample.py # 何も表示されなければテスト成功
$ python doctest_sample.py -v # 詳細なログが表示される
Trying:
    div(5, 2)
Expecting:
    2.5
ok

Trying:
    [div(i, 2) for i in range(5)]
Expecting:
    [0.0, 0.5, 1.0, 1.5, 2.0]
ok

Trying:
   ・・・
   
3 items in 4 items.
4 passed and 0 failed.
Test passed.
```

```python:期待結果が例外の場合
def div(a, b):
    """
    割り算の答えを少数で返す

    第2引数がゼロだった場合、ZeroDivisionError例外がスローされる
    >>> div(2, 0)
    Traceback (most recent call last):
        ... # コールスタックが不要な場合は「...」で省略可能
    ZeroVisisionError: division by zero
    """
    return a / b
```

doctestをテキストファイルに保存しておき、それを実行することもできる。
```python
import doctest

doctest.testfile('doctest_sample.txt')
```

## 16.2 unittest
特徴
* テストの自動化
* 初期設定と終了処理の共有化
* テストの分類
* テスト実行と結果レポートの分離

例外が送出されたかのアサーションは`assertRaises(exception)`でチェックする。
テストファイルがおかれているディレクトリに`__init__.py`が必要。

### subTestで全てのアサートを見る
`subTest(msg=None, **params)`
```python
import unittest
from expample import add # テスト対象の関数

class AddTest(unittest.TestCase):
    def test_add(self):
        patterns = [
            ('a', 'A'),
            ('b', 'B'),
            ('c', 'c'), # NG
            ('d', 'D'),
            ('e', 'e'), # NG
        ]

        for lower, upper in patterns:
            with self.subTest('%s is upper case of %s' % (upper, lower)):
                # 'c'で止まらず全てテストされる
                self.assertEqual(lower.upper(), upper)
```

### setUp/tearDownの順番
`@classmethod setupClass(cls)`(1度だけ)
↓
`setUp(self)`(テスト毎)
↓
`tearDown(self)`(テスト毎)
↓
`@classmethod tearDownClass(cls)`(1度だけ)

### コマンドラインから実行
```shell
$ python -m unittest test_module1 test_module2 # 2つのモジュールのテスト実行
$ python -m unittest test_module3.TestClass # 特定のテストクラスだけ動かす
$ python -m unittest test_module3.TestClass.test_func # 特定の関数関数だけ動かす
```

unittestのオプション
| オプション | 解説 |
|:-|:-|
| -b, --buffer  | 標準出力/エラーをテスト実行の間バッファリングする  |
| -c, --catch  | Ctrl+Cされても実行中のテスト終わるまで終了しない<br>2回目のCtrl+Cされたらすぐ終了する  |
| -f, --failfast  | テスト失敗したらすぐに停止する  |
| -k  | 正規表現で実行するテストを指定する  |
| --locals  | トレースバック内の局所変数を表示する  |

`discover`サブコマンドで、テストファイルの検出ルールをカスタマイズできる。
| オプション | 解説 |
|:-|:-|
| -v, --verbose  | 詳細な出力を行う  |
| -s, --start-directory  | 検出を開始するディレクトリを指定する<br>（デフォルトはカレントディレクトリ)  |
| -p, --perttern  | テストファイル名のパターンを指定する<br>(デフォルトは`test*.py`)  |
| -t, --top-level-directory  | プロジェクトの最上位の検出ディレクトリを指定する  |

## 16.3 unittest.mock
```python
from unittest.mock import MagicMock
from expample import ShoppingAPI # モック対象

api = ShoppingAPI()

# search_item関数の戻り値を差し替える
api.search_item = MagicMock()
api.search_item.return_value = ['モック商品1', 'モック商品2', 'モック商品3']

items = api.search_item(code=12345)
print(items)
# ['モック商品1', 'モック商品2', 'モック商品3']

# 例外を送出するように差し替える
api.search_item.side_effect = Exception('ダミーの例外')
api.search_item(code=12345)
# Traceback (most recent call last):
#    Exception: 'ダミーの例外'
```
### patch
```python:デコレーターを使う例
import unittest
from unittest.mock import patch
from expample import ShoppingAPI # モック対象

class ExampleTest(unittest.TestCase):
    @patch('ShoppingAPI.search_item')
    def test_shopping_api(self, APIMock):
        api = APIMock()
        api.search_item.return_value = ['モック商品1', 'モック商品2', 'モック商品3']
        result = api.search_item()
        self.assertEqual(result, ['モック商品1', 'モック商品2', 'モック商品3'])
```

```python:コンテキストマネージャ(with)を使う例
import unittest
from unittest.mock import patch
from expample import ShoppingAPI # モック対象

class ExampleTest(unittest.TestCase):
    def test_shopping_api(self):
        with patch('ShoppingAPI.search_item') as APIMock:
            api = APIMock()
            api.search_item.return_value = ['モック商品1', 'モック商品2', 'モック商品3']
            result = api.search_item()
            self.assertEqual(result, ['モック商品1', 'モック商品2', 'モック商品3'])            
```

patch対象が存在しない場合、`ModuleNotFoundError`例外になる。

### Mockが呼び出されたかどうかassert
| メソッド名 | 解説 |
|:-|:-|
| `self.assert_called()`  | モックが1回以上は呼び出されているか  |
| `self.assert_called_once()`  | モックが1回だけ呼び出されているか  |
| `self.assert_called_with(*args, **kwargs)`  | モックが指定された引数で1回以上呼び出されたか  |
| `self.assert_called_once_with(*args, **kwargs)`  | モックが指定された引数で1回だけ呼び出されたか  |
| `self.assert_not_called()`  | モックが1回も呼び出されていないか  |

# 17.デバッグ(出題数2：5%)
## 17.1 pdb、breakpoint
| pdbオプション | 解説 |
|:-|:-|
| h  | help ヘルプコマンド  |
| w  | where スタックトレースを出力する  |
| n  | next 次の行に進む  |
| l [s, e] | list 現在の位置のソースコード周囲11行を表示する。s, eで行数を指定できる。   |
| c  | continue ブレークポイントに到達するまで実行する  |
| p 変数  | print 変数の値を出力する  |
| pp 変数  | pprintモジュールを使って変数の値を出力する  |
| q  | quit デバッグを終了する  |

### breakpointを設定する
```python
import pdb

def func():
    a = 1
    b = 2
    c = a + b
    breakpoint() # ここで止まる。pdb.set_trace()でもOK
    return c

if __name__ == '__main__':
    func()
```
対話モードでエラーが発生した場合は`pdb.pm()`を実行するとデバッグモードになる。

## 17.2 timeitモジュール
### コマンドラインから計測する
```shell
$ python -m timeit <実行オプション> '<pythonコード>'
```
| 実行オプション | 解説 |
|:-|:-|
| `-n N`, `--number=N`  | Pythonコードを実行する回数。未指定の場合、実行時間が0.2秒になる回数  |
| `-r N`, `--repeat=N`  | 計測の繰り返す回数（デフォルトは5)  |
| `-s <pythonコード>`,<br> `--setup=<pythonコード>`  | 最初に1度だけ実行する文 (デフォルトは`pass`)  |
| `-p`, `--process`  | 実行時間ではなくプロセス時間を計測する  |
| `-u`, `--unit=<単位>`  | 出力する時間の単位を指定する<br>`nsec`, `usec`, `msec`, `sec`から選択（デフォルトは`nsec`）   |
| `-v`, `--verbose`  | 1ループあたりの平均時間ではなく、詳細な計測結果を出力する  |

### Pythonコードで計測する
```python
import timeit

def func():
    """重たい処理"""

# ガベージコレクション込みで計測する
timeit.timeit('func()', setup='gc.enable()', globals=globals()) # funcはglobalsで名前空間の指定が必要

# オブジェクト化の例
t = timeit.Timer('foo()', setup='gc.enable()', globals=globals())
t.timeit(number=100) # 計測回数100(デフォルト100万回)
t.repeat(repeat=10, number=1000000) # 繰り返し回数10(デフォルト5x100万回)
```

## 17.4 loggingモジュール
デフォルトは以下
* メッセージは標準出力に出力される
* 出力フォーマットは「`ログレベル`:`ロガー名`:`メッセージ`」
* ログレベルはWARNING

| ログレベル | 値 | メソッド | 使うタイミング |
|:-|-:|:-|:-|
| CRETICAL  | 50  | `logging.cretical(msg)`  | プログラム全体が動作できないケース |
| ERROR  | 40  | `logging.error(msg)`  | プログラムの機能が動作できないケース |
| WARNING  | 30  | `logging.warning(msg)` ※デフォルト値 | プログラムは動作するが想定外の問題が発生したとき |
| INFO  | 20  | `logging.info(msg)`  | プログラムが正常に動作している場合 |
| DEBUG  | 10  | `logging.debug(msg)`  | デバッグ用 |
| NOTSET  | 0  | なし  | |

メッセージ指定は、f-stringを使わないようにする。
※3.11 + VSCode(Windows)だとf-stringでも動いた
```python
import logging

s = 'Pen'
logging.warning('I have a %s!', s)
# WARNING:root:I have a Pen!
```

### 階層構造
`logger = logging.getLogger('hoge.fuga.piyo')`とすると、
`root`→`hoge`→`fuga`→`piyo`の階層構造ができる。
`hoge.fuga`に対して設定をすると`piyo`にも適用される。


### logging.basicConfig
```python
import logging

logfmt = '%(asctime)s %(levelname)s %(message)s'
logging.basicConfig(filename='./test.log',
                    filemode='a',
                    style='%' or '{' or '$'
                    format=logfmt,
                    datefmt='%Y/%m/%d %H:%M:%S.nnn',
                    level=logging.DEBUG,
                    stream=ストリーム(filenameと排他),
                    handlers=実行する関数(filename, streamと排他),
                    force=True # 他で設定されたものがリセットされる
                    encoding='utf-8',
                    errors='?' # 出力できない文字を置き換える(デフォルトはバックスラッシュ?)
                    
```
フォーマットに使える属性
| フォーマット | 解説 |
|:-|:-|
| `%(asctime)s`  | `2024-12-31 23:59:59,123`の形式の時刻  |
| `%(filename)s`  | ロギング呼び出しファイル名  |
| `%(pathname)s` | ロギング呼び出しフルパス |
| `%(funcName)s`  | ロギング呼び出し関数名  |
| `%(levelname)s`  | ログレベル名  |
| `%(lineno)d`  | ロギング呼び出しの行番号  |
| `%(module)s`  | モジュール名(filenameの名前部分)  |
| `%(message)s`  | ログメッセージ  |
| `%(name)s`  | ロギングの名前(`logging.getLogger(name)`)  |
| `%(process)d`  | プロセスID  |
| `%(thread)d`| スレッドID |

### logging.dictConfig
```python
import logging
from logging.config import dictConfig

config = {
    'version': 1, # 1のみサポート
    'disable_existing_loggers': False # 既存の設定を無効化しない(デフォルトTrue)
    'formatters': { # フォーマットを定義できる
        'format1': {
            'format': '%(asctime)s %(levelname)s %(message)s'
        }        
    },
    'filters': { # フィルターを定義できる
        'hoge-filter': {
            'name': 'hoge.fuga'
        }
    },
    'handlers': { # ハンドラーを定義できる
        'file': {
            'level': 'INFO',
            'filename': './test.log',
            'formatter': 'format1'
        },
        'console': {
            'level': 'ERROR'
            'formatter': 'format1'
        }
    },
    'loggers': { # ロガーを定義
        'hoge': {
            'handlers': ['file', 'console'],
            'level': 'INFO'
            # ・・・
        },
    }, 
}
```

ファイルローテーションのhandler
| handler | 説明 |
|:-|:-|
| `logging.handlers.RotatingFileHandler`  | ログファイルが一定のファイルサイズになったらローテーションする  |
| `logging.handlers.TimedRotatingFileHandler`  | 特定の時間間隔でローテーションする  |

### logging.fileConfig
今後の機能追加は`logging.dictConfig`に対して行われているため、利用は非推奨。

# 18.暗号関連(出題数1：2.5%)
18.3 cryptographyは出題なし

## 18.1 secretsモジュール
安全な乱数を生成する。

### choise関数
```python:8桁のパスワード文字列を生成する例
import secrets
import string

password_chars = string.ascii_letters + string.digits
password = ''.join(secrets.choise(password_chars) for i in range(8))
print(password)
# 'POVQu190'
```

### token_xxx関数
| 関数名 | 概要 | デフォルトサイズ |
|:-|:-|:-|
| `secrets.token_bytes(nbytes=None)`  | トークン(バイト列)を返す  | 32  |
| `secrets.token_hex(nbytes=None)`  | 16進数の文字列を返す  | 64  |
| `secrets.token_urlsafe(nbytes=None)`  | URL用のテキストとしてBase64エンコードされた文字列を返す  | 43  |

トークンの比較は、`secrets.compare_digest(a, b)`を使う。

## 18.2 hashlibモジュール
ハッシュ値(不可逆値)を生成する。

```python
import hashlib

# 利用可能なハッシュアルゴリズムを取得する
print(hashlib.algorithms_available) # 実行プラットフォームに依存する一覧
print(hashlib.algorithms_guaranteed) # 実行プラットフォームに依存しない一覧
# {'sha512', 'sha1', 'md4', ・・・}

hash = hashlib.sha256()
hash.update(b'python library book 2')
hash_value = hash.hexdigest()
print(hash_value)
# 'feb7058093ca35f79...'
```

| ハッシュアルゴリズム | バイト数 | 解説 |
|:-|:-|:-|
| SHA-1 | 20 | Secure Hash Algorithm-1。脆弱性があり非推奨 |
| SHA-256  | 32  | SHA-1の後継であるSHA-2の主要アルゴリズム  |
| SHA-512  | 64  | SHA-256よりも安全性が高い  |
| SHA3-512  | 64  | SHA-3アルゴリズムで、SHA-2と同等の安全性  |
| BLAKE2b  | 64  | 高速なアルゴリズムで64bit環境に最適化されている<br>32bit環境向けにはBLAKE2sを使用する  |
| MD5  | 16  | ファイルの整合性検証に使われる。脆弱性があり、暗号化の用途では推奨されていない  |

```python:ファイルの整合性検証の例
import hashlib

# Python-3.9.6.tgzの期待するハッシュ値
check_sum = '798b9d3e866e1906f6e32203c4c560fa'

hash_md5 = hashlib.md5()
with open('Python-3.9.6.tgz', 'rb') as f:
    hash_md5.update(f.read())

print(check_sum == hash_md5.hexdigest())
# True
```

### pbkdf2_hmac関数でソルト(salt)を付ける
```python
import hashlib

password = b'user_password'
salt = b'your_secret_salt'
iteration = 100000 # SHA-256では10万回が推奨

hashed_passwd = hashlib.pbkdf2_hmac('sha256', password, salt, iteration).hex()
print(hashed_passwd)
# '68edb4f6...'
```

ソルトは、毎回同じものを使うわないようにする。
ソルトは、なるべく長いものを使用する。

# 19.並行処理、並列処理(出題数0)
pass