🖊

Python3エンジニア認定実践試験の勉強をするよ その2

2024/11/19に公開

はじめに

Python実践レシピに沿って学習します。
各項目について「試験合格に必要な情報」と「実務を見据えた情報」の2本立てで整理します。

  • 試験対策
    • 各章の「覚えておくこと」を参照
    • 本の内容をほぼ並べるだけって時は省略します(意味がないので)
  • 実務に向けた情報
    • 各章の「役に立つこと」を参照
    • 筆者の経験上、一緒に押さえておきたいこと、さらに深堀りしたいポイントを記載

以前の記事

1. 例外処理

覚えておくこと

例外とは?

Python実行中に検出されるエラーのことを、例外(Exception)と呼ぶ。

1 / 0  # 0除算: ZeroDivisionError

# 例外が発生すると、以下のようにTracebackが出力されプログラムが強制終了する。
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# ZeroDivisionError: division by zero

例外処理とは?

エラー発生時にどのように処理をするか定めたもの。

実行継続が難しいような例外が常に発生するとは限らない。
そのため、発生が想定できる例外について事前に対応処理(=例外処理)を考えておく。
これにより、プログラムが予期しないタイミングで終了することを防ぐことができる。

Pythonに標準で備わっている「組込み例外」と、自作で定義ができる「自作例外」がある。
組込み例外については、組込み例外の階層構造を参照

例外処理の構文: try-except

class MyError(Exception):
    """自作例外"""
    pass

try:
    ...
    # !!!! 期待していないデータが渡されるなどの問題発生 !!!!
    if ext != ".csv":
        raise MyError("拡張子違うじゃないっすか")

except MyError as e: # try内でMyErrorが発生したことを補足
    print(f"拡張子間違えてるんで、xxしますね")

except Exception as e: # 何かしらの例外が発生したことを補足
    print(f"とりあえず例外起きたからooするね")

else: # 例外が起きなかった
    print("正常に終わったよ")

finally: # 例外発生しようがしまいが、必ず動かす
    print("ファイル閉じてないからクローズするぞ")
    xxx.close()

役に立つこと

組込み例外の階層構造

標準実装されている例外は、以下の公式ドキュメントを参照してください。
各例外はBaseExceptionを最上位の親クラスとして継承した子クラスとなります。
例外のクラス階層

例外処理の考え方

実務で開発するにあたり、例外処理を考えることは重要です。
ちょっと期待外の動かし方をされただけで、急に落ちるようなシステムって使い物になりませんよね?
ということで、参考記事

準正常系テストや異常系テストについても把握しておくといいかも。

2. with文

覚えておくこと

上記コメントを踏まえたうえで、with文についてまとめます。

# with句を使わない場合
file = open("tmp.txt", mode="w", encoding="shift-jis")
file.write("Hello")
file.close() # 明示的にクローズしないとファイルを開きっぱなしになる
# with句を使う場合
with open("tmp.txt", mode="w", encoding="shift-jis") as f:
    f.write("Hello")

# with句のインデントを抜けると、ファイルは自動的にクローズされる

リソース解放を確実に実施できるため、ファイルIOの時はwithを用いることを推奨します。

コンテキストマネージャーの仕組み

コンテキストマネージャーのポイント

  • クラス実装は__enter____exit__メソッドが必要

    • with文でコンテキストマネージャーとして呼び出されたときに__enter__実行
    • with文を抜けるときに__exit__実行
  • 関数実装はcontextlib.contextmanagerによるデコレーターを使用

from contextlib import contextmanager
import sqlite3


@contextmanager
def connect_db(db_name: str):
    try:
        # with句に入るとき: DBへ接続
        conn = sqlite3.connect(db_name)
        yield conn  # as で取得可能
    except:
        # 例外発生時に実行
        raise
    finally:
        # with句を抜けるときDB接続クローズ
        conn.close()


with connect_db("test.sqlite3") as return_conn:
    # DB接続して実行したい処理をwith内に実装
    cursor = return_conn.cursor()
    cursor.execute(
        "CREATE TABLE USERS (UserID varchar(255) NOT NULL, PRIMARY KEY (UserID)) "
    )

# finally句に入ってクローズされる

3. 関数の引数

覚えておくこと

位置引数 / キーワード引数

引数の種類として、大きく分けてタイトルの2種があります。
まとめてみていきましょう。

def setting_config_pattern1(
    param1: str,
    param2: str,
    value1: int,
    rate: float = 0.8,  # デフォルト値付きの仮引数
    threshold_lower: float = 0.3,
):
    print(f"{param1=}\n{param2=}\n{value1=}\n{rate=}\n{threshold_lower=}\n")

# ===================================================================
# 位置引数     : 設定した引数の順番に対応
# キーワード引数: 仮引数名=実引数 という形で渡せる。順不同
# ===================================================================
# OK
setting_config_pattern1(
    "set1", # 位置 = param1
    "set2", # 位置 = param2
    value1=10, # キーワード
    rate=0.7, # キーワード
)
# param1='set1'
# param2='set2'
# value1=10
# rate=0.7 <= 実引数がセット
# threshold_lower=0.3 <= 実引数を渡さないと、デフォルト値がセット

# ===================================================================
# キーワード引数は順不同
# ===================================================================
# OK
setting_config_pattern1(
    "set1",  # 位置 = param1
    rate=0.7,  # キーワード
    value1=1000,  # キーワード
    param2="set2",  # キーワード
)
# param1='set1'
# param2='set2'
# value1=1000
# rate=0.7
# threshold_lower=0.3

# ===================================================================
# 関数呼び出しの時、位置引数 -> キーワード引数の順で渡す必要がある
# ===================================================================
# NG
setting_config_pattern1(
    "set1", # 位置 = param1
    value1=10, # キーワード
    "set2", # 位置 = param2
    rate=0.7, # キーワード
)
# SyntaxError: positional argument follows keyword argument

位置専用引数 / キーワード専用引数

def setting_config_pattern2(
    param1: str,
    param2: str,
    value1: int,
    /,  # 位置専用引数の設定
    *,  # キーワード専用引数の設定
    rate: float = 0.8,
    threshold_lower: float = 0.3,
):
    print(f"{param1=}\n{param2=}\n{value1=}\n{rate=}\n{threshold_lower=}\n")

# ===============================================================
# "/" より前に設定された引数は、位置引数として呼び出す必要がある。
# ===============================================================
# NG
setting_config_pattern2(
    "set1",
    "set2",
    value1=10,  # value1は位置引数である必要がある
    rate=0.7,
)
# TypeError: setting_config_pattern2() got some positional-only arguments passed as keyword arguments: 'value1'

# ===============================================================
# "*" より後に設定された引数は、キーワード引数として呼び出す必要がある
# ===============================================================
# NG
setting_config_pattern2(
    "set1",
    "set2",
    10,
    0.7,    # rateは位置引数である必要がある
)
# TypeError: setting_config_pattern2() takes 3 positional arguments but 4 were given

可変長位置引数 / 可変長キーワード引数

# * をつけた仮引数は、可変長(任意の数)の位置引数を受けることができます
def positional_args_sum(*args: int, start=0):
    ret = start
    print(f"{args=}")   # args=(1, 2, 3)  タプルで取得できる
    for num in args:
        ret += num

    return ret


positional_args_sum(1, 2, 3, start=100)
# -> 106
# ** をつけた仮引数は、可変長(任意の数)のキーワード引数を受けることができます。
def keyword_args_config(**kwargs: dict):
    print(f"{kwargs}")
    # {'param1': 'set1', 'param2': 'set2', 'value1': 10, 'rate': 0.8, 'threshold_lower': 0.3}
    for k, v in kwargs.items():
        print(f"{k}: {v}")


keyword_args_config(
    param1="set1",
    param2="set2",
    value1=10,
    rate=0.8,
    threshold_lower=0.3,
)
# param1: set1
# param2: set2
# value1: 10
# rate: 0.8
# threshold_lower: 0.3

4. アンパック

覚えておくこと

複数の要素を持つ型(タプル、リスト、辞書など)から要素をまとめて取り出して変数に入れる機能。

list_1 = [1, 2, 3]
a1, a2, a3 = list_1
print(f"{a1=}, {a2=}, {a3=}")
# a1=1, a2=2, a3=3

# 辞書の場合はキーワードのほうが渡される
dict_1 = {"key1": 1, "key2": 2}
k1, k2 = dict_1
print(f"{k1=}, {k2=}")
# k1='key1', k2='key2'

# 値を渡すならvaluesメソッドを使用
v1, v2 = dict_1.values()

関数にまとめて実引数を渡すときにも使うことができる

def setting_config_pattern1(
    param1: str,
    param2: str,
    value1: int,
    rate: float = 0.8,
    threshold_lower: float = 0.3,
):
    print(f"{param1=}\n{param2=}\n{value1=}\n{rate=}\n{threshold_lower=}\n")

# ==============================================
# * で位置引数をアンパックで渡すことができる
# ==============================================
args = ["s1", "s2", 1, 0.1, 0.1]
setting_config_pattern1(*args)


# ==============================================
# ** で辞書をアンパックで渡すことができる
# ==============================================
kwargs = {
    "threshold_lower": 0.9,     # keyを仮引数と同名にする
    "rate": 0.9,                # キーワード引数のように順不同で渡せる
    "param1": "s1",
    "param2": "s2",
    "value1": 1000,
}
setting_config_pattern1(**kwargs)
# param1='s1'
# param2='s2'
# value1=1000
# rate=0.9
# threshold_lower=0.9

5. 内包表記

覚えておくこと

リスト内包表記

リストを高速に生成できる書き方。
普通にfor文で回すよりも速度が速いため、こちらを使うことが多い。
ただし、可読性が落ちる場合はfor文という判断もする。

number = [i for i in range(10)]
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

辞書型内包表記

リストと同じような形式で辞書を生成する書き方

params = ["param1", "param2", "param3"]
values = [1, 2, 3]

config = {key: value for key, value in zip(params, values)}
# {'param1': 1, 'param2': 2, 'param3': 3}

集合型内包表記

num = [1, 2, 2, 3, 3, 3]

set_num = {n for n in num}
# {1, 2, 3}

ジェネレーター式

gen = (i * 2 for i in range(5))

for g in gen:   # ループで要素を取り出すときに、ジェネレーター式の処理を実行
    print(g)
# 0
# 2
# 4
# 6
# 8

要素を取り出すタイミングで処理を実行する。
そのため、メモリの使用量を抑える効果がある。
6. ジェネレーターで、詳細に解説します。

6. ジェネレーター

覚えておくこと

ジェネレーター式にも書きましたが、ジェネレーターは要素を取り出すタイミングで処理を実行します。
これは大量のデータを扱うときに役に立ちます。

たとえば、1,000,000個の数値情報を持つリストの要素を2倍して、何かの処理に利用するとします。

import sys


def double_element(element):
    return element * 2


numbers = list(range(1_000_000))
base_size = sys.getsizeof(numbers)
print(f"{base_size=:,} Byte")

# 2倍した値を保持するリスト
double_numbers = []

for num in numbers:
    double_numbers.append(double_element(num))

double_size = sys.getsizeof(double_numbers)
print(f"{double_size=:,} Byte")

# base_size=8,000,056 Byte
# double_size=8,448,728 Byte

まず、最初のリストをジェネレーター式に変更します

import sys


def double_element(element):
    return element * 2


numbers = (i for i in range(1_000_000))
base_size = sys.getsizeof(numbers)
print(f"{base_size=:,} Byte")

# 2倍した値を保持するリスト
double_numbers = []

for num in numbers:
    double_numbers.append(double_element(num))

double_size = sys.getsizeof(double_numbers)
print(f"{double_size=:,} Byte")

# base_size=192 Byte
# double_size=8,448,728 Byte

次に、2倍した数値情報をジェネレーターで作ります

import sys


def double_element(elements: list[int]):
    # 関数内でループを動かす
    for elem in elements:
        # returnではなくyieldを使用
        yield elem


numbers = (i for i in range(1_000_000))
base_size = sys.getsizeof(numbers)
print(f"{base_size=:,} Byte")

double_numbers = double_element(numbers)

double_size = sys.getsizeof(double_numbers)
print(f"{double_size=:,} Byte")

# base_size=192 Byte
# double_size=192 Byte

このジェネレーターで作成したdouble_numbersは、forループやnext関数で取り出すことができます。

print(next(double_numbers))
print(next(double_numbers))
# 0
# 1

for n in double_numbers:
    print(n)
# 2
# 3
# 4
# ...
# 999996
# 999997
# 999998
# 999999

ジェネレーターは逐次に処理を行っていくものになるため、
一度最後まで回し切ると中身は空となります。

print(next(double_numbers))
# StopIteration                             Traceback (most recent call last)
# Cell In[17], line 1
# ----> 1 print(next(double_numbers))

# StopIteration:

for i in double_numbers:
    print(i)
# 何も出力されない

7. デコレーター

覚えておくこと

まずは構文をおまじない的に覚えておきましょう。

from dataclasses import dataclass


# @xxxx という形でクラスや関数の定義前につけることができます
@dataclass
class Config:
    param: str
    param2: str

デコレーターというものの実態を見るために、自作します。

import functools
import time
from typing import Callable


# デコレーター用の関数
def time_counter(func: Callable) -> Callable:
    @functools.wraps(func)  # ラップする関数のメタ情報を保持します(書籍参照)
    def wrap_time_counter():
        st = time.perf_counter()
        func()  # time_counter引数として渡された関数が実行
        ed = time.perf_counter()
        print(f"{func.__name__} : Time={ed-st:.3f}")

    return wrap_time_counter  # 関数が返却される(高階関数)


@time_counter
def make_list(element_num: int = 1_000_000):
    return [i for i in range(element_num)]

make_list()

以下のような呼び出し方をされているイメージです。

def make_list_no_decorator(element_num: int = 1_000_000):
    return [i for i in range(element_num)]


# 実際はこんな感じで実行されている
wrap_func = time_counter(make_list_no_decorator)
wrap_func()

Discussion