実践ChatGPT：本番環境で利用できるコード生成の基本

本番環境で利用できるコードをChatGPTで利用する場合の例です。
本番環境で利用するコードはただ動くだけではなく、①再利用できて、②エラー処理もあり、③コメントもされていて、④ユニットテストもされて、⑤ドキュメントされていることが最低条件になります。ChatGPTを上手に使えば、この条件を満たすコードを生成させ、コーディング作業の効率を上げることが可能になります。

ChatGPTが生成できるコードの長さには制限がある。長いコードを生成する場合は関数に分割するか、回答を分けるように指示する。推奨は短い関数に分割することである。その方がユニットテストも生成して、テストの実行もできる。

ChatGPTでコードを生成する場合は次を行う。 
1. 機能を分割して、短いコードを生成するようにする
1. 質問をテンプレート化して、テンプレートを改善する
1. 生成するコードの言語、フレームワーク、アルゴリズムを具体的に指定する
1. 言語毎及びフレームワーク毎にテンプレートを作成する
1. モジュールを利用する場合は、具体的に利用するモジュールや関数を制限する
1. 入力を明示的に指定する。例えば引数の日付配列、日付形式、日付配列のタイムゾーン、ソート順
1. 例を提供する。例えば「['2023-05-07','2022-05-07'.'2024-05-07'」をソートする。
1. 条件を明示する。例えば「引数で指定された日付形式のPython配列を降順にソートする関数を生成してください。」
1. エラー処理も指定する。引数が正しくない場合はNoneを返す
1. 出力に含むものを指定する。例えば、コードにコメントを含める
1. 回答を実際に実行して正しいか確認する。時々、エラーになる場合もある
1. 生成されたコードを「エレガント」（優雅）にする
1. ユニットテストを生成する
1. ドキュメントを生成する

注意点：生成されたコードが数回間違えがある場合は最初からやり直す。継続して質問すると同じ回答をループする傾向がある。

## コード生成の生成
テンプレートを作成する例です。
### 1. 単純に機能を説明する。本例ではpythonでソートする関数を作成する
プロンプト：python配列をソートする関数を生成してください。
```
def sort_array(arr):
    sorted_arr = sorted(arr)
    return sorted_arr
```
正しいコードですが、実際のプログラムではソートするオブジェクト型によりソートに仕方が異なるために詳細条件を指定するようにします。

### 2. ソートする内容を具体的に指定する。本例では日付をソートする。
プロンプト：yyyy-mm-dd hh:MM:ss形式の日付型python配列を降順にソートする関数を生成してください。
```
from datetime import datetime

def sort_dates_descending(dates):
    # convert dates to datetime objects
    datetime_dates = [datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') for date_str in dates]
    # sort datetime objects in descending order
    sorted_dates = sorted(datetime_dates, reverse=True)
    # convert datetime objects back to date strings
    sorted_date_strings = [datetime.strftime(date_obj, '%Y-%m-%d %H:%M:%S') for date_obj in sorted_dates]
    return sorted_date_strings
```
上記のコードも正しいコードですが、異なる形式の日付もソートできるように日付形式も引数で渡すようにします。

### 3. 入力を具体的に指定する。本例では日付配列と日付形式を指定する。
プロンプト：引数で指定された日付形式のpython配列を降順にソートする関数を生成してください。
```
from datetime import datetime

def sort_dates(dates, date_format):
    sorted_dates = sorted(dates, key=lambda x: datetime.strptime(x, date_format), reverse=True)
    return sorted_dates
```
正常系処理としては正しいのですが、実際のプログラムではエラー処理も含める必要があるため、エラー処理も指定します。

### 4. エラー処理を指定する。
プロンプト：引数で指定された日付形式のpython配列をソートする関数を生成してください。引数は日付配列、日付形式、引数日付のタイムゾーン、ソート順です。引数が正しいかのチェックも含めてください。エラーの場合はNoneを返します。正しい場合はローカルタイムゾーンに変換した日付配列を返します。
```
from datetime import datetime
import pytz

def sort_dates(dates, date_format, timezone, sort_order):
    # 引数が正しいかどうかをチェック
    if not dates or not date_format or not timezone or not sort_order:
        return None

    # タイムゾーンを設定
    try:
        tz = pytz.timezone(timezone)
    except pytz.exceptions.UnknownTimeZoneError:
        return None

    # 日付をパースしてローカルタイムゾーンに変換
    local_dates = []
    for date in dates:
        try:
            parsed_date = datetime.strptime(date, date_format)
            local_date = tz.localize(parsed_date)
            local_dates.append(local_date)
        except ValueError:
            return None

    # ソート
    if sort_order.lower() == "ascending":
        sorted_dates = sorted(local_dates)
    elif sort_order.lower() == "descending":
        sorted_dates = sorted(local_dates, reverse=True)
    else:
        return None

    return sorted_dates

```
comprehensionを利用してpython風に書き換えます。また関数、引数、コード内のコメントも記述するようにします。

### 5. 利用する機能を明示的に指定する。また出力されたい内容を明示的に指定する。
本例ではPython comprehensionを利用するようにし、出力にコメントを含むようにする。
プロンプト：引数で指定された日時形式のpython配列をソートする関数を生成してください。引数は日付配列、日付形式、引数日付のタイムゾーン、ソート順です。ループする場合はpythonのcomprehensionを利用してください。引数が正しいかのチェックも含めてください。エラーの場合はNoneを返します。正しい場合はローカルタイムゾーンに変換した日付配列を返します。関数と引数のコメントとコードにコメントを含めてください。

```
from datetime import datetime
from typing import List, Union
import pytz


def sort_dates(dates: List[str], date_format: str, timezone: str, sort_order: str) -> Union[None, List[datetime]]:
    """
    指定された日時形式のPython配列をソートする関数

    :param dates: 日付配列 (list[str])
    :param date_format: 日付形式 (str)
    :param timezone: 引数日付のタイムゾーン (str)
    :param sort_order: ソート順 (str)
    :return: ローカルタイムゾーンに変換した日付配列 (list[datetime]) もしくは None (引数が正しくない場合)
    """

    # 引数チェック
    if not isinstance(dates, list) or not all(isinstance(date, str) for date in dates):
        return None

    try:
        tz = pytz.timezone(timezone)
        parsed_dates = [datetime.strptime(date, date_format) for date in dates]
        timezone_aware_dates = [tz.localize(date) for date in parsed_dates]
    except (ValueError, pytz.UnknownTimeZoneError):
        return None

    # ソート順に応じたソート
    if sort_order == "asc":
        sorted_dates = sorted(timezone_aware_dates)
    elif sort_order == "desc":
        sorted_dates = sorted(timezone_aware_dates, reverse=True)
    else:
        return None

    # ローカルタイムゾーンに変換して返す
    return [date.astimezone(pytz.timezone('local')) for date in sorted_dates]

```
pytzはpython2で使われているモジュールです。python3ではdatetimeモジュールが推奨されているためpython3で使われるモジュールに制限します。

### 6. 利用するモジュールを制限する。本例ではPython3標準モジュールを使うようにする。
プロンプト：引数で指定された日時形式のpython配列をソートするpython3関数を生成してください。引数は日付配列、日付形式、引数日付のタイムゾーン、ソート順です。ループする場合はpythonのcomprehensionを利用してください。なるべくpython3標準モジュールを利用してください。引数が正しいかのチェックも含めてください。エラーの場合はNoneを返します。正しい場合はローカルタイムゾーンに変換した日付配列を返します。関数と引数のコメントとコードにコメントを含めてください。
```
import datetime

def sort_dates(dates, date_format, timezone, sort_order):
    """
    引数で指定された日時形式のPython配列をソートするPython3関数。
    
    :param dates: ソートする日付の配列
    :type dates: list[str]
    :param date_format: 日付のフォーマット
    :type date_format: str
    :param timezone: 引数日付のタイムゾーン
    :type timezone: str
    :param sort_order: ソート順。'asc' (昇順)または'desc' (降順)
    :type sort_order: str
    :return: ソートされた日付の配列をローカルタイムゾーンに変換したもの
    :rtype: list[datetime.datetime] or None
    """
    # 引数のチェック
    if not isinstance(dates, list) or not all(isinstance(date, str) for date in dates):
        return None
    if sort_order not in ['asc', 'desc']:
        return None
    try:
        tz = datetime.timezone(datetime.timedelta(hours=int(timezone)))
    except:
        return None
    
    # 日付をdatetimeオブジェクトに変換して、タイムゾーンを設定する
    dates = [datetime.datetime.strptime(date, date_format).replace(tzinfo=tz) for date in dates]
    
    # 日付をソートする
    dates = sorted(dates, reverse=(sort_order=='desc'))
    
    # 日付をローカルタイムゾーンに変換する
    local_tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
    dates = [date.astimezone(local_tz) for date in dates]
    
    return dates
```
上記コードでも正しく動作しましが、綺麗に短くするように指示します。

### 7. コードをもっと短くする
プロンプト：コードをもっとエレガントにしてください。
```
import datetime

def sort_dates(dates, date_format, timezone, sort_order):
    """
    引数で指定された日時形式のPython配列をソートするPython3関数。

    :param dates: ソートする日付の配列
    :type dates: list[str]
    :param date_format: 日付のフォーマット
    :type date_format: str
    :param timezone: 引数日付のタイムゾーン
    :type timezone: str
    :param sort_order: ソート順。'asc' (昇順)または'desc' (降順)
    :type sort_order: str
    :return: ソートされた日付の配列をローカルタイムゾーンに変換したもの
    :rtype: list[datetime.datetime] or None
    """
    try:
        tz = datetime.timezone(datetime.timedelta(hours=int(timezone)))
    except ValueError:
        return None
    except TypeError:
        return None
    
    try:
        sorted_dates = sorted([datetime.datetime.strptime(date, date_format).replace(tzinfo=tz) for date in dates],
                              reverse=(sort_order == 'desc'))
    except ValueError:
        return None
    
    return [date.astimezone() for date in sorted_dates]

```
実際に生成されたコードが動くかの確認するために関数を呼び出す例も生成します。

### 8. 関数ろ呼び出す例を作成する
プロンプト：sort_dates関数を呼び出す例を生成してください。
```
# 例: sort_dates()関数を呼び出す
dates = ['2022-04-30 14:30:00', '2022-04-29 10:45:00', '2022-05-01 08:15:00']
date_format = '%Y-%m-%d %H:%M:%S'
timezone = '+9'
sort_order = 'asc'

sorted_dates = sort_dates(dates, date_format, timezone, sort_order)

if sorted_dates is not None:
    for date in sorted_dates:
        print(date.strftime(date_format))
else:
    print("Invalid arguments.")

```

項目6と項目7をPythonを抽象化して基本テンプレートの一つとします。類似した機能の開発する場合はこのテンプレートを利用します。flaskやデータベース処理用にも類似した手順でテンプレートを作成します。テンプレートを作成することがChatGPTを学び始めた人でも本番環境で利用できるコードを生成できるようにします。

## テストケースを作成する
エラー処理も含めて正しく動作するか確認するためにテストケースも生成します。
1. プロンプト：unittestを作成してください。
```
import unittest
from datetime import datetime
from typing import List
from sort_dates import sort_dates


class TestSortDates(unittest.TestCase):

    def test_valid_arguments(self):
        # テストパラメータ
        dates = ['2022-04-30 14:30:00', '2022-04-29 10:45:00', '2022-05-01 08:15:00']
        date_format = '%Y-%m-%d %H:%M:%S'
        timezone = '+9'
        sort_order = 'asc'

        # 期待される結果
        expected_result = [
            datetime.strptime('2022-04-29 10:45:00', date_format),
            datetime.strptime('2022-04-30 14:30:00', date_format),
            datetime.strptime('2022-05-01 08:15:00', date_format)
        ]

        # テスト実行
        result = sort_dates(dates, date_format, timezone, sort_order)

        # 結果を検証する
        self.assertEqual(result, expected_result)

    def test_invalid_date_format(self):
        # テストパラメータ
        dates = ['2022-04-30 14:30:00', '2022-04-29 10:45:00', '2022-05-01 08:15:00']
        date_format = '%Y-%m-%d %H:%M:%'
        timezone = '+9'
        sort_order = 'asc'

        # 期待される結果
        expected_result = None

        # テスト実行
        result = sort_dates(dates, date_format, timezone, sort_order)

        # 結果を検証する
        self.assertEqual(result, expected_result)

    def test_invalid_timezone(self):
        # テストパラメータ
        dates = ['2022-04-30 14:30:00', '2022-04-29 10:45:00', '2022-05-01 08:15:00']
        date_format = '%Y-%m-%d %H:%M:%S'
        timezone = '+30'
        sort_order = 'asc'

        # 期待される結果
        expected_result = None

        # テスト実行
        result = sort_dates(dates, date_format, timezone, sort_order)

        # 結果を検証する
        self.assertEqual(result, expected_result)

    def test_invalid_sort_order(self):
        # テストパラメータ
        dates = ['2022-04-30 14:30:00', '2022-04-29 10:45:00', '2022-05-01 08:15:00']
        date_format = '%Y-%m-%d %H:%M:%S'
        timezone = '+9'
        sort_order = 'invalid'

        # 期待される結果
        expected_result = None

        # テスト実行
        result = sort_dates(dates, date_format, timezone, sort_order)

        # 結果を検証する
        self.assertEqual(result, expected_result)

    def test_empty_dates(self):
```
途中に終了しているために続きを表示するように問いかける
プロンプト：続きを表示してください。
```
def test_empty_dates(self):
    # テストパラメータ
    dates = []
    date_format = '%Y-%m-%d %H:%M:%S'
    timezone = '+9'
    sort_order = 'asc'

    # 期待される結果
    expected_result = []

    # テスト実行
    result = sort_dates(dates, date_format, timezone, sort_order)

    # 結果を検証する
    self.assertEqual(result, expected_result)

def test_none_argument(self):
    # テストパラメータ
    dates = None
    date_format = '%Y-%m-%d %H:%M:%S'
    timezone = '+9'
    sort_order = 'asc'

    # 期待される結果
    expected_result = None

    # テスト実行
    result = sort_dates(dates, date_format, timezone, sort_order)

    # 結果を検証する
    self.assertEqual(result, expected_result)

def test_invalid_date(self):
    # テストパラメータ
    dates = ['2022-04-30 14:30:00', 'invalid date', '2022-05-01 08:15:00']
    date_format = '%Y-%m-%d %H:%M:%S'
    timezone = '+9'
    sort_order = 'asc'

    # 期待される結果
    expected_result = None

    # テスト実行
    result = sort_dates(dates, date_format, timezone, sort_order)

    # 結果を検証する
    self.assertEqual(result, expected_result)
```

## ドキュメントを生成する
最後に、ドキュメントも生成します。
プロンプト：ドキュメントを作成してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698027/75a39bfc-ca09-4b42-7d6e-b23654127511.png)

# 最後に
PowerShell, Java, bashスクリプトでも同様に本番環境で利用できるコードを生成できます。関数の分け方（モデリング）、アルゴリズムの選択、ソフトウエアアーキテクチャ、システムアーキテクチャについてどこまでChatGPTを利用することが可能なのかは調査中です。
ChatGPTにコーディングさせることにより、IT技術者にはより高度な技術力が求められるようになると思います。

以上