0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonのdatetimeで時間要素を動的に扱う

Last updated at Posted at 2021-12-05

はじめに

Flaskのサーバーを立てて何でもかんでもPythonでおこなうのでなく、PythonとJavaScriptを併用したプロジェクトを作っている。
JavaScriptで自由に時間を扱おうとして回り道した話をこちらに書いた。さっそくコメントをいただきありがたいことである。プロジェクトが終わったらこれについての追加の勉強をしよう。

本記事はPython側の知見である。

【追記】

いただいたコメントをもとにいくつかの知見を追加。
shiracamusさんどうもありがとうございます。

Pythonで動的に関数を指定する

これは以前、この記事を書く途中で知った。eval()を使うのだ。
前回の記事との対比で一応書いておくが、実際は使わない。

eval()

eval(expression, globals=None, locals=None)
文字列expressionをPythonの式として解釈する。globalslocalsについては割愛。より詳しい記事はこちら

eval()
hensu = "1+1"                               # 文字列
print (eval(hensu))                         # 基本的なかたち 文字列を式として解釈する
# 結果 2


def funcA(x=0):
    print ("Aが実行された:引数は", x)

def funcB(x=0):
    print ("Bが実行された:引数は", x)

func_name1 = "funcA"                        # 関数名の文字列
eval(func_name1)(1)                         # 関数名を指定して関数を実行する カッコが必要
# 結果 Aが実行された:引数は 1

func_name2 = "funcB(2)"                     # カッコで引数まで付いた関数の文字列
eval(func_name2)                            # こっちはカッコはいらない
# 結果 Bが実行された:引数は 2

datetimeの時間要素を動的に扱う

JavaScriptでは Moment.js や Day.js を知らなかったこともあり動的に関数を指定するだけで一本の記事としてしまったが、本来の目的は時間要素を動的に扱うことだった。
PythonのdatetimeオブジェクトはJavaScriptのDateオブジェクトよりは高性能なので、複数の関数を用意してそれをeval()で呼び分けることなく目的を達成できそうだ。
JavaScriptの Moment.js や Day.js に相当する、Pythonでdatetimeをさらに便利に扱うことができるようになるライブラリは探してみたが見つけることができなかった。

【旧】任意の単位の時間要素を取得する(この段、不適切)

datetimeから「時」や「分」といった要素を自由に得られるようにする。
datetimeでは時を取得するのはdatetime.hour、分はdatetime.minute…となっておりJavaScriptのDateオブジェクトと同様やや使いづらいが、datetime.strftime()で自由にフォーマットを定めることができるのでこれを活用した。もっとエレガントな方法があったら教えてください。

get_time_element()
import datetime

def get_time_element(dt, unit):
    return int(dt.strftime(unit))

dt = datetime.datetime.now()                # 現在日時 例 2021-12-03 12:34:56.123456
unit = "%M"                                 # 取得する単位 datetime.strftime()のフォーマット
value = get_time_element(dt, unit)          # 時間要素を取得する
print (value)                               # 結果 34

【追記】オブジェクトから特定要素を取得する

datetime.datetime

datetimeはdatetime.datetime.now()という使い方しかしたことがないが、datetimeオブジェクトは具体的には次のようになっている(一部省略)。
datetime.datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None)

多くの解説サイトで

datetime
import datetime

dt = datetime.datetime (2021, 12, 31, 12, 34, 56, 123456)

と紹介されているアレだ。こんな定義の仕方しねーよとしか思わないが、使う使わないは別としてdatetimeオブジェクトはこのようなかたちになっている。
そして、このdatetimeオブジェクトから各時間要素を取り出すのは、datetimeモジュールの特別な関数ではなくPythonの標準的な関数で可能だった。

オブジェクトの属性の値を取得する getattr()

getattr(object, name[, default])

objectの指名された属性nameの値を返す。指名された属性が存在しない場合defaultが与えられていればそれを返し無ければエラーとなる。

つまり、私が目指していたものは

getattr()
import datetime

def get_time_element(dt, unit):
    return getattr(dt, unit)

dt = datetime.datetime.now()                # 現在日時 例 2021-12-03 12:34:56.123456
unit = "minute"                             # 取得する単位 datetimeの属性の名前
value = get_time_element(dt, unit)          # 時間要素を取得する
print (value)                               # 結果 34

という、個別の関数として外に出すまでもないものだったのだ。これは恥ずかしいー!

さて、多くの解説サイトでは自作のクラスを使ってgetattr()を説明しているので、ここではdatetimeと同様、既存のオブジェクトを触ってみよう。
こちらの記事に書いたように、PILのImageオブジェクトはさまざまな情報を持っている。ここで**「時と場合によって幅と高さのどちらかを取得したいんじゃ、両方取得して片方は使わないなんて勿体ない真似は出来んのじゃ」**というコードを書く必要がある場合はgetattr()が役に立つ。

PIL
from PIL import Image

filename = "hoge.png"
imgPIL = Image.open(filename)
# imgPIL.show()

print (imgPIL.width)                # 普通の取得方法

print (getattr(imgPIL, "width"))    # getattr()で取得

attr_name = "width"
print (getattr(imgPIL, attr_name))  # getattr()ならば属性名を変数にできる

任意の単位の時間要素を変更する

時間要素を変更するにはdatetime.replace()を使う。この関数は具体的には**datetime.replace(year=self.year, month=self.month, day=self.day, 以下略)**となっており各引数には元の時間要素がデフォルト値として入っている。だから変更したい要素のみ指定すればよい。
今回は、関数の引数だけでなくそのキーワードも動的に変更すればよいわけだ。
いろいろググって試していたら偶然できてしまった。単位の表記が要素取得時と異なるのがイマイチだが、先に結果を書くとこうなる。

set_time_element()修正済
import datetime

def set_time_element(dt, unit, value):
    return  dt.replace(**{unit: value})

dt = datetime.datetime.now()                # 現在日時 例 2021-12-03 12:34:56.123456
value = 0                                   # 変更する値
unit = "minute"                             # 変更する単位 datetime.replace()のキーワード
dt1 = set_time_element(dt, unit, value)     # 時間要素を変更する
print (dt1)                                 # 結果     2021-12-03 12:00:56.123456

もちろん偶然できたで終わりにしてはいけない。どのような挙動になっているのか詳しく調べねば。

**kwargs

関数の引数に**kwargsを指定すると可変長のキーワード引数を辞書として受け取る/渡すことができる。kwargsは慣例的な変数名でこうでなくてはならないわけではない。重要なのは**だ。
datetime.replace(**kwargs)は以下のfunc2()に相当する使い方だ。
受け取る使い方は多くの解説サイトで見ることができたが渡す使い方の解説はなかなか見つけることができなかった。渡す使い方ではkwargsという用語を用いないため検索しづらいのだ。

**kwargs
# 可変数の辞書を受け取る例
def func1(**kwargs):
    print (kwargs)

func1()                         # 結果 {}
func1(hoge=1)                   # 結果 {'hoge': 1}
func1(fuga=2, piyo=3)           # 結果 {'fuga': 2, 'piyo': 3}


# 可変数のキーワード引数を**辞書で渡す例
def func2(hoge=0, fuga=0):      # デフォ値 0 0
    print (hoge, fuga)

func2(fuga=1)                   # 普通の使い方 結果 0 1
func2({"fuga": 1})              # 失敗 {'fuga': 1} 0 最初の値が変更され2番目以降はデフォ値のまま
                                #                     関数にデフォ値が設定されてなかったらエラー
func2(**{"fuga": 1})            # 成功 0 1 辞書のかたちでキーワード引数を変更するにはこうする

key, value = "fuga", 1
func2(**{key: value})           # 成功 0 1 この記載方法ならキーワードを変数で指定できる
# func2(key=value)              # 失敗 'key'というキーワードはないのでエラーになる

*args

今回は使っていないが、ついでに。
関数の引数に*argsを指定すると可変数の値をタプルとして受け取る/渡すことができる。argsは慣例的な変数名でこうでなくてはならないわけではない。重要なのは*だ。
*args**kwargsは引数付きのPythonスクリプトをコマンドラインから実行させるときによく使う。

*args
# 可変数の値を受け取る例
def func1(*args):
    print (args)

func1()                         # 結果 ()
func1(1)                        # 結果 (1,)
func1(2, 3)                     # 結果 (2, 3)


# 可変数の値を*タプルで渡す例
def func2(hoge=0, fuga=0):      # デフォ値 0 0
    print (hoge, fuga)

func2(hoge=1, fuga=2)           # 普通の使い方 結果 1 2
func2(1, 2)                     # 普通の使い方 結果 1 2
func2((1, 2))                   # 失敗 (1, 2) 0 最初の値が変更され2番目以降はデフォ値のまま
                                #                関数にデフォ値が設定されてなかったらエラー
func2(*(1, 2))                  # 成功 1 2

func2(*(1,))                    # 結果 1 0 数が足りないときは残りはデフォ値になる
                                #           関数にデフォ値が設定されてなかったらエラー

【追記】オブジェクトの属性の値を変更する setattr()

setattr(object, name, value)
getattr()の相方。…って、公式の説明がそれでいいのかよ!

datetimeは便利な関数なので(先日使いづらいって書いたくせに…)datetime.replace()という関数を用意してくれているが、それがなくてもPythonが最初から持っている関数で変更できるというわけですね。

seteattr()
import datetime

def set_time_element2(dt, unit, value):
    setattr(dt, unit, value)
    return dt

dt = datetime.datetime.now()                # 現在日時 例 2021-12-03 12:34:56.123456
value = 0                                   # 変更する値
unit = "minute"                             # 変更する単位 datetime.replace()のキーワード
dt2 = set_time_element2(dt, unit, value)    # 時間要素を変更する
print (dt2)                                 # 結果??
結果
AttributeError: attribute 'minute' of 'datetime.datetime' objects is not writable

…ダメでした。素直にdatetime.replace()を使えということですね。

これらを使いやすくする関数

分を扱うのにget_time_element()では%Mを指定しset_time_element()ではminuteを指定するというのは使い勝手が悪い。
人間様にとってわかりやすいminuteをキー、%Mをその値とする辞書を作る。それでget/setと単位minuteを指定すると適切な処理をしてくれる関数を作れば使いやすくなるんじゃないかな。
私は作らないけど。

終わりに

ものすごく中途半端だが、今回の目的は便利な関数を作ることではないのでこれで良しとする。

限定された機能でもかまわないから、とにかく手掛けているプロジェクトを最後まで完成させよう。

0
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?