🖊

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

2025/01/09に公開

はじめに

Python実践レシピに沿って学習します。

以前の記事

今回の範囲

タイトル 問題数 問題割合 備考
8 日付と時刻の処理 2 5.0% 8.4 dateutilは除く
9 データ型とアルゴリズム 5 12.5% 9.3 bisectは除く
9.5 pprint は除く

日付と時刻の処理: 日付と時刻 - datetimeモジュール

覚えておくこと

  • dateオブジェクト
  • timeオブジェクト
  • datetimeオブジェクト
  • timedeltaオブジェクト

基本的な理解にはdatetimeオブジェクトとtimedeltaオブジェクトを覚えておけばOK。
以下を押さえておきましょう。

  • datetimeオブジェクトとして日時を持っておくことで、日時の計算処理が可能。
    • 文字列で持つと計算ができない
  • 日時情報の差をtimedeltaオブジェクトとして持つ。
  • csvに書き出すとかで文字列に変更したい場合は、strftime()を用いる
    • 文字列変換時のフォーマットが決まっているので覚えておく。

datetimeオブジェクト

datetime.datetimeは日付と時刻の両方を取り扱う。
このオブジェクトから日付を扱うdateオブジェクト、時刻を扱うtimeオブジェクトへの変換も可能であるため
まず、このオブジェクトを把握しておけばOK

from datetime import datetime

# 日時を扱うdatetime.datetime(=datetimeモジュールの中にあるdatetimeオブジェクト)
now_time = datetime.now()
today_time = datetime.today()
print(f"{now_time=}")
print(f"{today_time=}")
# -> now_time=datetime.datetime(2025, 1, 6, 21, 58, 38, 816664)
# -> today_time=datetime.datetime(2025, 1, 6, 21, 58, 38, 816665)

# datetime.datetime -> datetime.dateに変換
today_only = today_time.date()
# datetime.datetime -> datetime.timeに変換
today_time_only = today_time.time()
print(f"{today_only=}")
print(f"{today_time_only=}")
# -> today_only=datetime.date(2025, 1, 6)
# -> today_time_only=datetime.time(21, 58, 38, 816665)

timedeltaオブジェクト

日時計算するときに用いるオブジェクト
たとえば、ある時刻から+1分した時刻や、次の日が欲しいときなど。

from datetime import datetime, timedelta

now_time = datetime.now()
# 1分後の日時が欲しい
after_1min = now_time + timedelta(minutes=1)
# 1時間前の時刻が欲しい
before_1hour = now_time - timedelta(hours=1)
before_1hour_only_time = before_1hour.time()
# 次の日が欲しい
after_1day = now_time + timedelta(days=1)
after_1day_only_day = after_1day.date()

print(f"{now_time=}")
print(f"{after_1min=}")
print(f"{before_1hour=}")
print(f"{before_1hour_only_time=}")
print(f"{after_1day=}")
print(f"{after_1day_only_day=}")
# now_time=datetime.datetime(2025, 1, 6, 22, 9, 44, 751496)
# after_1min=datetime.datetime(2025, 1, 6, 22, 10, 44, 751496)
# before_1hour=datetime.datetime(2025, 1, 6, 21, 9, 44, 751496)
# before_1hour_only_time=datetime.time(21, 9, 44, 751496)
# after_1day=datetime.datetime(2025, 1, 7, 22, 9, 44, 751496)
# after_1day_only_day=datetime.date(2025, 1, 7)
from datetime import datetime

# オブジェクト同士の計算も可能
day = datetime(year=2024, month=12, day=24)
after_1week = datetime(year=2024, month=12, day=31)

# 計算結果はtimedeltaオブジェクトになる
day_num = after_1week - day
print(f"{day_num=}")
# day_num=datetime.timedelta(days=7)

strftime() でオブジェクトを文字列に変換する

date/time/datetimeオブジェクトにはstrftime()が実装されている。
これを使うことで文字列型に変換をすることが可能。

変換時にフォーマットが決まっており、これはそういう仕様であるので覚えておく。

from datetime import datetime


now_time = datetime.now()
# %Y : 西暦表示(yyyy)
# %m : 月表示(mm)
# %d : 日表示(dd)
# %Y/%m/%d = yyyy/mm/dd
str_date = now_time.strftime("%Y/%m/%d")
# %Y-%m-%d = yyyy-mm-dd
str_date2 = now_time.strftime("%Y-%m-%d")
print(f"{type(now_time)} : {now_time=}")
print(f"{type(str_date)} : {str_date=}")
print(f"{type(str_date2)} : {str_date2=}")

# %y : 二桁のみ(yy)
# %H : 時間(hh)
# %M : 分(mm)
# %S : 秒(ss)
# %f : マイクロ秒(ms)
str_datetime = now_time.strftime("%y/%m/%d %H:%M:%S.%f")
print(f"{type(str_datetime)} : {str_datetime=}")
# <class 'datetime.datetime'> : now_time=datetime.datetime(2025, 1, 6, 22, 18, 17, 986952)
# <class 'str'> : str_date='2025/01/06'
# <class 'str'> : str_date2='2025-01-06'
# <class 'str'> : str_datetime='25/01/06 22:18:17.986952'

逆に、文字列からdatetimeに変換するにはstrptimeを使用する

from datetime import datetime


str_time = "2024/12/24"
# 文字列の日時形式に合わせてformatも渡す
cast_time = datetime.strptime(str_time, "%Y/%m/%d")
print(f"{cast_time=}")
# cast_time=datetime.datetime(2024, 12, 24, 0, 0)

日付と時刻の処理: 時刻 - timeモジュール

覚えておくこと

timeモジュールでもdatetimeのように時刻を取得できます。
この辺はそういうものとして覚えておくくらいなので省略。書籍参照。

sleep()を持っているため、プログラムのスリープを実施できます。

from datetime import datetime
import time

print(datetime.now())
time.sleep(3)
print(datetime.now())
# 2025-01-06 22:41:54.337357
# 2025-01-06 22:41:57.338346

役に立つこと

プログラムの処理速度を見たいときはtime.perf_counter()をよく用います。

import time

start = time.perf_counter()
# 何らかの処理
time.sleep(2)

end = time.perf_counter()
print(f"time = {end-start:.3f}")
# time = 2.001

データ型とアルゴリズム: ソート

覚えておくこと

組み込み関数のsorted / reversed

  • sorted()は渡されたイテラブルオブジェクト(リストやタプルなど)を並び変える
    • 昇順/降順ともに可能
    • 並び変えた結果は新たなリストとして生成される
    • 元のオブジェクトはそのまま残る
  • reversed()は要素を逆順に並び変える
  • key引数を渡すことで任意の要素変換等をしながら並び変えることができる
    • sorted(["B", "C", "a"], key=str.lower)はリスト要素を小文字に変換しながら並び変える

リストのメソッドであるsort / reverse

  • リスト自体にもsort(), reverse()メソッドが存在する
    • ただし、これはリストそのものを書き換える

データ型とアルゴリズム: collections

覚えておくこと

  • Counterはリスト内の要素のカウントができます
    • 返り値は辞書型となり、要素名がkey、カウント数がvalueとなる
  • defaultdictは存在しないキーにアクセスしたときに指定の関数を用いて、デフォルト値を設定できます
  • OrderedDictはデータの挿入順序を保ちます
    • 古いPythonでは、辞書のkeyの並びが保証されていなかったため作られた
    • 最近のPythonでは順序が保たれるようになったため、現在用いる機会はあまりない。
  • namedtupleは、タプルの各要素に名前をつけることができます
    • たとえば、方向を表す数値(x, y, z) をタプルで作成するとします。

      通常のタプル

      direction = (150, 120, 10)
      print(f"x={direction[0]}")
      # x=150
      

      namedtuple:

      from collections import namedtuple
      
      Direction = namedtuple("Direction", "x, y, z")
      direction = Direction(150, 120, 10)
      print(f"x={direction.x}")
      # x=150
      
    • このようにすることで、タプル内の各要素は何を格納しているものであるのかハッキリし、可読性が向上します。

namedtupleの補足

typingモジュールにあるNamedtupleを用いて、クラスとして定義することもできます。

from typing import NamedTuple


class Direction(NamedTuple):
    x: int
    y: int
    z: int


direction = Direction(x=150, y=120, z=10)
print(f"{direction=}")
# direction=Direction(x=150, y=120, z=10)

データ型とアルゴリズム: enum

覚えておくこと

列挙型Enumでは定数を設定できます
上記で学んだNamedtupleも組み合わせて、サンプルコードを書いてみます。

from enum import Enum
from typing import NamedTuple


class RGBColor(NamedTuple):
    r: int
    g: int
    b: int


# プログラム内で用いる主要なRGBコードを定数としてまとめる
class ColorMap(Enum):
    RED = RGBColor(r=255, g=0, b=0)
    GREEN = RGBColor(r=0, g=255, b=0)
    BLUE = RGBColor(r=0, g=0, b=255)
    WHITE = RGBColor(r=255, g=255, b=255)
    BLACK = RGBColor(r=0, g=0, b=0)
    # 別に直接タプルで書けばいいじゃん!と思ったあなた。
    # はい、正直ここはNamedtupleにするまでもないかもしれません。
    # 結局この辺は状況によりけり、好みによりけりです。
    # やっておいたほうが可読性は高くなるが、コード量は増えていきます。


green = ColorMap.GREEN
print(green)
print(green.name)
print(green.value)
print(green.value.r)
print(green.value.g)
print(green.value.b)
# ColorMap.GREEN
# GREEN
# RGBColor(r=0, g=255, b=0)
# 0
# 255
# 0

データ型とアルゴリズム: copy

覚えておくこと

Pythonの変数代入の仕様について

# 購入する果物を定義する
buy_fruits = ["リンゴ", "バナナ", "ぶどう"]

# いったん別の変数に入れておく
copy_fruits = buy_fruits

# バナナをメロンに変更する
copy_fruits[1] = "メロン"

print(f"{copy_fruits=}")
print(f"{buy_fruits=}")
# copy_fruitsを変更したのに、buy_fruitsの結果も変わっている!
# copy_fruits=['リンゴ', 'メロン', 'ぶどう']
# buy_fruits=['リンゴ', 'メロン', 'ぶどう']

これはPythonの言語仕様によるもの。
Pythonの変数代入は、値を渡しているのではなく、オブジェクトへの参照を渡しています。
といっても分かりにくいので、以下はC言語のポインタについてですが、説明しやすいのでこれを参照して…
https://x.com/okonomiyonda/status/1539704488545492992

オブジェクトへの参照を渡しているというのは、オブジェクトのある場所を指し示している
ようなイメージ。
C言語だと、この指し示している場所情報をポインタと言います。
intで宣言した変数の場所(メモリアドレス)は、ポインタであるint*(左側にいるアーニャ) が指し示しているということになります。

Pythonでは、全てのオブジェクトにはオブジェクトIDが振られており、これがオブジェクトの場所を示しています。

これを踏まえて、copy_fruits = buy_fruitsの処理を見ると、これは指し示しているオブジェクトIDをcopy_fruitsに入れただけになります。
つまり、指し示している場所は同じです。(両方とも左側のアーニャ)

これを確認するために、id()を使って、オブジェクトIDを確認してみます。

print(id(copy_fruits))
print(id(buy_fruits))
# 2465931542976
# 2465931542976

どちらも同じになることが確認できました。

copy - シャローコピーとディープコピー

では、どのようにすれば元のリストの要素を変更せずに、コピー先だけ変えられるのか?
となりますが、copy()を用いることで可能です。
copy.copy()によるコピー方法を、シャローコピー(=浅いコピー)と言います。

import copy

# 購入する果物
buy_fruits = ["リンゴ", "バナナ", "ぶどう"]

# copy()によるシャローコピー
copy_fruits = copy.copy(buy_fruits)

# バナナをメロンに変更する
copy_fruits[1] = "メロン"

print(f"{copy_fruits=}")
print(f"{buy_fruits=}")

print(id(copy_fruits))
print(id(buy_fruits))
# copy_fruits=['リンゴ', 'メロン', 'ぶどう']
# buy_fruits=['リンゴ', 'バナナ', 'ぶどう']
# 2465931652800     <- 別のオブジェクトとしてcopy_fruitsが作られている
# 2465930770112

「浅い」コピーという名前の通り、これは一部のみが別オブジェクトとして作られます。
それを確認するために、以下のサンプルコードを見てみましょう

import copy

# 購入する果物(Aさん、Bさん、Cさん)
buy_fruits_by_person = [
    ["リンゴ", "バナナ", "ぶどう"],
    ["ぶどう"],
    ["メロン", "リンゴ"],
]

# copy()によるシャローコピー
copy_fruits = copy.copy(buy_fruits_by_person)

# Aさんが購入するバナナをメロンに変更する
copy_fruits[0][1] = "メロン"

print(f"{copy_fruits=}")
print(f"{buy_fruits_by_person=}")
# copy()を使ったのに要素が変わっている!
# copy_fruits=[['リンゴ', 'メロン', 'ぶどう'], ['ぶどう'], ['メロン', 'リンゴ']]
# buy_fruits_by_person=[['リンゴ', 'メロン', 'ぶどう'], ['ぶどう'], ['メロン', 'リンゴ']]

これは、1階層目のリストだけコピーされ、要素となっているリストのオブジェクトIDは変わらず同じ参照先になっているためです。

print(id(copy_fruits[0]), id(copy_fruits[1]), id(copy_fruits[2]))
print(id(buy_fruits_by_person[0]), id(buy_fruits_by_person[1]), id(buy_fruits_by_person[2]))
# 2465930482880 2465923320832 2465930241344
# 2465930482880 2465923320832 2465930241344

これに対応するため、「深い」コピー(=ディープコピー)と言われる方法があります。

import copy

# 購入する果物(Aさん、Bさん、Cさん)
buy_fruits_by_person = [
    ["リンゴ", "バナナ", "ぶどう"],
    ["ぶどう"],
    ["メロン", "リンゴ"],
]

# copy()によるディープコピー
copy_fruits = copy.deepcopy(buy_fruits_by_person)   # <- deepcopy()で実行

# Aさんが購入するバナナをメロンに変更する
copy_fruits[0][1] = "メロン"

print(f"{copy_fruits=}")
print(f"{buy_fruits_by_person=}")
# 要素が保たれている
# copy_fruits=[['リンゴ', 'メロン', 'ぶどう'], ['ぶどう'], ['メロン', 'リンゴ']]
# buy_fruits_by_person=[['リンゴ', 'バナナ', 'ぶどう'], ['ぶどう'], ['メロン', 'リンゴ']]

オブジェクトIDも確認しておきましょう。

print(id(copy_fruits[0]), id(copy_fruits[1]), id(copy_fruits[2]))
print(id(buy_fruits_by_person[0]), id(buy_fruits_by_person[1]), id(buy_fruits_by_person[2]))
# 2465930172160 2465930175616 2465930174208
# 2465924471424 2465930969728 2465930448192

要素のリストのオブジェクトIDも変更されていることが確認できました。

備考

ちなみにですが、deepcopyをしてもオブジェクトIDが変更されないパターンもあります。
最初のシャローコピーでのサンプルにdeepcopy()を適用してみます。

import copy

# 購入する果物
buy_fruits = ["リンゴ", "バナナ", "ぶどう"]

copy_fruits = copy.deepcopy(buy_fruits)

# バナナをメロンに変更する
copy_fruits[1] = "メロン"

print(id(copy_fruits[0]), id(copy_fruits[1]), id(copy_fruits[2]))
print(id(buy_fruits[0]), id(buy_fruits[1]), id(buy_fruits[2]))
# 2465923010912 2465930785248 2465930784368
# 2465923010912 2465930576320 2465930784368

この場合、各要素のIDは同値です。
これがどういう場合に起きるのか、いまいち把握しきれておらず…Pythonの言語仕様を掘り下げる必要があるのかなと思います。
(イテラブルオブジェクトが条件ならば、文字列もイテラブルなはずでは?)

Discussion