Help us understand the problem. What is going on with this article?

AWS Lambda書いてて、Python引数のデフォルト値で少しハマった話

経緯

AWS LambdaのPythonコード内で、指定した日付(指定なければ1年前)を元に結果を返す関数を作った。
※下のコードは単に値を返しているだけだが、実際はboto3のリクエスト投げて結果を返している。

<コード>

from datetime import datetime
from dateutil.relativedelta import relativedelta

one_year_ago = (datetime.now() - relativedelta(years=1)).isoformat()[:-3] + "Z"

def lambda_handler(event, context):

    # 問題の関数
    def test_func(last_time: str = one_year_ago) -> str:
        return "last_time: " + last_time

    print(one_year_ago)

    # 関数呼んでるところ
    print(test_func("2019-09-20T06:28:26.000Z"))
    print(test_func())

    return 0


lambda_handler("","")

<実行結果>

2018-12-08T20:22:57.183Z
last_time: 2019-09-20T06:28:26.000Z
last_time: 2018-12-08T20:22:57.183Z

AWS Lambdaのベストプラクティスに沿うと、ロジックを分離した方が良いとの事だったので、以下のように修正した。

from datetime import datetime
from dateutil.relativedelta import relativedelta

one_year_ago: str = ""

def lambda_handler(event, context):
    global one_year_ago
    one_year_ago = (datetime.now() - relativedelta(years=1)).isoformat()[:-3] + "Z"
    print(one_year_ago)

    # 関数呼んでるところ
    print(test_func("2019-09-20T06:28:26.000Z"))
    print(test_func())
    return 0

# 問題の関数
def test_func(last_time: str = one_year_ago) -> str:
    return "last_time: " + last_time


lambda_handler("","")
  • Lambdaハンドラー部以外はコールドスタート時しか呼ばれない1ので、1年前を求めるのはハンドラー内。
    ※なので最初のコード自体も正しく動作しない。
  • あらかじめ空文字で変数定義しておく(定義しておかないと「not defined」になってしまう)。

何が起きたか

日付を指定すると動作するが、指定しないとエラーになった。デフォルト値が無効になっているように見える。

<実行結果>

2018-12-08T20:15:21.611Z
last_time: 2019-09-20T06:28:26.000Z
last_time: 

※実際のコードはboto3のリクエスト投げていて、InvalidParameterValueException2というエラーが出力した。

結論

デフォルト引数は、初回のみ評価されるので、初回の空文字がセットされてしまった。
https://note.nkmk.me/python-argument-default/

かっこ悪い気がするけど、とりあえず下記のようにして回避した。
<コード>

from datetime import datetime
from dateutil.relativedelta import relativedelta

one_year_ago: str = ""

def lambda_handler(event, context):
    global one_year_ago
    one_year_ago = (datetime.now() - relativedelta(years=1)).isoformat()[:-3] + "Z"
    print(one_year_ago)

    # 関数呼んでるところ
    print(test_func("2019-09-20T06:28:26.000Z"))
    print(test_func())
    return 0

# 問題の関数
def test_func(last_time: str = "one_year_ago") -> str:
    if last_time == "one_year_ago":
        last_time = one_year_ago
    return "last_time: " + last_time


lambda_handler("","")

<実行結果>

2018-12-08T20:21:13.942Z
last_time: 2019-09-20T06:28:26.000Z
last_time: 2018-12-08T20:21:13.942Z

余談

  • もっと綺麗な書き方がありそうな気がする。
  • 1年前を求めるところも、無理やり感がある。。
  one_year_ago = (datetime.now() - relativedelta(years=1)).isoformat()[:-3] + "Z"
  def cases_get(last_time: str = "one_year_ago") -> list:
    if last_time == "one_year_ago":
        last_time = one_year_ago
    cases_detail: list = []
    for lang in ("ja", "en"):
        for page in cases_paginator.paginate(
            includeResolvedCases=True,
            maxResults=100,
            language=lang,
            includeCommunications=False,
            afterTime=last_time,
        ):
            for cases in page["cases"]:
                cases["displayId"] = int(cases["displayId"])
                cases["yearIndex"] = int(cases["timeCreated"][:4])
                cases_detail.append(cases)

    return cases_detail

  1. https://dev.classmethod.jp/cloud/aws/lambda-outside-handler-running-first/ 

  2. こんなエラー botocore.exceptions.ClientError: An error occurred (InvalidParameterValueException) when calling the DescribeCases operation: '' is not a valid value for afterTime. Times must be valid ISO8601 strings. 

sakojun
クラウドエンジニア。 「AWS DevOps Engineer Professional」保有。
https://sakojun.work/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away