業務用スクリプトの依頼文に入力・出力・エラー処理のルールを入れる
「これを自動化して」を、実行可能な task description に変えます。sensitive data を含まない合成 sample と、手作業で確認した expected result を添えて、team ごとの work logs を集計する script request を完成させます。
手計算できる4行のデータと12個の unit tests を使って aggregation function を確認します。通常値だけでなく、空入力、0、小数、不正入力も検証します。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者AI が生成した Python コードが動作するという理由だけで業務に使うことに不安がある初学者
AI のコードが error なく終了したからといって、計算が正しいとは限りません。この記事では実際の AI service には接続せず、独立して作成した aggregation function と小さな合成データを使って検証手順を練習します。同じ方法を、確認したい function に適用できます。API key や外部 packages は不要です。
python example.py| item | quantity | unit_price | 行ごとの金額 |
|---|---|---|---|
| 노트 | 2 | 2500.00 | 5000.00 |
| 펜 | 3 | 1200.50 | 3601.50 |
| 노트 | 1 | 2500.00 | 2500.00 |
| Sample | 0 | 999.99 | 0.00 |
rows は4件ですが、total quantity は 2 + 3 + 1 + 0 = 6 です。total amount は 5000.00 + 3601.50 + 2500.00 = 11101.50 です。最後の quantity 0 の行も input rows の件数には含まれます。item ごとでは、노트 (notebook) は quantity 3、amount 7500.00、펜 (pen) は quantity 3、amount 3601.50、샘플 (sample) は quantity 0、amount 0.00 です。
したがって total だけを比較せず、row count、total quantity、per-item values を一緒に確認してください。notebook のように同じ item を2行に分けて入力すると、grouping と summing も確認できます。per-item results の順序は name で sort されており、sales rank や input order を意味しません。
| 項目 | 許可ルール | 拒否例 |
|---|---|---|
| Column | 3つ: item, quantity, unit_price | columns の欠落または追加 |
| item | 先頭と末尾の spaces を除去した後の空でない string | 空文字列または whitespace only |
| quantity | 0 から 1,000,000 までの integer string | -1, 1.5, 1e2 |
| unit_price | 0 から 1,000,000,000、小数点以下2桁まで | NaN, Infinity, 0.001, 1,000 |
amounts は同じ架空通貨を使用すると仮定します。negative quantities と unit prices は許可しないため、returns の表現ルールはありません。先に input rules を定義しておくと、コードが値を黙って 0 に変更したり、一部の rows を skip したりしていないか判断できます。この function は invalid row を見つけると ValueError で停止します。
aggregate_sales は input rows を変更せず、result だけを返します。file の読み込みと result の書き込みは main に分離しています。これにより tests では small dictionaries を直接渡し、calculation だけを確認できます。各 expected value は assertEqual で比較し、拒否されるべき values は assertRaises で確認します。
"""집계 함수를 손계산 기대값과 unittest로 검증합니다. 외부 AI 호출은 없습니다."""
import argparse
import csv
import json
import re
import sys
import unittest
from decimal import Decimal
from pathlib import Path
BASE = Path(__file__).resolve().parent
OUTPUT = BASE / "outputs"
FIELDS = {"item", "quantity", "unit_price"}
def money(cents):
return f"{cents // 100}.{cents % 100:02d}"
def aggregate_sales(rows):
"""문자열 딕셔너리 행을 검증하고 집계합니다. rows와 원본 파일을 변경하지 않습니다."""
totals = {}
row_count = total_quantity = total_cents = 0
for number, row in enumerate(rows, start=1):
if not isinstance(row, dict) or set(row) != FIELDS:
raise ValueError(f"{number}행: item,quantity,unit_price 세 열이 필요합니다.")
if any(not isinstance(value, str) for value in row.values()):
raise ValueError(f"{number}행: 모든 값은 문자열이어야 합니다.")
item, quantity_text, price_text = (row[key].strip() for key in ("item", "quantity", "unit_price"))
if not item:
raise ValueError(f"{number}행: 품목이 비어 있습니다.")
if not re.fullmatch(r"[0-9]{1,7}", quantity_text):
raise ValueError(f"{number}행: 수량은 0 이상의 정수여야 합니다.")
quantity = int(quantity_text)
if quantity > 1_000_000:
raise ValueError(f"{number}행: 수량은 1,000,000 이하여야 합니다.")
if not re.fullmatch(r"[0-9]{1,10}(?:\.[0-9]{1,2})?", price_text):
raise ValueError(f"{number}행: 단가는 0 이상, 소수점 둘째 자리까지 입력하세요.")
price = Decimal(price_text) # 문자열에서 직접 생성해 float 오차를 피합니다.
if price > Decimal("1000000000"):
raise ValueError(f"{number}행: 단가는 1,000,000,000 이하여야 합니다.")
cents = int(price * 100) * quantity
bucket = totals.setdefault(item, {"quantity": 0, "cents": 0})
bucket["quantity"] += quantity
bucket["cents"] += cents
total_quantity += quantity
total_cents += cents
row_count += 1
return {
"row_count": row_count,
"quantity": total_quantity,
"amount": money(total_cents),
"by_item": [
{"item": item, "quantity": totals[item]["quantity"], "amount": money(totals[item]["cents"])}
for item in sorted(totals)
],
}
def sale(item="펜", quantity="1", unit_price="1200.50"):
return {"item": item, "quantity": quantity, "unit_price": unit_price}
class TestAggregate(unittest.TestCase):
# 기대값은 검증 대상 함수로 만들지 않고 손계산한 상수로 적습니다.
def test_empty_input(self):
self.assertEqual(aggregate_sales([]), {"row_count": 0, "quantity": 0, "amount": "0.00", "by_item": []})
def test_single_row(self):
self.assertEqual(aggregate_sales([sale(quantity="3")])["amount"], "3601.50")
def test_zero_quantity(self):
result = aggregate_sales([sale(quantity="0")])
self.assertEqual((result["row_count"], result["quantity"], result["amount"]), (1, 0, "0.00"))
def test_repeated_item(self):
result = aggregate_sales([sale(quantity="2"), sale(quantity="1")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 3, "amount": "3601.50"}])
def test_decimal_addition(self):
result = aggregate_sales([sale(unit_price="0.10"), sale(unit_price="0.20")])
self.assertEqual(result["amount"], "0.30")
def test_whitespace(self):
result = aggregate_sales([sale(item=" 펜 ", quantity=" 1 ", unit_price=" 2.00 ")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 1, "amount": "2.00"}])
def test_bad_quantity(self):
for value in ("-1", "1.5", "", "1000001", "1e2", "1"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(quantity=value)])
def test_bad_price(self):
for value in ("-1", "0.001", "NaN", "Infinity", "", "1,000", "1000000001"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(unit_price=value)])
def test_blank_item(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(item=" ")])
def test_wrong_columns(self):
for row in ({"item": "펜"}, {**sale(), "extra": "x"}):
with self.subTest(row=row), self.assertRaises(ValueError):
aggregate_sales([row])
def test_non_string(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(quantity=True)])
def test_upper_boundary(self):
self.assertEqual(
aggregate_sales([sale(quantity="1000000", unit_price="1000000000")])["amount"],
"1000000000000000.00",
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--test", action="store_true", help="단위 테스트만 실행합니다.")
args = parser.parse_args()
# unittest의 명시적 비교는 python -O에서도 생략되지 않습니다.
result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(
unittest.defaultTestLoader.loadTestsFromTestCase(TestAggregate)
)
if not result.wasSuccessful():
return 1
if args.test:
return 0
if OUTPUT.exists() or OUTPUT.is_symlink():
raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
with (BASE / "input.csv").open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream, strict=True)
if reader.fieldnames != ["item", "quantity", "unit_price"]:
raise ValueError("input.csv의 열은 item,quantity,unit_price 순서여야 합니다.")
actual = aggregate_sales(reader)
expected = json.loads((BASE / "expected.json").read_text(encoding="utf-8"))
if actual != expected:
raise ValueError("예제 집계가 expected.json의 손계산 기대값과 다릅니다.")
OUTPUT.mkdir()
with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
json.dump(actual, stream, ensure_ascii=False, indent=2)
stream.write("\n")
with (OUTPUT / "verification.txt").open("x", encoding="utf-8") as stream:
stream.write(f"unittest: {result.testsRun}개 통과\n예제 기대값: 일치\n외부 AI API 호출: 없음\n")
print(f"완료: {actual['row_count']}행, 수량 {actual['quantity']}, 금액 {actual['amount']}")
print(OUTPUT)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, csv.Error) as error:
print(f"중지: {error}", file=sys.stderr)
raise SystemExit(2)
unit price は string から Decimal に変換し、100 を掛けて integer units にした後で合計します。結果の amount も小数点以下2桁の string として返します。これはこの例の two-decimal unit price rule に合う選択であり、すべての数値計算に共通する universal rule ではありません。
| カテゴリ | 組み込み tests | 確認内容 |
|---|---|---|
| Normal | 1 row, repeated item, leading and trailing spaces | 基本計算と grouping |
| Boundary | Empty input, quantity 0, upper limit | 小さい値と大きい値での定義 |
| Decimals | 0.10 + 0.20 = 0.30 | amount representation と addition |
| Error | Invalid quantity, unit price, blank item, columns, types | invalid values が黙って受け入れられないこと |
各 test name の横に ok が表示され、最後に ‘Ran 12 tests’ と ‘OK’ が出ることを確認します。さらに ‘완료: 4행, 수량 6, 금액 11101.50’ (Done: 4 rows, quantity 6, amount 11101.50) が表示された場合だけ、CSV expected-value comparison も合格しています。unit tests だけが合格して result が expected.json と異なる場合、default run は失敗し、新しい results は保存されません。
python example.py --test--test は unit tests だけを再実行し、outputs は作成しません。results folder がすでに存在していても使用できるため、function を編集しながら素早く確認するのに適しています。一方、default command は既存 outputs を上書きせず停止します。
この実験の目的は、tests が error を検出できることを確認することです。この例の作成時にも、同じ種類の error を temporary copy に意図的に入れ、tests が失敗し results が書き込まれないことを確認しました。意味のある failure を一度見ることで、test code は動いているのに何も確認していない状態を避けやすくなります。
新しいコードでは、まず input と output の例を固定します。original files に書き込む部分がある場合は copies に限定し、先に calculation を行う function を分離します。normal data、empty data、0、repeated items、許可されない values を1セットずつ用意し、expected values を自分で計算します。
input.csv を自分の data に置き換える場合は、expected.json も別途計算して更新する必要があります。input だけを変更した後、expected values と不一致だからといって code defect だと結論付けないでください。どの rules とどの data が変わったかを確認するところから reproducible verification が始まります。
| 結果 | 意味 | 次に確認すること |
|---|---|---|
| FAIL or FAILED | calculated result が test の expected value と異なる | failed test の name と2つの values を確認します。 |
| Stopped due to input rules | CSV value が allowed range の外にある | 示された row と original data を確認します。 |
| Differs from the hand-calculated expected value | CSV aggregation が expected.json と一致しない | data が変わったか、hand calculation が正しいかの両方を再確認します。 |
| outputs already exists | 以前の successful result が存在する | --test だけで checks を実行するか、results を別の場所に移して保持します。 |
この例で tests に合格することは、記載した rules に対する evidence です。実際の AI model の performance 測定でも、すべての accounting work の accuracy 保証でもありません。taxes, discounts, exchange rates, returns, multiple currencies, large data での performance には別の rules と tests が必要です。failure cases を増やすほど、適用可能な範囲をより明確に説明できます。
2026-09-19 · Windows 11 · CPython 3.12.14 · 追加 packages なし · distribution の temporary copy で実行
コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。
サンプルZIPをダウンロードサンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。
独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。