入力・出力・状態|論理・アルゴリズム・情報の基礎
このページはプロモーション(広告)を含みます。
論理・アルゴリズム・情報の基礎(初級・読む25分+演習35分)
この授業のゴール
- 手順を「入力・処理・出力」に分け、各部分の型(整数・文字列・リストなど)を言える
- 状態(呼び出しをまたいで残る値)と副作用(戻り値以外の変化)が、同じ入力でも結果を変える例を示せる
- 引数を書き換える関数と、書き換えずに新しい値を返す関数の違いを、実行結果で確かめられる
- 0と欠損(None)を区別し、集計結果にどう影響するかを数値で説明できる
仕事でどう使うか
「同じデータを入れたのに、昨日と今日で結果が違う」という問い合わせは、実務で最も多いものの1つです。原因の多くは、手順が入力以外の何か(前回の実行で残った値、書き換えられたデータ)に影響されていることです。入力・出力・状態を区別できると、再現できない不具合を整理できます。
| 路線 | この授業の使いどころ |
|---|---|
| DS(データサイエンティスト) | ノートブックを上から順に実行し直したら結果が変わる、という問題を、前のセルが残した状態として説明する。欠損と0の扱いで平均が変わる |
| ML(機械学習エンジニア) | 前処理が元のデータを書き換えていないか確認する。学習用と評価用で、同じ処理を同じ状態で適用する |
| DX(DX推進・AI企画) | 業務システムの状態(受付→承認→完了)と、状態を変える出来事(イベント)を表にして要件定義に書く |
| RS(研究者) | 解析手順の入力・出力・乱数の状態を記録し、他の人が同じ結果を再現できるようにする |
この授業のゴールは、手順を見たときに、「この結果は入力だけで決まるのか、それとも状態に左右されるのか」を問えるようになることです。
前提
次の言葉と操作を使います。不安があれば先に戻ってください。
- 変数と代入、型(
int、str、list、bool)→ 変数と代入、数値・文字列・真偽値 - 手順・分岐・繰り返しと追跡表 → 手順・分岐・繰り返し
- 欠損(値がない・不明)と0は別物であること → 数値・カテゴリ・日時・欠損
- 関数の書き方(
def、引数、return)は、この授業で使いながら説明します。詳しくは 「関数と引数」(有料版) と 「戻り値と副作用」(有料版) で学びます。
コードは、画面内の 実行 ボタンで動かせます。実行前に結果を予想してください。
身近な状況:同じ3,000円の買い物なのに、2回目は500円高い
ネットショップが、「500円引きクーポン」を1人1回だけ使えるようにしました。担当者が次のプログラムを書きました。
used_coupon = False # クーポンを使ったか(この値は、関数の外に残る)
def price_with_coupon(price):
global used_coupon # 関数の外の変数 used_coupon を書き換える宣言
if not used_coupon:
used_coupon = True
return price - 500
return price
print(price_with_coupon(3000))
print(price_with_coupon(3000))
1回目は2,500円、2回目は3,000円になります。入力はどちらも3,000円で同じなのに、出力が違います。理由は、関数の外にある used_coupon という値が、1回目の呼び出しで書き換えられ、2回目に影響したからです。
「1人1回」という仕様どおりに動いているなら、これは正しい動作です。しかし、この関数をテストするときや、別の人の注文に使うときは、前に誰が使ったかによって結果が変わってしまいます。同じ入力に同じ出力を返す関数ではないため、結果の再現も確認もしにくくなります。
この授業では、手順の中の「入力」「出力」「状態」を分けて見る方法を学びます。
直感的な説明:自動販売機は、お金・商品・在庫で動く
自動販売機で考えます。
- 入力:投入したお金と、押したボタン。
- 出力:出てくる飲み物と、お釣り。
- 状態:機械の中に残っているもの。在庫、投入済みの金額、お釣り用の硬貨の枚数。
同じ入力(120円を入れて、お茶のボタンを押す)でも、在庫が0なら飲み物は出ません。出力は、入力だけでなく状態にも左右されます。そして、1回買うと在庫が1本減ります。出力のほかに、状態も変わります。
プログラムの関数にも、2種類があります。
- 状態を持たない関数:同じ入力には、いつでも同じ出力を返す。計算機のようなもの。
- 状態を持つ手順:過去の呼び出しの結果が残り、同じ入力でも出力が変わる。自動販売機のようなもの。
状態を持つこと自体は悪くありません。注文の受付から配送までの流れは、状態の変化そのものです。大切なのは、状態がどこにあり、何が書き換えるのかを説明できることです。
たとえ話の限界:自動販売機の状態は目に見えますが、プログラムの状態(変数、書き換えられたリスト)は見えません。見えないものは、追跡表のように書き出して確かめます。
正確な定義:入力・出力・状態・副作用・純粋関数
用語を、正確に定義します。
- 入力:手順が受け取る値。関数では引数(ひきすう)。型(
int、str、listなど)を決めておく。 - 出力:手順が返す値。関数では戻り値(
returnで返す値)。returnを書かない関数はNone(何もない、という特別な値)を返す。 - 状態:手順の呼び出しをまたいで残る値。関数の外にある変数、リスト、ファイル、データベースの内容など。
- 副作用:戻り値以外に起きる変化。引数として渡されたリストを書き換える、画面に
printする、外部の変数を書き換える、ファイルに書く、など。 - 純粋関数:同じ入力には必ず同じ出力を返し、副作用がない関数。状態に依存しないので、検査ケースで確かめやすい。
print と return の違いを、実行して確認します。print は画面に文字を出す副作用で、値を返しません。
def double_print(x):
print(x * 2)
def double_return(x):
return x * 2
a = double_print(5)
b = double_return(5)
print('a =', a, type(a).__name__)
print('b =', b, type(b).__name__)
a は None(型名 NoneType)、b は10(型名 int)です。double_print の結果は、画面に出ただけで、変数に入っていません。続けて a + 1 を計算すると、エラーになります。
0と欠損(None)の区別:0は「0という値がある」、None は「値がない・わからない」です。たとえば売上が「0円」の日と「入力されていない」日は別の意味なので、計算でも区別します。
手順を設計するときの整理表を作ると、見落としが減ります。
| 確認する点 | 問い |
|---|---|
| 入力 | 型は? 単位は? 0や空や欠損が来たらどうなる? |
| 出力 | 型は? 返し忘れ(None)はない? |
| 状態 | 手順の外に残る値は何か? 誰が書き換える? |
| 副作用 | 引数を書き換えていないか? 画面やファイルに出していないか? |
段階付き例題:ポイントカードを、書き換えずに更新する
問題:ポイントカードがあり、購入金額100円ごとに1ポイントが付きます(100円未満は切り捨て)。関数 add_points(card, amount) を作り、元のカードを書き換えずに、新しいカードを返してください。
ステップ1:入力と出力を決める。入力:card(辞書:owner は str、points は int)、amount(int:購入額・円)。出力:更新後の新しいカード(辞書)。
ステップ2:計算式を決める。付与ポイントは、amount を100で割った商です。Python では amount // 100 と書き、小数点以下を切り捨てた整数になります。たとえば 1250 // 100 は12、99 // 100 は0です。
ステップ3:書き換えずに返す。dict(card) で辞書のコピーを作り、コピーのほうの points を更新して返します。
def add_points(card, amount):
new_card = dict(card) # コピーを作る
new_card['points'] = card['points'] + amount // 100
return new_card
card0 = {'owner': 'A', 'points': 20}
card1 = add_points(card0, 1250)
card2 = add_points(card1, 99)
print(card0)
print(card1)
print(card2)
ステップ4:追跡表で確認する。
| 呼び出し | 入力のカード | 購入額 | 付与 | 出力のカード |
|---|---|---|---|---|
add_points(card0, 1250) |
points 20 | 1,250 | 12 | points 32 |
add_points(card1, 99) |
points 32 | 99 | 0 | points 32 |
card0 は20のまま変わりません。入力を書き換えていないので、card0 をもう一度使って別の計算をしても、結果が変わりません。
ステップ5:意味を言葉に戻す。「99円の買い物ではポイントが付かない」「元のカードは、更新の記録を残すため変えない」という2つの文が、出力と一致しています。状態の変化を、「古い状態から新しい状態を作る」形で書くと、各時点の状態が全部残り、後から追跡できます。
操作して理解する:注文の状態遷移を表にして、許されない出来事を見つける
注文には状態(ordered:注文済み、paid:支払済み、shipped:発送済み、delivered:配達済み、cancelled:取消、refunded:返金済み)があり、出来事(pay:支払い、ship:発送、deliver:配達、cancel:取消、refund:返金)で状態が変わります。許される組み合わせを表(辞書)にすると、許されない出来事を見つけられます。
TRANSITIONS = {
('ordered', 'pay'): 'paid',
('ordered', 'cancel'): 'cancelled',
('paid', 'ship'): 'shipped',
('paid', 'refund'): 'refunded',
('shipped', 'deliver'): 'delivered',
}
def next_status(status, event):
return TRANSITIONS.get((status, event)) # 表に無い組み合わせは None
status = 'ordered'
for event in ['pay', 'ship', 'deliver', 'cancel']:
new = next_status(status, event)
print(f'{status:10} --{event}--> {new}')
if new is not None:
status = new
print('最終状態:', status)
実行する前に、最後の cancel がどうなるかを予想してください。答え:配達済み(delivered)の注文は、取消の出来事を受け付ける行が表に無いので、None が返り、状態は変わりません。最終状態は delivered です。
次に、自分で書き換えてみましょう。
- イベントを
['pay', 'refund', 'ship']にすると、最終状態は何ですか。 - 表に
('shipped', 'refund'): 'refunded'を加えると、配送中の返金も扱えます。この変更で、何が起きやすくなるか考えてください。
答えは、(1) refunded(paid→refunded の後、ship は表に無いので無視される)、(2) 発送済みの商品の返金を許すと、商品の回収という別の業務が必要になる。状態遷移の表は、業務ルールそのものです。
注意:
Noneを返して「許されない」を表す設計では、呼び出す側がNoneの確認を忘れてstatus = next_status(status, event)と書くと、状態がNoneに上書きされて失われます。上のコードのように確認しても、許されない出来事は黙って無視されるだけなので、気づきにくくなります。実務では、許されない出来事をエラーにして止めるか、ログに残す設計がよく使われます。
発展:状態を隠す設計・再現性・欠損の扱い
状態を引数に出す:関数が外部の変数に依存する代わりに、状態を引数と戻り値で受け渡す設計にすると、同じ入力に同じ出力を返す純粋な関数になります。例題の add_points がその形です。テストは簡単になりますが、状態の受け渡しを呼び出す側が管理する必要があります。
乱数の状態:乱数(ランダムな値)を使う手順では、乱数生成器が内部に状態を持ちます。同じ結果を再現するには、シード(乱数の出発点)を固定します。機械学習や統計の実験では、シードを記録しておくことが再現性の基本です。
ノートブックの隠れた状態:ノートブックは、セルの実行順序によって変数が残ります。セルを飛ばして実行したり、上のセルを書き換えたのに下のセルを再実行しなかったりすると、画面に見える順序と実行の順序が食い違います。結果を共有する前に、最初から全セルを順に実行し直して、同じ結果になるか確認します。
欠損の扱いは仕様で決める:欠損を0として扱うか、除いて計算するか、別の値で埋めるかは、指標の定義で決めます。どれを選んでも、選んだ理由と影響を記録してください。欠損を0にすると平均が下がることは、このあとの失敗3で確かめます。
同時実行:同じ状態を複数の処理が同時に書き換えると、順序によって結果が変わります。この授業の範囲を超えますが、業務システムで「二重登録」「在庫のずれ」が起きる原因の代表例です。
よくある失敗
失敗1:引数のリストを書き換えてしまい、2回目の結果が変わる
消費税10%を加える関数を、引数のリストを直接書き換える形で書いた例です。
def add_tax_wrong(prices):
for i in range(len(prices)):
prices[i] = prices[i] * 110 // 100 # 引数のリストを直接書き換える
return prices
def add_tax_right(prices):
return [p * 110 // 100 for p in prices] # 新しいリストを作る
cart = [1000, 2000]
print(add_tax_wrong(cart))
print(add_tax_wrong(cart))
print('cart =', cart)
cart2 = [1000, 2000]
print(add_tax_right(cart2))
print(add_tax_right(cart2))
print('cart2 =', cart2)
誤った関数は、1回目が [1100, 2200]、同じ cart で2回目を呼ぶと [1210, 2420] になります。税込みが二重にかかり、cart 自体も書き換わっています。正しい関数は、何回呼んでも [1100, 2200] で、cart2 は元のままです。
直し方:入力のリストは書き換えず、新しいリストを作って返す。関数の中で prices[i] = ... や prices.append(...)、prices.sort() のような書き換えを見つけたら、引数を壊していないか確認します。
失敗2:デフォルト引数のリストが、呼び出しをまたいで共有される
cart=[] をデフォルト値にした関数は、予想外の状態を持ちます。
def add_item(item, cart=[]):
cart.append(item)
return cart
def add_item_right(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(add_item('りんご'))
print(add_item('みかん'))
print(add_item_right('りんご'))
print(add_item_right('みかん'))
誤った関数は、1回目は ['りんご']、2回目は ['りんご', 'みかん'] になります。2回目の呼び出しで cart を渡していないのに、1回目のりんごが残っているためです。デフォルト値のリストは、関数を定義したときに1度だけ作られ、以後の呼び出しで共有されます。
直し方:デフォルト値は None にし、関数の中で if cart is None: cart = [] と、呼び出しごとに新しいリストを作ります。詳しくは 「デフォルト引数」(有料版) で扱います。
失敗3:欠損(None)を0として扱い、平均が下がる
3日間の売上のうち、2日目は「入力されていない」(欠損)です。これを0にして平均を出した例です。
sales = [1000, None, 3000] # None は「未入力」を表す
as_zero = [0 if v is None else v for v in sales]
print('欠損を0として平均:', round(sum(as_zero) / len(as_zero), 1))
known = [v for v in sales if v is not None]
print('欠損を除いて平均 :', round(sum(known) / len(known), 1))
欠損を0にすると、平均は1,333.3円。除いて計算すると2,000円です。実際に売上が0円だった日ならば、0を含めるのが正しいのですが、単に未入力だった日を0にすると、平均を実際より低く見せてしまいます。
直し方:0と欠損の意味を、データ辞書や指標の定義で決める。欠損を除く場合は、除いた件数も一緒に報告する(3件中2件で計算した、と書く)。
まとめ
- 手順は入力・出力・状態に分けて見る。同じ入力で出力が変わるなら、状態や副作用を疑う。
- 副作用(引数の書き換え、画面表示、外部変数の更新)を意識する。入力は書き換えず、新しい値を返す設計だと検査しやすい。
printは画面に出すだけで、returnが値を返す。returnが無い関数はNoneを返す。- 0と欠損は別物。欠損を0にすると平均が下がる。扱い方は指標の定義で決め、除いた件数も報告する。
用語
- 状態(state)
手順の呼び出しをまたいで残る値。外部の変数、リスト、ファイル、データベースの内容など。
- 副作用(side effect)
戻り値以外に起きる変化。引数の書き換え、画面への出力、外部の変数の更新など。
- 純粋関数(pure function)
同じ入力に必ず同じ出力を返し、副作用がない関数。
- 戻り値(return value)
関数が return で呼び出し元に返す値。return が無ければ None。
- 状態遷移(state transition)
出来事(イベント)によって、状態が別の状態に変わること。許される組み合わせを表で表す。
よくある質問
状態を持つ手順は避けるべきですか?
避ける必要はありません。注文の流れや在庫など、業務は状態の変化そのものです。大切なのは、状態がどこにあり、何がそれを書き換えるのかを説明でき、検査できることです。計算部分は純粋な関数にして、状態の更新は少数の場所にまとめると管理しやすくなります。
副作用は、すべて悪いものですか?
いいえ。画面への表示、ファイルへの保存、データベースの更新は、副作用がなければ成り立たない仕事です。問題になるのは、意図せず呼び出し元のデータが書き換わる場合です。副作用がある処理は、名前や説明で明示し、計算の部分と分けて書きます。
リストを書き換える `sort()` と、新しいリストを返す `sorted()` は、どう使い分けますか?
list.sort() は元のリストを並べ替え、None を返します。sorted(list) は元を変えず、並べ替えた新しいリストを返します。元の順序をあとで使うなら sorted を使います。x = x.sort() と書くと、x が None になる誤りが典型です。
0と欠損は、実務でどう区別して記録しますか?
データ辞書に「0は〜を意味する」「空欄は未入力を意味する」と書きます。システムによっては、欠損が空文字や -1 や 9999 のような特別な値で入ることがあります。集計の前に、値の分布を見て、特別な値が混じっていないか確認します。
ノートブックで結果が再現しないとき、何を確認しますか?
まず、カーネルを再起動して全セルを上から順に実行し直します。次に、乱数のシードが固定されているか、読み込むデータが同じか、ライブラリの版が同じかを確認します。セルの実行順や、前の実行で残った変数が原因のことが多いです。
状態遷移の表は、どこで使われますか?
注文、申請、チケットなど、状態が変わる業務で使います。状態の一覧と、許される出来事の組み合わせを表にすると、要件定義でのレビューが簡単になり、実装やテストの確認項目にもなります。許されない組み合わせの扱い(エラーか、無視か)も決めておきます。
確認問題
問1 「純粋関数」の説明として、最も適切なのはどれか。
- ① print を一度も使わない関数
- ② とても短い関数
- ③ 同じ入力には必ず同じ出力を返し、副作用がない関数
- ④ 引数が1つだけの関数
解答と解説
正解:③
純粋関数は、出力が入力だけで決まり、外部の状態を読み書きしない関数です。検査ケースで確かめやすい性質があります。
問2 関数の中で、引数として受け取ったリストに prices[0] = 0 と代入した。呼び出し元のリストはどうなるか。
- ① 呼び出し元のリストも書き換わる
- ② 呼び出し元のリストは変わらない
- ③ エラーになる
- ④ リストが空になる
解答と解説
正解:①
リストは、引数として渡しても同じリストを指します。関数の中での書き換えは、呼び出し元にも反映されます。
問3 def f(x): print(x * 2) と定義し、r = f(5) を実行した。r の値はどれか。
- ① 10
- ② '10'
- ③ 5
- ④ None
解答と解説
正解:④
f は print で画面に出すだけで、return を書いていないので None を返します。
問4 同じ入力を与えても、呼び出すたびに出力が変わる関数がある。最も疑うべきものはどれか。
- ① 入力の型が int であること
- ② 関数の外にある状態(変数)を読み書きしていること
- ③ 関数の名前が長いこと
- ④ 戻り値があること
解答と解説
正解:②
入力が同じなのに出力が変わるなら、入力以外の値(状態)が影響しているはずです。
問5 売上 [1000, None, 3000] の平均を、欠損(None)を0として計算した値と、除いて計算した値の組み合わせは、どれか。
- ① 約1333円と1333円
- ② 2000円と2000円
- ③ 約1333円と2000円
- ④ 2000円と約1333円
解答と解説
正解:③
0として計算すると 4000÷3 で約1333円、除くと 4000÷2 で2000円です。欠損の扱いで平均が変わります。
問6 def add(x, box=[]): box.append(x); return box を、add(1)、add(2) の順に呼ぶと、2回目の戻り値はどれか。
- ① [1, 2]
- ② [2]
- ③ [1]
- ④ エラーになる
解答と解説
正解:①
デフォルト値のリストは、定義時に1度だけ作られ、呼び出しをまたいで共有されます。2回目は1回目の要素が残っています。
次に学ぶこと
次は 2進数・ビット・容量 で、コンピュータが値を2進数(0と1)で持つしくみと、容量の見積もりを学びます。この授業の「型」と「0と欠損」の話が、そのまま活きます。
関数の戻り値と副作用は 「戻り値と副作用」(有料版)、デフォルト引数の落とし穴は 「デフォルト引数」(有料版) で詳しく扱います。この授業の確認問題だけでモジュールの習得とはしません。ON06 の総合テストと、会員割引の条件を表で検証する自力課題で判定します。
キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。
アプリを無料で開く有料版と無料版の違い