Python3エンジニア認定実践試験の勉強をするよ その3
はじめに
Python実践レシピに沿って学習します。
以前の記事
-
Python3エンジニア認定実践試験の勉強をするよ その1
Python環境、コーディング規約、型ヒント -
Python3エンジニア認定実践試験の勉強をするよ その2
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言語のポインタについてですが、説明しやすいのでこれを参照して…
オブジェクトへの参照を渡しているというのは、オブジェクトのある場所を指し示している
ようなイメージ。
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