Python言語の文法解説
基本文法から、データ構造・アルゴリズムの実装、並行処理、メモリ管理まで、基本情報技術者試験に頻出するCS基礎トピックをPythonのコードで確認しながら学びます。
文法解説
1. コメント
# の後ろは行末までコメントになります。複数行の文字列リテラル("""...""")を文の位置に書くとドキュメンテーション用のコメント(docstring)として使われることもあります。
# これは1行コメントです
x = 10 # 行末コメント
def greet(name):
"""関数の説明を書くdocstring。help(greet)で表示できる。"""
return f"こんにちは、{name}さん"
2. 変数とデータ型(動的型付けと型ヒント)
Pythonは変数の型を宣言せずに代入するだけで使える動的型付け言語です。変数は値そのものではなく、値(オブジェクト)への参照を保持します。主なデータ型は数値(int, float, complex)、文字列(str)、真偽値(bool)、リスト(list)、タプル(tuple)、辞書(dict)、集合(set)、Noneです。Python 3.5以降は型ヒント(type hints)を書けますが、これはあくまで注釈であり、実行時の型チェックは行われません(静的型付け言語のように強制されるわけではない点に注意)。
age = 20 # int
height = 172.5 # float
name = "太郎" # str
is_student = True # bool
fruits = ["apple", "banana", "orange"] # list(変更可能)
point = (10, 20) # tuple(変更不可)
scores = {"math": 90, "eng": 80} # dict
uniq = {1, 2, 2, 3} # set -> {1, 2, 3}
# 型ヒント(実行時には強制されない、mypy等の静的解析ツールでチェックする)
def add(a: int, b: int) -> int:
return a + b
user_name: str = "花子"
scores_list: list[int] = [80, 90, 70]
print(type(age)) #
print(f"{name}さんは{age}歳です")
3. 演算子
算術演算子(+ - * / // % **)、比較演算子(== != < > <= >=)、論理演算子(and or not)、代入演算子(+= -= *= など)、メンバシップ演算子(in, not in)、同一性演算子(is, is not)があります。/ は常に浮動小数点数を返し、// は切り捨て除算(整数除算)です。== は値の等価性、is はオブジェクトの同一性(同じメモリ上のオブジェクトか)を判定する点が異なります。
print(7 / 2) # 3.5 (常にfloat)
print(7 // 2) # 3 (切り捨て除算)
print(7 % 2) # 1 (剰余)
print(2 ** 10) # 1024 (べき乗)
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True (値が等しい)
print(a is b) # False (別オブジェクト)
print(3 in a) # True
print(True and False) # False
print(not True) # False
4. 制御構文(if / for / while)
条件分岐はif / elif / else、繰り返しはforとwhileを使います。他の多くの言語と異なり、ブロックは中括弧ではなくインデント(半角スペース4つが慣習)で表現します。breakでループを中断、continueで次の周回へスキップできます。
score = 75
if score >= 80:
print("優")
elif score >= 60:
print("良")
else:
print("再履修")
for i in range(5):
if i == 2:
continue # 2のときは以降をスキップ
if i == 4:
break # 4に達したら終了
print(f"{i}回目のループ")
count = 0
while count < 3:
print(count)
count += 1
else:
# whileがbreakされずに正常終了したときだけ実行される
print("whileループ正常終了")
5. 関数(デフォルト引数・可変長引数・ラムダ・再帰・スコープ/クロージャ)
defで関数を定義します。引数にはデフォルト値、可変長位置引数(*args)、可変長キーワード引数(**kwargs)を指定できます。無名関数はlambda式で書けます。関数は自分自身を呼び出す再帰も可能です。変数のスコープはローカル→エンクロージング(外側の関数)→グローバル→組み込みの順に探索されます(LEGBルール)。内側の関数が外側の変数を覚えたまま返される関数をクロージャと呼びます。
def greet(name, honorific="さん"):
return f"{name}{honorific}、こんにちは"
def total(*args, **kwargs):
print("kwargs:", kwargs)
return sum(args)
square = lambda x: x * x
def factorial(n):
# 再帰関数:終了条件(ベースケース)を必ず用意する
if n <= 1:
return 1
return n * factorial(n - 1)
def make_counter():
count = 0
def increment():
nonlocal count # 外側関数の変数を書き換える
count += 1
return count
return increment # 状態を保持したまま返す=クロージャ
print(greet("花子"))
print(total(1, 2, 3, x=10))
print(square(5))
print(factorial(5)) # 120
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
6. 文字列操作
文字列(str)はイミュータブル(変更不可)です。f文字列(f"...")による埋め込み、スライス、split/join/replace/stripなどのメソッドがよく使われます。
s = " Hello, Python World "
print(s.strip()) # 前後の空白除去
print(s.strip().lower()) # 小文字化
print(s.strip().replace("Python", "みんな"))
print(",".join(["a", "b", "c"])) # "a,b,c"
print("a,b,c".split(",")) # ['a', 'b', 'c']
name = "太郎"
age = 20
print(f"{name}さんは{age}歳です") # f文字列(推奨)
print("{}さんは{}歳です".format(name, age))
text = "Programming"
print(text[0:4]) # "Prog" スライス
print(text[-4:]) # "ming" 末尾から4文字
print(text[::-1]) # "gnimmargorP" 逆順
7. リスト内包表記
Pythonらしい書き方として「リスト内包表記」があります。通常のfor文よりも簡潔に書け、内部的にも最適化されているため高速です。辞書内包表記・集合内包表記も同様の構文で書けます。
numbers = [1, 2, 3, 4, 5, 6]
# 通常のfor文
squares = []
for n in numbers:
squares.append(n * n)
# リスト内包表記(推奨)
squares_fast = [n * n for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
# 辞書内包表記
square_map = {n: n * n for n in numbers}
# 集合内包表記
even_set = {n for n in numbers if n % 2 == 0}
print(squares_fast) # [1, 4, 9, 16, 25, 36]
print(evens) # [2, 4, 6]
print(square_map) # {1: 1, 2: 4, ...}
8. イテレータとジェネレータ
for文で回せるオブジェクトをイテラブル、__iter__と__next__を実装したオブジェクトをイテレータと呼びます。yieldを使う関数(ジェネレータ関数)は、呼び出すたびに値を1つずつ返す「遅延評価」を行い、リスト全体をメモリに保持しないため大量データや無限列の処理にメモリ効率よく対応できます。
def countdown(n):
while n > 0:
yield n # ここで一時停止し、値を返す
n -= 1
for num in countdown(3):
print(num) # 3, 2, 1
# ジェネレータ式(リスト内包表記の()版、メモリ効率が良い)
squares_gen = (n * n for n in range(1_000_000))
print(next(squares_gen)) # 0
print(next(squares_gen)) # 1
class RangeIterator:
"""独自のイテレータクラスの例"""
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
for v in RangeIterator(0, 3):
print(v)
9. クラスとオブジェクト指向(カプセル化・継承・ポリモーフィズム)
classでクラスを定義し、__init__がコンストラクタの役割を果たします。selfはインスタンス自身を指します。属性名の先頭にアンダースコアを付ける(_xや__x)ことで「非公開」であることを慣習的に示すカプセル化ができます(強制力はない)。class Dog(Animal)のように親クラスを指定すると継承になり、親と同名のメソッドを子クラスで再定義(オーバーライド)することで、同じ呼び出し方でも異なる動作をするポリモーフィズムを実現できます。特殊メソッド(__str__, __eq__, __len__など)を定義すると、組み込み関数や演算子の挙動をカスタマイズできます。
class Animal:
def __init__(self, name):
self._name = name # 慣習的な非公開属性(カプセル化)
def speak(self):
return f"{self._name}が鳴きました"
def __str__(self): # print()やstr()で使われる特殊メソッド
return f"Animal({self._name})"
class Dog(Animal): # 継承
def speak(self): # オーバーライド(ポリモーフィズム)
return f"{self._name}: ワン!"
class Cat(Animal):
def speak(self):
return f"{self._name}: ニャー!"
animals = [Dog("ポチ"), Cat("タマ")]
for a in animals:
print(a.speak()) # 呼び出し方は同じでも動作が異なる=ポリモーフィズム
print(str(Dog("ポチ"))) # Animal(ポチ) -> __str__が呼ばれる
10. デコレータ
デコレータは、既存の関数やメソッドの前後に処理を追加する「関数を受け取り関数を返す関数」です。@デコレータ名という構文糖衣で適用します。ログ出力、実行時間計測、認証チェック、キャッシュ(functools.lru_cacheなど)でよく使われます。
import time
from functools import wraps
def timer(func):
@wraps(func) # 元の関数名やdocstringを保持する
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}の実行時間: {elapsed:.6f}秒")
return result
return wrapper
@timer
def heavy_task(n):
return sum(i * i for i in range(n))
heavy_task(1_000_000)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2) # 結果がキャッシュされ再計算されない
print(fib(30))
11. コンテキストマネージャ(with文)
with文はファイルやロック、DB接続などの「後始末(クローズ)」を確実に行うための構文です。__enter__と__exit__を実装したオブジェクトをコンテキストマネージャと呼び、ブロックを抜けるとき(例外発生時も含む)に自動で__exit__が呼ばれます。contextlib.contextmanagerを使えばジェネレータ関数から簡単に作成できます。
# ファイル操作の定番パターン(自動的にclose()される)
with open("sample.txt", "w", encoding="utf-8") as f:
f.write("Hello, Python!")
from contextlib import contextmanager
@contextmanager
def timer_context(label):
import time
start = time.perf_counter()
try:
yield # withブロックの中身がここで実行される
finally:
print(f"{label}: {time.perf_counter() - start:.6f}秒")
with timer_context("処理A"):
total = sum(range(1_000_000))
class ManagedResource:
def __enter__(self):
print("リソースを確保")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("リソースを解放")
return False # 例外を再送出する(Trueにすると例外を握りつぶす)
with ManagedResource() as r:
print("リソースを使用中")
12. 例外処理(try / except / finally、カスタム例外)
実行時エラーはtry / except / finallyで捕捉します。finallyは例外の有無にかかわらず必ず実行されます。else節は例外が発生しなかったときだけ実行されます。Exceptionを継承した独自クラスを作れば、業務ロジックに応じたカスタム例外を定義できます。
class InsufficientStockError(Exception):
"""在庫不足を表すカスタム例外"""
pass
def withdraw(stock, qty):
if qty > stock:
raise InsufficientStockError(f"在庫が{stock}個しかありません")
return stock - qty
try:
result = withdraw(5, 10)
except InsufficientStockError as e:
print(f"エラーが発生しました: {e}")
except ZeroDivisionError:
print("ゼロ除算エラー")
else:
print("正常に処理されました:", result)
finally:
print("処理終了(必ず実行される)")
13. データ構造の実装(スタックとキュー)
Pythonのlistはスタック(後入れ先出し, LIFO)としてそのまま使えますが(append/pop)、キュー(先入れ先出し, FIFO)をlistで実装すると先頭からのpop(0)がO(n)となり非効率です。collections.deque(両端キュー)を使えば両端の追加・削除がO(1)になり、キューの実装に適しています。
from collections import deque
class Stack:
"""LIFO(後入れ先出し): listのappend/popで実装"""
def __init__(self):
self._data = []
def push(self, value):
self._data.append(value) # O(1)
def pop(self):
return self._data.pop() # O(1)
def is_empty(self):
return len(self._data) == 0
class Queue:
"""FIFO(先入れ先出し): dequeで実装(両端O(1))"""
def __init__(self):
self._data = deque()
def enqueue(self, value):
self._data.append(value) # O(1)
def dequeue(self):
return self._data.popleft() # O(1)(listのpop(0)はO(n)なので非推奨)
def is_empty(self):
return len(self._data) == 0
s = Stack()
s.push(1); s.push(2); s.push(3)
print(s.pop()) # 3(最後に入れたものが先に出る)
q = Queue()
q.enqueue(1); q.enqueue(2); q.enqueue(3)
print(q.dequeue()) # 1(最初に入れたものが先に出る)
14. アルゴリズムと計算量(Big-O記法)
アルゴリズムの効率はデータ量nに対する処理時間の増え方(計算量・Big-O記法)で評価します。探索では、先頭から順に調べる線形探索がO(n)、ソート済み配列の中央値と比較を繰り返す二分探索がO(log n)です。整列では、隣接要素を比較し続けるバブルソートがO(n²)、基準値(ピボット)で分割して再帰的に整列するクイックソートが平均O(n log n)(最悪O(n²))です。
def linear_search(arr, target):
"""線形探索: 計算量 O(n)"""
for i, value in enumerate(arr):
if value == target:
return i
return -1
def binary_search(arr, target):
"""二分探索: 計算量 O(log n)(arrはソート済みが前提)"""
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
def bubble_sort(arr):
"""バブルソート: 計算量 O(n^2)"""
arr = arr.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def quick_sort(arr):
"""クイックソート: 平均 O(n log n)、最悪 O(n^2)"""
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + mid + quick_sort(right)
data = [5, 2, 9, 1, 5, 6]
print(bubble_sort(data)) # [1, 2, 5, 5, 6, 9]
print(quick_sort(data)) # [1, 2, 5, 5, 6, 9]
sorted_data = quick_sort(data)
print(binary_search(sorted_data, 6)) # 4
print(linear_search(data, 9)) # 2
15. 並行処理とGIL(multiprocessing / asyncio)
CPython実装にはGIL(グローバルインタプリタロック)があり、1つのプロセス内では常に1つのスレッドしかPythonバイトコードを実行できません。そのためthreadingはI/O待ちの間だけ効果を発揮し、CPUを使い切る計算処理(CPUバウンド)の並列化には向きません。CPUバウンドな処理を並列化するにはプロセスを複数立ち上げるmultiprocessingを使います(各プロセスが独立したGILを持つ)。一方、ネットワーク通信やファイルI/Oの待ち時間が多い処理(I/Oバウンド)には、1スレッドで多数の処理を切り替えながら効率よく待つasyncio(async/await)が適しています。
import asyncio
from multiprocessing import Pool
# --- I/Oバウンド: asyncio ---
async def fetch_data(name, delay):
print(f"{name}: 取得開始")
await asyncio.sleep(delay) # I/O待ちの間、他のタスクに処理を譲る
print(f"{name}: 取得完了")
return name
async def main():
results = await asyncio.gather(
fetch_data("A", 1),
fetch_data("B", 1),
fetch_data("C", 1),
) # 3つ合計でも約1秒で完了する(並行実行)
print(results)
# asyncio.run(main())
# --- CPUバウンド: multiprocessing ---
def square(n):
return n * n
if __name__ == "__main__":
with Pool(processes=4) as pool:
# 複数プロセスに分散して計算するのでGILの制約を受けない
print(pool.map(square, [1, 2, 3, 4, 5]))
16. モジュール・パッケージ管理
importで標準ライブラリや自作モジュール、外部パッケージを読み込めます。複数のモジュールを1つのディレクトリにまとめ、__init__.pyを置くとパッケージとして扱えます。外部パッケージはpip install パッケージ名でインストールし、プロジェクトごとに依存関係を分離するためにvenvで仮想環境を作るのが一般的です。
import math
from collections import Counter
import numpy as np # pip install numpy でインストールした外部パッケージ
print(math.sqrt(16))
print(Counter(["a", "b", "a", "c", "a"]))
print(np.array([1, 2, 3]).sum())
# ターミナルでの操作例(コマンド自体はPythonコードではない)
# python -m venv .venv 仮想環境を作成
# .venv\Scripts\activate 仮想環境を有効化(Windows)
# pip install requests パッケージをインストール
# pip freeze > requirements.txt 依存関係を書き出す
17. メモリ管理(参照カウントとガベージコレクション)
CPythonは基本的に参照カウント方式でメモリを管理します。オブジェクトを参照する変数の数(参照カウント)が0になった瞬間に、そのオブジェクトは即座に解放されます。ただし、オブジェクト同士が互いを参照し合う「循環参照」は参照カウントだけでは解放できないため、定期的に動作する世代別ガベージコレクタ(GC)が循環参照を検出して回収します。
import sys
import gc
a = [1, 2, 3]
print(sys.getrefcount(a)) # 参照カウントを確認(引数分+1される)
b = a # 参照が増える
print(sys.getrefcount(a))
del b # 参照が減る
# 循環参照の例(互いを参照し合う)
class Node:
def __init__(self):
self.other = None
n1, n2 = Node(), Node()
n1.other = n2
n2.other = n1 # n1とn2が循環参照している
del n1, n2
gc.collect() # 世代別GCが循環参照を検出して回収する
処理速度改善のポイント
Pythonは書きやすい反面、実行速度で不利になりがちです。初心者〜熟練者まで役立つ高速化のコツを紹介します。
- リスト内包表記・ジェネレータ式を使う:通常のforループより内部的に最適化されており高速で、ジェネレータはメモリ使用量も抑えられます。
- 文字列連結は
join()を使う:+=を繰り返すと毎回新しい文字列オブジェクトが生成され非効率です。 - 組み込み関数・標準ライブラリを優先:
sum(),map(),collectionsなどはC言語で実装されており高速です。 - 適切なデータ構造を選ぶ:検索が多い場合はlistではなく
dictやset(平均O(1)検索)を使いましょう。両端の追加・削除にはdequeが有効です。 - 関数結果のキャッシュ:
functools.lru_cacheで同じ引数に対する再計算を避けられます。 - ループ内でのグローバル変数アクセスを避ける:ローカル変数の方が参照速度が速いです。
- CPUバウンドな処理は
multiprocessing、I/Oバウンドな処理はasyncioを活用:GIL(グローバルインタプリタロック)の制約を回避できます。 - 大量の数値計算はNumPyを利用:C言語ベースの実装でPythonネイティブのループより大幅に高速化できます。