初心者向け No.002

tkinterでシンプル電卓を作る(evalを使わない計算ロジック)

ボタンを19個並べて、押した順に式を組み立て、計算して表示する。電卓を1本作ると、grid()での配置・ボタンのイベント処理・入力の検証がまとめて身につきます。計算部分は eval() を使わない書き方で組み立てます。

🎯 難易度: ★☆☆ 易しい 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜60分 🖥️ 確認環境: Windows 11(Python 3.12.10 / Tk 8.6)/Linux Mint 22.3(起動確認) 🔄 最終更新: 2026年9月10日
🧭
急いでいる人へ

コードだけ欲しい方は「4. 完全なソースコード」へ。ボタンが全部同じ動きをする方は「どのボタンを押しても同じ文字が入る」キーボードが効かない方は「キーボードから入力できない」eval() の危険性だけ知りたい方は「6. なぜ eval() を使わないのか」へ進んでください。

1. このページで作るもの

電卓はGUI入門の定番です。理由は、小さいのに要素が全部そろっているからです。ボタンをきれいに並べる配置の話、押されたことを受け取るイベントの話、入力された文字を計算に変える処理の話、そして計算に失敗したときの後始末の話。この4つを一度に練習できるアプリは、ほかにあまりありません。

完成する画面が次の4枚です。ウィンドウのサイズは280×380ピクセル、ボタンは全部で19個です。4枚目は同じコードを Linux Mint 22.3 で実行した画面です。

起動直後の電卓ウィンドウ。上部の濃紺の表示欄に右寄せで0、その下に4列5行のボタンが並ぶ。1行目は赤いC、オレンジの左向き矢印、灰色のパーセント、青い割る記号
図1: 起動直後。表示欄は「0」
数式を入力中の電卓ウィンドウ。表示欄に365かける24かける60かける60という式が文字列として表示されている
図2: 押した順に式が伸びていく
計算結果を表示した電卓ウィンドウ。表示欄に31536000という数値が右寄せで表示されている
図3: = を押すと結果に置き換わる(1年は31,536,000秒)
Linux Mint 22.3 で実行した電卓ウィンドウ。濃紺の表示欄に右寄せで0、その下に4列5行のボタンが並び、図1と同じ配置で表示されている
実行画面(Linux Mint)

読み終わったとき、あなたは次の3つができるようになります。

  1. 19個のボタンをgrid()で狙った位置に並べられる
  2. 「押した文字を式にする処理」と「式を計算する処理」を分けて書ける
  3. ユーザーが入力した文字列を、そのまま実行せずに安全に計算できる

3つ目が、このページのいちばんの中身です。電卓の作り方を調べると、計算部分に eval() という組み込み関数を使う例が多く見つかります。手軽ですが、公式ドキュメントがはっきり警告している関数でもあります。このページでは、まず動く電卓を作り、そのうえで eval() を使わない書き方に置き換えます。

このページで扱う機能

  • 数字と小数点の入力(先頭のゼロや小数点の重複を防ぐ)
  • 四則演算(足す・引く・かける・割る)と優先順位
  • パーセント計算・1文字削除・全消し
  • キーボードからの入力
  • ゼロ除算や未完成の式のエラー処理
  • 19個のボタンをリストとループでまとめて生成
📌
この記事の確認環境と検証方法

図1〜図3 は Windows 11 で撮影し、並べた4枚目は Linux Mint 22.3(仮想マシン)で撮影しました。掲載コードは Windows 11 / Python 3.12.10(Tcl/Tk 8.6)で検証しました。Linux Mint 22.3(仮想マシン)でも起動と画面表示を確認しました。操作しての検証と実測は Windows 11 のみです。macOS は未検証です。検証は、ウィンドウを組み立てたうえでボタン押下と同じ処理を呼び出し、表示欄の文字列を読み取る方法で行いました(実験2のテストコードと同じ考え方です)。仕様の説明はPython公式ドキュメントで裏取りし、引用箇所には出典を添えました。実測値はすべて筆者環境での測定値で、機種や環境によって変わります。

tkinterでウィンドウを出すところがまだ不安な方は、先にtkinterで最初のウィンドウを表示するを読んでおくと、この先の話がつながります。

2. 電卓を3つの部品に分けて考える

いきなりコードを書き始めると、ボタンの処理と計算の処理が混ざって手が止まります。電卓は、次の3つの部品に分けて考えると迷いません。

部品役割この記事での名前
入力を組み立てる押されたボタンの文字を、式の文字列に足していくappend_char()
式を計算する「365*24*60*60」のような文字列を数値にするsafe_eval()
結果を表示する数値を表示用の文字列にして画面に書くformat_result()StringVar

電卓の状態は、たったひとつの文字列で表せます。self.expression という変数に、押された順で "365*24*60*60" のように文字がたまっていくだけです。= が押されたら、その文字列を数値に変えて、結果でまた置き換えます。

💡
計算部分をクラスの外に出す理由

このページのコードでは、append_char()safe_eval()format_result() の3つを、クラスの中ではなくファイルの先頭に普通の関数として置いています。ウィンドウがなくても呼べる形にしておくと、画面を開かずに計算のテストができるからです。実際に筆者もこの形で先にロジックだけ動かし、そのあと画面をつなぎました。テストの書き方は実験2で紹介します。

関数の書き方そのものに不安があれば、関数の使い方Pythonの基本構文もあわせてどうぞ。

3. ボタンをgrid()で並べる(4列6行)

tkinterには配置の方法が3つあります。公式ドキュメントは grid について「ウィジェットを行と列の2次元の表に並べる。もっとも柔軟なマネージャで、まず選ぶべきもの」と説明しています。電卓のように格子状に並ぶ画面は、まさに grid の得意分野です。

まず、完成形の座標を先に決めてしまいます。次の図が、画面と grid(row=, column=) の対応です。

電卓のボタン配置とgridの行・列の対応図。4列6行の格子で、row=0の1行は表示欄がcolumnspan=4で4列すべてをぶち抜き、row=1はC・バックスペース・パーセント・割る、row=2は7・8・9・かける、row=3は4・5・6・ひく、row=4は1・2・3・たす、row=5は0・小数点・イコールで、0だけがcolumnspan=2で2列分を占める。ボタンは19個、占めるセルは20個
図4: 行と列の番号を先に決めてしまえば、あとはリストに書き写すだけです

数えると、ボタンは19個、使うセルは20個、格子は4列6行です。ボタン19個に対してセルが1つ多いのは、0のボタンだけが2列分の幅を占めるからです。

19個のボタンをリストで定義する

ボタンを1個ずつ tk.Button(...) と書くと、19回同じような行が並びます。そこで、ボタンの情報だけをリストにまとめ、ループで生成します。1つのタプルが1つのボタンで、中身は(テキスト, 行, 列, columnspan, 背景色)です。

BUTTONS = [
    ("C",   1, 0, 1, "#e74c3c"), ("←",  1, 1, 1, "#e67e22"),
    ("%",   1, 2, 1, "#95a5a6"), ("÷",  1, 3, 1, "#3498db"),
    ("7",   2, 0, 1, "#ecf0f1"), ("8",   2, 1, 1, "#ecf0f1"),
    # …(中略。全19個は4章のコードを参照)
    ("0",   5, 0, 2, "#ecf0f1"), (".",   5, 2, 1, "#ecf0f1"),
    ("=",   5, 3, 1, "#2ecc71"),
]

この形にしておくと、ボタンを増やしたいときはリストに1行足すだけで済みます。位置を変えたいときも、数字を書き換えるだけです。「見た目のデータ」と「作る処理」を分けるという考え方は、この先どのGUIアプリでも効いてきます。

ループで生成し、grid()で置く

for (text, row, col, span, color) in self.BUTTONS:
    btn = tk.Button(
        self.root,
        text=text,
        bg=color,
        command=lambda t=text: self.on_press(t)   # ← t=text がポイント
    )
    btn.grid(
        row=row, column=col, columnspan=span,
        sticky="nsew", padx=2, pady=2
    )

grid()に渡しているオプションの意味は、それぞれ次のとおりです。

指定意味
row / column何行目・何列目のセルに置くか(0から数える)
columnspan横に何セル分ぶち抜くか。表示欄は4、0のボタンは2
sticky="nsew"セルの上下左右に貼り付ける。これがないとボタンは文字の大きさのまま中央に小さく置かれる
padx / padyボタンとセルの境目のすきま(ピクセル)

ウィンドウを広げたときに一緒に伸ばす

sticky だけでは、ウィンドウが大きくなったときにボタンは付いてきません。行と列に「余った空間を受け取る権利」を与える必要があります。

for i in range(4):
    self.root.columnconfigure(i, weight=1)   # 4つの列
for i in range(6):
    self.root.rowconfigure(i, weight=1)      # 6つの行

ここで筆者がつまずいた点を1つ。weight=1 は「余った分を均等に配る」指定であって、「列の幅を等しくする」指定ではありません。実際に組み立てて各ボタンの幅を測ると、次のようにばらつきました。

設定column=0column=1column=2column=3最大差
weight=1 のみ(掲載コード)64px71px67px62px9px
weight=1, uniform="calc"66px66px66px66px0px

幅の基準になるのは、その列に入っているウィジェットが必要とする大きさです。「←」と「%」では文字の幅が違うため、列の基準幅も変わります。掲載しているキャプチャの列幅がわずかに不揃いなのは、このためです。きっちり等幅にしたい場合の直し方は実験1にまとめました。

同じ入れ物の中で pack() と grid() を混ぜない

公式ドキュメントは「同じコンテナを共有する2つのウィジェットに pack() と grid() を適用してはいけない。両者はサイズの調整方法が相容れず、互いにコンテナのサイズを変え合ってアプリが固まることがある。組み合わせたいときは、それぞれのマネージャのウィジェットを別のフレームに入れること」と警告しています。電卓に後からタイトル用のラベルを pack() で足す、といった改造をすると、エラーも出ないまま画面が固まります。

出典: Python公式ドキュメント tkinter(ジオメトリマネージャの説明・pack と grid の混在に関する警告)(原文は英語・2026年8月16日確認)/列幅は筆者環境(Windows 11 / Python 3.12.10 / Tk 8.6)での実測値

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)

全体で233行、空行とコメントを除くと185行です。前半が計算のロジック、後半が画面という2段構えになっています。まずはそのままコピーして app002.py という名前で保存し、動く画面を見てしまうのが近道です。1つずつの意味は次の章で分解します。

app002.py
"""シンプル電卓(tkinter)— eval() を使わない安全な計算ロジック版"""
import ast
import operator
import tkinter as tk

# ------------------------------------------------------------
# 計算ロジック(GUIから独立させておくとテストできる)
# ------------------------------------------------------------
DIGITS = "0123456789"
OPERATORS = "+-*/"

# 許可する演算だけを表に持つ。ここに無い演算はすべて拒否される
_BINARY_OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
}
_UNARY_OPS = {
    ast.UAdd: operator.pos,
    ast.USub: operator.neg,
}


def safe_eval(expression):
    """数値と + - * / だけからなる式を計算する。

    eval() と違い、許可した種類のノードしか実行しない。
    許可外の式は ValueError を送出する。
    """
    tree = ast.parse(expression, mode="eval")
    return _eval_node(tree.body)


def _eval_node(node):
    """構文木のノードを1つ評価する(再帰)"""
    if isinstance(node, ast.Constant):
        # bool は int のサブクラスなので明示的に弾く
        if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
            raise ValueError("数値以外のリテラルは使えません")
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in _BINARY_OPS:
        left = _eval_node(node.left)
        right = _eval_node(node.right)
        return _BINARY_OPS[type(node.op)](left, right)
    if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
        return _UNARY_OPS[type(node.op)](_eval_node(node.operand))
    raise ValueError(f"許可されていない式です: {type(node).__name__}")


def current_number(expression):
    """式の末尾にある「入力中の数値」部分を取り出す"""
    i = len(expression)
    while i > 0 and (expression[i - 1] in DIGITS or expression[i - 1] == "."):
        i -= 1
    return expression[i:]


def append_char(expression, char):
    """1文字の入力を式に足す。電卓としての入力ルールはここに集約する"""
    if char in OPERATORS:
        if not expression:
            # 先頭に置けるのはマイナス(負の数)だけ
            return "-" if char == "-" else expression
        if expression[-1] in OPERATORS:
            # 演算子の連打は置き換える("9**" を作らせない)
            return expression[:-1] + char
        return expression + char

    if char == ".":
        number = current_number(expression)
        if "." in number:
            return expression          # 小数点は1つの数値に1個まで
        if number == "":
            return expression + "0."   # ".5" ではなく "0.5" にする
        return expression + "."

    # ここから数字(想定外の文字は無視する)
    if char not in DIGITS:
        return expression
    if current_number(expression) == "0":
        return expression[:-1] + char  # 先頭のゼロを置き換える("05" を防ぐ)
    return expression + char


def format_result(value):
    """計算結果を表示用の文字列にする"""
    if isinstance(value, float):
        if value.is_integer():
            return str(int(value))     # 4.0 → 4
        return f"{value:.10g}"         # 0.30000000000000004 → 0.3
    return str(value)


# ------------------------------------------------------------
# GUI
# ------------------------------------------------------------
class Calculator:
    """電卓アプリのメインクラス"""

    # ボタンの定義: (テキスト, 行, 列, columnspan, 背景色)
    BUTTONS = [
        ("C",   1, 0, 1, "#e74c3c"), ("←",  1, 1, 1, "#e67e22"),
        ("%",   1, 2, 1, "#95a5a6"), ("÷",  1, 3, 1, "#3498db"),
        ("7",   2, 0, 1, "#ecf0f1"), ("8",   2, 1, 1, "#ecf0f1"),
        ("9",   2, 2, 1, "#ecf0f1"), ("×",  2, 3, 1, "#3498db"),
        ("4",   3, 0, 1, "#ecf0f1"), ("5",   3, 1, 1, "#ecf0f1"),
        ("6",   3, 2, 1, "#ecf0f1"), ("−",  3, 3, 1, "#3498db"),
        ("1",   4, 0, 1, "#ecf0f1"), ("2",   4, 1, 1, "#ecf0f1"),
        ("3",   4, 2, 1, "#ecf0f1"), ("+",   4, 3, 1, "#3498db"),
        ("0",   5, 0, 2, "#ecf0f1"), (".",   5, 2, 1, "#ecf0f1"),
        ("=",   5, 3, 1, "#2ecc71"),
    ]

    # 画面上の記号 → Python の演算子
    OP_MAP = {"×": "*", "÷": "/", "−": "-"}

    def __init__(self, root):
        self.root = root
        self.root.title("電卓")
        self.root.geometry("280x380")
        self.root.resizable(False, False)
        self.root.configure(bg="#2c3e50")
        self.expression = ""
        self._build_ui()
        self._bind_keyboard()

    def _build_ui(self):
        """UIを構築する"""
        # 表示エリア
        self.display_var = tk.StringVar(value="0")
        display = tk.Label(
            self.root,
            textvariable=self.display_var,
            font=("Arial", 28, "bold"),
            bg="#34495e", fg="white",
            anchor="e", padx=12,
            height=2
        )
        display.grid(row=0, column=0, columnspan=4,
                     sticky="nsew", padx=4, pady=(4, 2))

        # ボタンを生成
        for (text, row, col, span, color) in self.BUTTONS:
            fg = "white" if color != "#ecf0f1" else "#2c3e50"
            btn = tk.Button(
                self.root,
                text=text,
                font=("Arial", 16, "bold"),
                bg=color, fg=fg,
                activebackground=self._darken(color),
                relief=tk.FLAT,
                cursor="hand2",
                command=lambda t=text: self.on_press(t)
            )
            btn.grid(
                row=row, column=col, columnspan=span,
                sticky="nsew", padx=2, pady=2
            )

        # グリッドの重み設定(均等に拡張)
        for i in range(4):
            self.root.columnconfigure(i, weight=1)
        for i in range(6):
            self.root.rowconfigure(i, weight=1)

    def _darken(self, hex_color):
        """押したときの色を少し暗くする"""
        mapping = {
            "#ecf0f1": "#bdc3c7", "#3498db": "#2980b9",
            "#e74c3c": "#c0392b", "#2ecc71": "#27ae60",
            "#e67e22": "#d35400", "#95a5a6": "#7f8c8d",
        }
        return mapping.get(hex_color, hex_color)

    def _bind_keyboard(self):
        """キーボード入力をバインドする"""
        # bind には記号そのものではなく keysym 名を使う
        key_map = {
            "Key-0": "0", "Key-1": "1", "Key-2": "2", "Key-3": "3", "Key-4": "4",
            "Key-5": "5", "Key-6": "6", "Key-7": "7", "Key-8": "8", "Key-9": "9",
            "plus": "+", "minus": "−", "asterisk": "×", "slash": "÷",
            "period": ".", "Return": "=", "BackSpace": "←", "Escape": "C",
        }
        for key, action in key_map.items():
            self.root.bind(f"<{key}>", lambda e, a=action: self.on_press(a))

    def on_press(self, char):
        """ボタン押下時の処理"""
        if char == "C":
            self.expression = ""
            self._show("0")
        elif char == "←":
            self.expression = self.expression[:-1]
            self._show(self.expression or "0")
        elif char == "=":
            self._calculate()
        elif char == "%":
            self._calculate(percent=True)
        else:
            self.expression = append_char(
                self.expression, self.OP_MAP.get(char, char))
            self._show(self.expression or "0")

    def _calculate(self, percent=False):
        """式を計算して表示する(% のときは percent=True)"""
        try:
            result = safe_eval(self.expression)
            if percent:
                result = result / 100
            text = format_result(result)
        except ZeroDivisionError:
            self._error("0除算エラー")          # 1/0 のとき
        except (SyntaxError, ValueError):
            self._error("入力エラー")            # 式が未完成・許可外のとき
        except OverflowError:
            self._error("桁あふれ")              # 極端に大きい値のとき
        else:
            self.expression = text
            self._show(text)

    def _show(self, text):
        self.display_var.set(text)

    def _error(self, message):
        self.display_var.set(message)
        self.expression = ""


if __name__ == "__main__":
    root = tk.Tk()
    app = Calculator(root)
    root.mainloop()

実行はターミナルで python app002.py です(Windowsで python が反応しないときは py app002.py)。ウィンドウが出てこない場合は、tkinterそのものが使えるかどうかの切り分けをtkinterが使えるか30秒で確認するにまとめてあります。

5. コード解説(部品ごとに)

上から順に、意味のかたまりごとに見ていきます。長く見えますが、やっていることは「押された文字を足す」「式を計算する」「表示する」の3つだけです。

5-1. import と、許可する演算の表

import ast
import operator
import tkinter as tk

DIGITS = "0123456789"
OPERATORS = "+-*/"

_BINARY_OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
}

ast は、Pythonのコードを「構文木」という形に分解する標準ライブラリです。operator+* といった演算を関数の形で持っているモジュールで、operator.add(2, 3)2 + 3 と同じ意味になります。

この辞書と、同じ形で書いた符号用の _UNARY_OPS(4章のコードの19〜22行目)が、この電卓の安全装置の本体です。ここに書いた6種類——四則演算の4つと、符号のプラス・マイナスの2つ——しか実行しないと決めてしまいます。累乗(**)や関数呼び出しは表にないので、書かれていても実行されません。

5-2. ボタンの一括生成と lambda の落とし穴

for (text, row, col, span, color) in self.BUTTONS:
    fg = "white" if color != "#ecf0f1" else "#2c3e50"
    btn = tk.Button(
        self.root,
        text=text,
        font=("Arial", 16, "bold"),
        bg=color, fg=fg,
        activebackground=self._darken(color),
        relief=tk.FLAT,
        cursor="hand2",
        command=lambda t=text: self.on_press(t)
    )

注目してほしいのは command=lambda t=text: self.on_press(t)t=text です。ここを lambda: self.on_press(text) と書くと、19個のボタンすべてが「=」として動きます。

理由は、lambdaの中の text「押されたときに」評価されるからです。押されるころにはループは終わっていて、text にはリストの最後の値だけが残っています。t=text と引数のデフォルト値にしておくと、lambdaを作った瞬間の値がその場で固定されます。ボタンをループで作るときの定番の書き方なので、形ごと覚えてしまって構いません。症状としての見え方は9章にまとめました。

5-3. append_char(): 押された文字を式に足す

電卓の「入力ルール」を全部ここに集めています。単純に文字をつなげるだけだと、正しく計算できない式が簡単にできてしまうためです。

def append_char(expression, char):
    """1文字の入力を式に足す。電卓としての入力ルールはここに集約する"""
    if char in OPERATORS:
        if not expression:
            return "-" if char == "-" else expression
        if expression[-1] in OPERATORS:
            return expression[:-1] + char       # 演算子の連打は置き換える
        return expression + char

    if char == ".":
        number = current_number(expression)
        if "." in number:
            return expression                   # 小数点は1つの数値に1個まで
        if number == "":
            return expression + "0."
        return expression + "."

    if char not in DIGITS:
        return expression
    if current_number(expression) == "0":
        return expression[:-1] + char           # 先頭のゼロを置き換える
    return expression + char

この関数は引数と戻り値だけで完結していて、画面にも self にも触りません。だからウィンドウを開かずにテストできます。実際に押した順を渡して確かめた結果が次の表です。

押した順できる式この処理がないと
0 → 5505 になり、計算時にエラーになる
1 → 2 → + → 0 → 512+512+05 で同じくエラー
9 → × → × → 99*99**9 という累乗の式になる
. → 50.5.5(Pythonでは動くが表示が読みにくい)
1 → . → 5 → . → 21.521.5.2 で計算できない式になる

2番目までが、先頭のゼロの話です。Python 3では 05 のような書き方が構文エラーになります。筆者が実際に出したメッセージは次のとおりでした。

>>> eval("05")
SyntaxError: leading zeros in decimal integer literals are not permitted;
use an 0o prefix for octal integers

「0を押してから5を押す」は、電卓では誰でもやる操作です。入力を組み立てる段階で潰しておかないと、実際の利用でエラー表示が出ます。3番目の 9**9 も同じで、×を2回押しただけで累乗の式ができてしまいます。これがどれだけ危ないかは6章で数字を出します。

5-4. safe_eval(): 式を構文木にしてからたどる

ここがこのページの中心です。文字列をそのまま実行するのではなく、いったん構文木という形に分解し、許可した種類のノードだけを自分で計算します。

evalとastによる安全な計算の比較図。左のevalは文字列をPythonの式としてそのまま実行するため、2+3*4は14になる一方で、かけるボタンを2回押すだけで作れる累乗の式も通り約2.3秒固まる、入力欄を足せば任意のコードも動く。右のsafe_evalはast.parseで構文木に変換し、足し算のノードの下に数値2とかけ算のノード、その下に3と4がぶら下がる形にしてから、許可表にあるAdd・Sub・Mult・Div・UAdd・USubだけを実行し、Pow・Call・Nameなどは拒否する
図5: 「文字列を実行する」から「木をたどる」へ。通す式を自分で決められるようになります
def safe_eval(expression):
    tree = ast.parse(expression, mode="eval")
    return _eval_node(tree.body)


def _eval_node(node):
    if isinstance(node, ast.Constant):
        if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
            raise ValueError("数値以外のリテラルは使えません")
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in _BINARY_OPS:
        left = _eval_node(node.left)
        right = _eval_node(node.right)
        return _BINARY_OPS[type(node.op)](left, right)
    if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
        return _UNARY_OPS[type(node.op)](_eval_node(node.operand))
    raise ValueError(f"許可されていない式です: {type(node).__name__}")

ast.parse(expression, mode="eval") は、公式ドキュメントの説明では「単一の式をパースする」モードです。文字列を解析して木の形にするだけで、この時点では何も実行されません"2+3*4" なら、足し算のノードの下に数値の2と、かけ算のノードがぶら下がり、そのさらに下に3と4がぶら下がる形になります。

あとは _eval_node() が木を上からたどるだけです。数値ならそのまま返し、演算のノードなら左右をそれぞれ評価してから許可表の関数を呼びます。表にない種類のノードに出会った時点で ValueError で止まります。実際に試した結果が次のとおりです。

渡した式safe_eval() の結果
2+3*414(優先順位はast側が解釈するので自分で書く必要なし)
(1+2)*39(かっこもそのまま扱える)
-5+3-2(先頭のマイナスは単項演算として許可済み)
9**9ValueError: 許可されていない式です: BinOp
__import__('os').listdir('.')ValueError: 許可されていない式です: Call
'a'*3ValueError: 数値以外のリテラルは使えません
1+SyntaxError: invalid syntax(式が未完成)

ポイントは、禁止したいものを列挙するのではなく、許可するものだけを列挙していることです。禁止リスト方式は「書き忘れた1つ」が穴になりますが、許可リスト方式なら、知らない書き方が来ても自動的に弾かれます。

📎
bool を弾いている理由

isinstance(node.value, bool) の1行は、一見むだに見えます。ですがPythonでは boolint のサブクラスなので、isinstance(True, int)True になります。数値だけを通したいときは、boolを先に弾いておくのが定石です。電卓のボタンからは入力できませんが、あとで入力欄を足したときに効いてきます。

5-5. format_result(): 結果を読める形にする

def format_result(value):
    if isinstance(value, float):
        if value.is_integer():
            return str(int(value))     # 4.0 → 4
        return f"{value:.10g}"         # 0.30000000000000004 → 0.3
    return str(value)

Pythonの / は、割り切れる場合でも小数を返します。8/42.0 です。電卓の表示としては不格好なので、is_integer() で「小数点以下がない小数」を判定して整数に直しています。

もう1つの f"{value:.10g}" は、有効数字10桁で表示する書式です。これがないと 0.1+0.2 の結果が 0.30000000000000004 と表示されます。バグではなく、コンピュータが2進数で小数を扱うことによる誤差です。表示だけを丸めているので、内部の値は変えていません。お金の計算のように誤差を持ち込みたくない場合の対処は実験4に書きました。

5-6. キーボードから入力できるようにする

key_map = {
    "Key-0": "0", "Key-1": "1", "Key-2": "2", "Key-3": "3", "Key-4": "4",
    "Key-5": "5", "Key-6": "6", "Key-7": "7", "Key-8": "8", "Key-9": "9",
    "plus": "+", "minus": "−", "asterisk": "×", "slash": "÷",
    "period": ".", "Return": "=", "BackSpace": "←", "Escape": "C",
}
for key, action in key_map.items():
    self.root.bind(f"<{key}>", lambda e, a=action: self.on_press(a))

18個のキーを、ボタンと同じ処理につないでいます。ここでも a=action というデフォルト引数が出てきます。理由は5-2と同じです。

bind() に渡す文字列は、公式ドキュメントによれば <修飾キー-種類-詳細> という形式です。この「詳細」の部分に何を書けるかで、初心者はよくつまずきます。筆者が実際に11通りの書き方を試した結果が次の表です(4つのグループに分けて示します)。

書き方実測した結果
<plus> <minus> <asterisk> <slash> <period>いずれも登録でき、対応するイベントで呼び出された
<+> <*> </> <.>記号1文字でも登録でき、呼び出された(keysym名と同じ扱い)
<->TclError: no event type or button # or keysym。ハイフンは区切り文字なので単独では書けない
<1>数字キーではなくマウスの左ボタンとして登録された。数字キーのイベントでは呼び出されない

つまり「記号はそのまま書けない」わけではありません。書けないのはハイフンだけで、本当に危ないのは数字です。<1> はマウスのボタン1という意味に解釈されるため、数字キーは <Key-1> と書く必要があります。迷ったときは全部 keysym 名で書いておけば事故が起きません。

5-7. 失敗したときの後始末

def _calculate(self, percent=False):
    try:
        result = safe_eval(self.expression)
        if percent:
            result = result / 100
        text = format_result(result)
    except ZeroDivisionError:
        self._error("0除算エラー")          # 1/0 のとき
    except (SyntaxError, ValueError):
        self._error("入力エラー")            # 式が未完成・許可外のとき
    except OverflowError:
        self._error("桁あふれ")              # 極端に大きい値のとき
    else:
        self.expression = text
        self._show(text)

電卓は「間違った入力をされる前提」で作る必要があります。except を3つに分けているのは、原因ごとに違う文言を出すためです。ひとまとめに except Exception と書くこともできますが、それだと自分のコードのバグまで「エラー」の一言で隠れてしまい、直せなくなります。

else の中で初めて self.expression を書き換えている点も大事です。失敗したときは _error() の中で式を空に戻すので、エラー表示のあとに数字を押せば新しい計算を始められます。個別のエラーの読み方はPythonエラー一覧&解決法にまとめてあります。

出典: Python公式ドキュメント ast(ast.parse の mode 引数)同 tkinter(bind の sequence 形式)(いずれも2026年8月16日確認)。実測値は筆者環境(Windows 11 / Python 3.12.10 / Tk 8.6)での測定

6. なぜ eval() を使わないのか

電卓の作り方を調べると、計算部分がこの1行になっている例をよく見かけます。

result = eval(self.expression)   # 手軽だが、電卓では使わないほうがいい

たしかに動きます。eval("2+3*4")14 を返します。優先順位もかっこも自分で実装しなくて済むので、入門記事で選ばれるのは自然なことです。ただし公式ドキュメントは、この関数についてはっきり警告しています。

「この関数は任意のコードを実行します。 信頼できないユーザ入力にて この関数を呼び出すと、セキュリティの脆弱性につながります。」(Python公式ドキュメント 組み込み関数 eval・2026年8月16日確認)

「でも、この電卓には数字と演算子のボタンしかない。文字は入力できないのだから安全では?」と思うかもしれません。筆者も最初はそう考えていました。実際に試したところ、そうではありませんでした。

実測1: ボタンを7回押すだけで、2秒以上フリーズする式が作れる

eval() をそのまま使い、入力ルールを入れていない実装では、×ボタンを2回続けて押すと式に ** が並びます(このページの append_char() は連打を置き換えるため、掲載コードでは起きません)。Pythonでは ** は累乗です。つまり 9 → × → × → 9 → × → × → 7 の7回で、9**9**7 という式ができあがります。この式の計算にかかった時間を測りました。

結果の桁数計算にかかった時間
9**9**4約6,300桁0.1ミリ秒
9**9**5約56,000桁2.0ミリ秒
9**9**6約507,000桁68ミリ秒
9**9**7約456万桁2.12秒 / 2.34秒 / 2.41秒(3回測定)
9**9**9約3.7億桁(計算せず対数で見積もり)実用的な時間では終わらない

この計算はイベントループの中で走ります。tkinterのウィンドウが固まる原因と同じで、計算が終わるまでウィンドウは再描画もクリックも受け付けません。悪意のある誰かではなく、ボタンを連打しただけのユーザーが自分でアプリを固められるということです。

実測2: 計算できても、表示でエラーになる

おもしろいことに、桁数の大きい結果は計算に成功しても表示できません。

>>> str(9**9**4)
ValueError: Exceeds the limit (4300 digits) for integer string conversion;
use sys.set_int_max_str_digits() to increase the limit

これは、巨大な整数を文字列にする処理が重くなりすぎることへの、Python側の安全装置です。eval() を使った実装では、この ValueError も「エラー」と表示して終わることになります。ユーザーには何が起きたか分かりません。

実測3: 入力欄を1つ足した瞬間に穴になる

電卓を作ったあと、「式を直接打ち込めるようにしたい」と tk.Entry を足すのは自然な発展です。その瞬間、eval() は任意のコードの実行口になります。

eval("__import__('os').listdir('.')")   # フォルダの中身が読める

これに対して「危ない名前を禁止すればいい」「__builtins__ を空にすればいい」という対策が紹介されることがありますが、公式ドキュメントはその方法についてもこう書いています。

__builtins__ の上書きで 利用可能な名前を制限や変更できますが、これはセキュリティ機構ではありません:実行されるコードは依然として全組み込み機能にアクセスできます。」(Python公式ドキュメント 組み込み関数 eval・2026年8月16日確認)

禁止リストを育てる方向は、公式が「セキュリティ機構ではない」と明言している道です。だから5-4のように、許可する演算を6つだけ書いて、それ以外は全部拒否する方式にしました。コード量は増えますが、増えるのは37行(空行を除くと32行)です。

⚠️
ast.literal_eval() なら安全、でもない

「evalの代わりに ast.literal_eval() を使えばいい」という説明も見かけます。ただし公式ドキュメントは「過去において、この関数は「安全」と記載されていましたが、その意味は定義されていませんでした。それは誤解を招くものでした」としたうえで、「比較的小さな入力でもメモリ枯渇またはCスタック枯渇を引き起こし、プロセスをクラッシュさせる可能性があります」「信頼できないデータで呼び出すことは推奨されません」と注意しています。そもそも literal_eval() はリテラルしか扱えないので、1+2 のような計算式には使えません。

まとめると、学習用の電卓で eval() を使うこと自体が即座に事故になるわけではありません。ただ、「ユーザーが打った文字列を計算する」という処理は、実務でもそのまま出てくる形です。最初に安全な形で覚えておくほうが、あとで書き換える手間より確実に安く済みます。

ただし safe_eval() にすれば何でも安全になるわけではありません。任意のコードが実行されるタイプの危険はなくなりますが、桁数の大きい計算に時間がかかることや、極端に長い式で解析側が限界を迎えることは残ります。限界と追加の対策は11章のQ&Aにまとめました。

出典: Python公式ドキュメント 組み込み関数 eval同 ast(literal_eval の注意書き・原文は英語/訳は筆者)(いずれも2026年8月16日確認)。時間と桁数は筆者環境(Windows 11 / Python 3.12.10)での実測値で、環境によって変わります

7. 空のウィンドウから作る7ステップ

233行をいきなり写経すると、どこで間違えたのか分からなくなります。空のウィンドウから始めて、1ステップごとに実行して確認するのが結局いちばん速い進み方です。各ステップの終わりで必ず一度実行してください。

  1. 1
    空のウィンドウを出す

    まず土台だけ作ります。ここで灰色のウィンドウが出れば、環境は問題ありません。出ない場合は先にHello Worldの記事で切り分けてください。

    import tkinter as tk
    
    root = tk.Tk()
    root.title("電卓")
    root.geometry("280x380")
    root.mainloop()
  2. 2
    表示欄を1つ置く

    StringVar と結びつけたラベルを、1行目に4列ぶち抜きで置きます。anchor="e" で電卓らしく右寄せになります。mainloop() より上に書いてください。

    display_var = tk.StringVar(value="0")
    display = tk.Label(root, textvariable=display_var, font=("Arial", 28, "bold"),
                       bg="#34495e", fg="white", anchor="e", padx=12, height=2)
    display.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=4, pady=(4, 2))
  3. 3
    ボタンを4個だけ並べてみる

    いきなり19個作らず、まず1行分で位置の感覚をつかみます。押したらターミナルに文字が出るだけの形にしておくと、配置の確認に集中できます。

    for i, text in enumerate(["7", "8", "9", "×"]):
        tk.Button(root, text=text, font=("Arial", 16, "bold"),
                  command=lambda t=text: print(t)).grid(
            row=2, column=i, sticky="nsew", padx=2, pady=2)
  4. 4
    19個をリストから一括生成する

    位置の感覚がつかめたら、3章BUTTONS リストに置き換えます。ここで columnconfigurerowconfigure も入れて、ウィンドウいっぱいに広がることを確認してください。

  5. 5
    押した文字を表示欄に出す

    まだ計算はしません。押した文字をつなげて表示するだけにします。数字と演算子が画面に並んでいくのを見ると、電卓の正体が「1本の文字列」だと実感できます。

    expression = ""
    
    
    def on_press(char):
        global expression
        expression += char
        display_var.set(expression)
  6. 6
    入力ルールと計算をつなぐ

    5章append_char()safe_eval() を追加し、= が押されたときに呼びます。ここまでで電卓として使えるようになります。

  7. 7
    クラスにまとめ、キーボードとエラー処理を足す

    global が増えてきたら、クラスにまとめる合図です。ウィジェットと式を self. で持たせると、関数の間で値を受け渡す悩みが消えます。最後に try-except とキーボードの bind() を足せば、4章の完成形と同じになります。

💡
エディタ側でつまずいたら

実行ボタンが出ない・Pythonが選べないといった場合は、VSCodeのPython環境セットアップで拡張機能とインタープリターの設定を確認してください。Python自体がまだならインストール手順からどうぞ。

8. 自分でいじって試す5つの実験

写経が終わったら、次は1か所ずつ書き換えて結果を見る番です。どれも数分で試せます。

実験1: ボタンの幅をきっちり揃える

3章で触れたとおり、weight=1 だけでは列幅が揃いません。uniform に同じ名前を付けた列は「同じ幅のグループ」として扱われます。

for i in range(4):
    root.columnconfigure(i, weight=1, uniform="calc")

筆者環境で各ボタンの実際の幅を測ったところ、weight=1 だけのときは 64・71・67・62px(最大差9px)でしたが、uniform="calc" を足すと 66px で揃いました。見た目の差はわずかですが、電卓のような格子状の画面では効きます。

実験2: 画面を開かずに計算ロジックをテストする

計算部分をクラスの外に出しておいた効果を確かめます。app002.py と同じフォルダに次のファイルを作って実行するだけです。ウィンドウは開きません。

test_calc.py
from app002 import append_char, safe_eval, format_result


def press(keys):
    """ボタンを順番に押したときの式を返す"""
    expression = ""
    for key in keys:
        expression = append_char(expression, key)
    return expression


cases = [
    ("05", "5"),
    ("12+05", "12+5"),
    ("9**9", "9*9"),
    (".5", "0.5"),
    ("365*24*60*60", "365*24*60*60"),
]
for keys, expected in cases:
    got = press(keys)
    print("OK " if got == expected else "NG ", keys, "→", got)

print(format_result(safe_eval("365*24*60*60")))
print(format_result(safe_eval("0.1+0.2")))

筆者の環境での出力は次のとおりでした。ボタンを何十回も押して確かめる代わりに、1秒で確認できます。

OK  05 → 5
OK  12+05 → 12+5
OK  9**9 → 9*9
OK  .5 → 0.5
OK  365*24*60*60 → 365*24*60*60
31536000
0.3

この形にしておくと、あとで入力ルールを変えたときに「前は動いていた操作が壊れていないか」をすぐ確認できます。GUIアプリのテストが難しいと言われるのは画面の部分で、ロジックを外に出しておけばテストは普通の関数と同じです。

実験3: かっこボタンを足す

safe_eval()ast に解析を任せているので、かっこは何もしなくても扱えます。実際 (1+2)*3 は 9 を返しました。かっこはノードではなく、木の形そのものとして表現されるためです。

("(",   6, 0, 1, "#95a5a6"), (")",   6, 1, 1, "#95a5a6"),

あとは BUTTONS に上のような行を足し、rowconfigure の範囲を range(7) に広げるだけです。加えて append_char() でかっこを受け付ける処理も要ります(今のままでは数字でも演算子でもないため無視されます)。閉じかっこが足りない式は SyntaxError になり、「入力エラー」と表示されます。自分で優先順位の計算を書かずに済むのが、構文木に任せる方式の利点です。

実験4: お金の計算を Decimal にする

浮動小数点の誤差が気になる用途では、decimal モジュールを使います。同じ計算の結果を並べると違いがはっきりします。

from decimal import Decimal

print(0.1 + 0.2)                          # 0.30000000000000004
print(Decimal("0.1") + Decimal("0.2"))    # 0.3

電卓に組み込むなら、_eval_node() の数値を返す部分で Decimal(str(node.value)) に変換し、割り算だけ桁数を決めて丸める形になります。ただし表示や例外処理まで一式変わるので、まず今の形が動くようになってから取り組むのがおすすめです。

実験5: 計算履歴を残す

計算した式と結果をリストにためて、別のウィジェットに表示します。_calculate() の成功時(else の中)に1行足すだけで、履歴の材料はそろいます。

self.history.append(f"{before} = {text}")   # before は計算前の式

表示先には tk.Listbox が手軽です。ここまで来ると、電卓は「状態を持つアプリ」になります。次に作るアプリを選ぶときの足がかりにもなるはずです。

9. うまく動かないときの症状別チェック

GUIは「エラーが出ないのに思ったとおりにならない」ことがあります。まず症状から当たりを付けてください。

症状まず疑うところ
どのボタンを押しても同じ動きlambdaの t=text の書き忘れ
押しても反応しない/起動しただけで動くcommand() を付けている
ボタンが見えない・重なるgrid() の呼び忘れ、行番号の重複
ボタンが小さいまま / 幅が不揃いstickyweightuniform
キーボードが効かないbind() の書式(数字は Key-1
数字を押しただけでエラー表示先頭のゼロ(05
結果が 0.30000000000000004浮動小数点の誤差と表示の丸め
0で割ると落ちるZeroDivisionError の捕捉漏れ
押したあと固まる累乗(**)の式ができている

❌ どのボタンを押しても同じ文字が入る

原因: ループの中で command=lambda: self.on_press(text) と書いています。lambdaの中の text が評価されるのはボタンが押されたときなので、そのころにはループが終わっていて、最後の値(このコードでは「=」)だけが残っています。

対処: デフォルト引数で値をその場に固定します。数字ボタンだけでなく、キーボードの bind() でも同じ書き方が必要です。

# NG: 押した時点の text を見にいく
command=lambda: self.on_press(text)

# OK: 作った時点の値を t に固定する
command=lambda t=text: self.on_press(t)

❌ ボタンを押しても反応しない・起動しただけで処理が動く

原因: command に関数の呼び出し結果を渡しています。command=self.on_press("1") と書くと、ボタンを作った瞬間に on_press("1") が実行され、その戻り値が command に入ります。筆者環境で確かめたところ、戻り値が None のときは command が空になり、以後どれだけ押しても何も起きませんでした。戻り値が None 以外の場合は、押した瞬間に TclError: invalid command name ... が出ます。

対処: 引数が要るときは lambda で包み、押されたときに呼ばれる形にします(値の固定もセットで必要です)。

# NG: 作った瞬間に実行され、command には戻り値が入る
command=self.on_press("1")

# OK: 押されたときに呼ばれる形にする
command=lambda t="1": self.on_press(t)

❌ ボタンが表示されない・重なる

原因: 3つあります。①grid() を呼んでいない(作っただけでは表示されません)、②同じ rowcolumn に2つ置いている、③同じ入れ物で pack() と混ぜている。

対処: ①はすべてのウィジェットに配置のメソッドを呼びます。②は行と列の番号を数え直します。表示欄が row=0 を使っているので、ボタンは row=1 から始めるのを忘れがちです。③はフレームで入れ物を分けてください。

なお btn = tk.Button(...).grid(...) と1行にまとめると、btn には None が入ります(grid() の戻り値)。あとから btn.config(...) したいときは、作成と配置を2行に分けてください。

❌ ボタンの幅が揃わない・ウィンドウを広げても伸びない

原因: 伸びない場合は stickyweight のどちらかが抜けています。sticky="nsew" はセルの中でウィジェットを広げる指定、columnconfigure(i, weight=1) は余った空間をその列に配る指定で、両方必要です。

対処: 幅を完全に揃えたい場合は uniform を足します。weight は「余りの配分」であって「幅を等しくする指定」ではない、と覚えておくと迷いません(実験1に実測値)。

root.columnconfigure(i, weight=1, uniform="calc")

❌ キーボードから入力できない

原因: bind() に渡す文字列の書式が違うか、ウィンドウがキー入力の対象になっていません。特に多いのが数字で、<1> と書くとマウスの左ボタンという意味になります。筆者の環境で確かめたところ、<1> に登録したハンドラは数字キーでは1度も呼ばれず、マウスのクリックで呼ばれました。

対処: 数字は <Key-0><Key-9>、記号は keysym 名(plusminusasteriskslashperiod)で書きます。<->TclError: no event type or button # or keysym になるので、マイナスは必ず <minus> と書いてください。

root.bind("<Key-1>", handler)     # 数字キーの1
root.bind("<plus>", handler)      # +
root.bind("<minus>", handler)     # -(<-> は書けない)
root.bind("<Return>", handler)    # Enter

それでも反応しない場合は、バインド先を確認してください。ウィジェット単位でバインドすると、そのウィジェットが選択されているときしか効きません。電卓全体で受けたいときは、このコードのように root にバインドします。

❌ 「05」と入力すると計算できない

原因: Python 3では、05 のように先頭に0が付いた10進数の書き方が構文エラーになります。文字を単純につなげる実装だと、0を押してから5を押しただけでこの状態になります。

SyntaxError: leading zeros in decimal integer literals are not permitted;
use an 0o prefix for octal integers

対処: 計算する前に、入力の段階で潰します。5-3append_char() では、いま入力中の数値が "0" のときだけ、その0を新しい数字で置き換えています。実際の電卓と同じ挙動になります。

❌ 0.1+0.2 が 0.30000000000000004 になる

原因: バグではありません。コンピュータは小数を2進数で近似して持つため、10進数で書いた 0.1 はぴったりの値になりません。Pythonに限らず、多くの言語で同じ結果になります。

対処: 表示だけ丸めるなら書式指定で足ります。金額のように誤差を許容できない場合は decimal モジュールを使います(実験4)。

print(f"{0.1 + 0.2:.10g}")   # 0.3

❌ 0で割ったときにアプリが落ちる

原因: ZeroDivisionError を捕まえていません。電卓では「1÷0」は珍しい操作ではないので、必ず起きる前提で書きます。

対処: try-except で捕まえ、表示欄にメッセージを出して式を空に戻します。1/0division by zero5/0.0float division by zero と、メッセージは違いますが例外の種類は同じです。エラーの読み方はZeroDivisionErrorの解説にまとめています。

try:
    result = safe_eval(self.expression)
except ZeroDivisionError:
    self._error("0除算エラー")

❌ ボタンを押したあと画面が固まる

原因: 計算そのものに時間がかかっています。eval() をそのまま使う実装では、×を2回押すだけで **(累乗)の式ができ、9**9**7 の計算だけで2秒以上かかりました(6章に実測表)。計算が終わるまでイベントループは他の操作を処理できません。

対処: 2つあります。①入力の段階で演算子の連打を置き換え、そもそも ** を作らせない。②計算側でも累乗のノードを許可しない。このページのコードは両方入れています。時間のかかる処理を扱うアプリ全般の考え方は、tkinterでウィンドウが固まるときの対処も参考にしてください。

出典: 症状と対処は筆者環境(Windows 11 / Python 3.12.10 / Tk 8.6)で再現・確認したもの。仕様の裏取りはPython公式ドキュメント tkinter(2026年8月16日確認)

10. 練習問題

手を動かして初めて身につきます。ヒントだけ添えるので、答えを見る前に自分で書いてみてください。

  1. 課題1: 符号反転ボタン(±)を付ける

    表示中の数値の符号を入れ替えるボタンを追加しましょう。
    ヒント: 式全体を計算してから -1 をかける方法と、いま入力中の数値だけを取り出して符号を付ける方法があります。後者には current_number() が使えます。

  2. 課題2: メモリ機能(M+ / MR / MC)

    計算結果をためる M+、呼び出す MR、消す MC の3つのボタンを追加しましょう。
    ヒント: self.memory = 0__init__ に用意します。表示のたびに BUTTONS を書き換える必要はありません。行を1つ増やして rowconfigure の範囲を range(7) にします。

  3. 課題3: 直前の計算をやり直す

    = を押した直後にもう一度 = を押すと、同じ演算をもう1回適用する機能を作りましょう(3+4= のあと = を押すと 11)。
    ヒント: 最後に使った演算子と右側の数値を覚えておく必要があります。self.last_operation のような属性を足します。

  4. 課題4: 許可する演算を1つ増やす

    safe_eval() に剰余(%、割った余り)を追加してみましょう。
    ヒント: _BINARY_OPSast.Mod: operator.mod を足します。画面の % ボタンはパーセント計算に使っているので、ボタンの割り当てをどうするかも考えてみてください。

11. よくある質問

Q. 電卓を作るのに追加のインストールは必要ですか?

不要です。tkinterはPythonの標準ライブラリで、astoperator も標準です。pip install は1つも要りません。ただしLinuxのディストリビューションによっては、tkinterが別パッケージになっていることがあります。判断は python -m tkinter の結果で付けてください(確認手順)。

Q. なぜ多くの入門記事は eval() を使っているのですか?

1行で書けて、演算子の優先順位もかっこも自動で処理されるからです。入門者に見せるコードとしては読みやすく、選ばれる理由はよく分かります。このページでは、ボタン操作だけで2秒以上フリーズする式が作れることを実測したうえで、37行(空行を除くと32行)増やして安全な形にしました(6章)。学習用として eval() の動きを知っておくこと自体は有益です。

Q. safe_eval() を使えば完全に安全ですか?

「任意のコードが実行される」タイプの危険はなくなりますが、完全ではありません。たとえばかけ算だけでも、桁数の大きい数を何度もかければ計算に時間がかかります。Python公式ドキュメント ast も、構文解析そのものについて「十分に大きい文字列や複雑な文字列によってPythonの抽象構文木コンパイラのスタックの深さの限界を越えることで、Pythonインタプリタをクラッシュさせることができます」と注意しています(2026年8月16日確認)。外部からの入力を受け付けるアプリでは、式の長さや桁数に上限を設けるなど、別の対策も必要になります。

Q. ボタンを2次元リストで作る書き方との違いは?

どちらでも構いません。2次元リスト([["7","8","9","×"], ...])は見た目が画面に近く、行と列を enumerate() で自動的に決められるのが利点です。このページで1次元のタプルにしているのは、columnspan と色をボタンごとに変えたかったからです。0のボタンだけ2列分にする、といった例外がある画面では、こちらのほうが素直に書けます。

Q. 演算子の優先順位は自分で実装しなくていいのですか?

必要ありません。ast.parse() がPythonの文法どおりに解釈するので、2+3*4 は 14 になります。かっこ付きの (1+2)*3 も 9 になりました。優先順位の処理を自分で書くのは、電卓を「文字列を左から順に処理する」方式で作った場合の話です。

Q. macOSやLinuxでも同じコードが動きますか?

tkinterはmacOSを含むほとんどのUnix系プラットフォームとWindowsで使えるため、コードはそのまま動く想定です。ただしボタンの見た目・フォント・ウィンドウの余白は環境によって変わります。このページの実測値はWindows 11のものです。当サイトでは Windows 11 と Linux Mint 22.3 で起動を確認しました(1章の4枚目が Linux Mint の画面です)。macOS は未検証です。ボタンの色や大きさの見え方はOSごとに差が出るため、他の環境で作る場合は色の指定を微調整してください。

Q. 表示が「入力エラー」から戻らなくなりました。元に戻すにはどうすればいいですか?

エラー表示のあとは式が空に戻っているので、数字を押せば新しい計算を始められます。もし数字を押しても何も起きない場合は、_error() の中で self.expression = "" を実行しているか確認してください。表示だけ変えて式を残すと、次に押した数字が壊れた式の後ろに付いてしまいます。

Q. コードは動いたけれど、ast の部分がよく分かりません。このまま次に進んでいいですか?

それで問題ありません。safe_eval() は関数として完結しているので、中身が分からなくても「数字と四則演算だけを計算してくれる部品」として使えます。1本作るごとに1つ増やせば十分です。まずは基本構文関数を固めて、次のアプリで同じ形が出てきたときに読み返してください。

12. 次の一歩

電卓が動いたら、次は同じ部品の組み合わせで、扱うデータを変える段階です。1本ごとに新しい要素を1つだけ足していくのが、結局いちばん速い進み方です。

次に作るものここで新しく身につく要素
温度変換アプリEntry からの入力の取り出しと、数値への変換
BMI計算アプリ複数の入力欄と、結果に応じた表示の出し分け
デジタル時計after() による定期更新(画面を止めずに動かす)
ToDoリストリスト表示と、データの追加・削除
科学電卓math モジュールの関数追加と、ボタンが増えたときの配置

同じ電卓でも sincoslog・√ まで扱いたい方は、科学電卓(No.076)へ進めます。このページは四則演算と「eval()を使わない計算ロジック」、科学電卓は math モジュールで関数を足していく回という住み分けです。grid() でボタンを並べる考え方はそのまま使い回せます。なお科学電卓のページは、math の関数に __builtins__ を空で添えた辞書を渡して eval() を呼ぶ実装です(呼べる名前は絞られますが、上の公式引用のとおりこれはセキュリティ機構ではありません)。6章の理由から、外部からの入力を受け取る形に育てるときは、このページの許可リスト方式に置き換えてください。

🚀
次に挑戦するアプリ

電卓で「押す→処理する→表示する」の型を作れたので、次は入力欄を使う温度変換アプリが自然な流れです。エラー処理の考え方はそのまま使い回せます。

作りたいものが決まっていないときは初心者用GUIアプリ一覧から好きなものを選んでください。標準ライブラリの外に出て何ができるかを知りたい方はPythonライブラリ一覧、独学の順番を1冊で固めたい方はPython学習本の比較もどうぞ。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
独学を1冊で体系化するなら

写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。