発展LESSON 12

この教材で学ぶこと 型・非同期処理・設計パターンをPythonで実装します。

Python 実践 — Kotlin エンジニアのための高速キャッチアップ

対象読者: Kotlin の経験を生かし、対比しながら Python の実践力を身につけたい人。


Kotlin → Python 対応表

Kotlin Python 備考
data class @dataclass / pydantic.BaseModel
suspend fun async def
Flow<T> AsyncGenerator / asyncio.Queue
interface Protocol / ABC
@Inject (Hilt) コンストラクタ注入 or dependency-injector
sealed class Enum + Union 型
?. (null safe) Optional[T] + if x is not None
Room @Dao SQLAlchemy / sqlite3
Retrofit httpx / requests
object (singleton) モジュールレベル変数 or @lru_cache

1. データクラスとバリデーション

Kotlin の data class に慣れているなら pydantic が自然:

# Kotlin:
# data class Book(val id: Long, val title: String, val status: ReadingStatus)

from pydantic import BaseModel
from enum import Enum

class ReadingStatus(str, Enum):
    UNREAD = "unread"
    READING = "reading"
    COMPLETED = "completed"

class Book(BaseModel):
    id: int
    title: str
    author: str
    status: ReadingStatus = ReadingStatus.UNREAD
    isbn: str | None = None  # nullable(Kotlinの String?)

# バリデーション付きで自動生成
book = Book(id=1, title="Clean Architecture", author="Robert Martin", status="reading")
print(book.model_dump())  # dictに変換
print(book.model_dump_json())  # JSONに変換

2. 非同期処理(Coroutines → asyncio)

Kotlin の suspend fun + coroutineScope とほぼ同じ概念:

# Kotlin:
# suspend fun fetchBook(id: Long): Book = withContext(Dispatchers.IO) { api.getBook(id) }
# 
# viewModelScope.launch {
#     val books = awaitAll(
#         async { fetchBook(1) },
#         async { fetchBook(2) }
#     )
# }

import asyncio
import httpx

async def fetch_book(client: httpx.AsyncClient, book_id: int) -> dict:
    """Kotlin の suspend fun に相当"""
    response = await client.get(f"https://api.example.com/books/{book_id}")
    response.raise_for_status()
    return response.json()

async def main():
    async with httpx.AsyncClient() as client:
        # Kotlin の awaitAll に相当(並列実行)
        books = await asyncio.gather(
            fetch_book(client, 1),
            fetch_book(client, 2),
            fetch_book(client, 3),
        )
    return books

asyncio.run(main())

3. Repository パターン(Kotlin の Room 経験を活かす)

# Kotlin:
# interface BookRepository {
#     fun getAllBooks(): Flow<List<Book>>
#     suspend fun insertBook(book: Book): Long
# }

from abc import ABC, abstractmethod
from typing import Protocol

# Protocol = Kotlin の interface
class BookRepository(Protocol):
    async def get_all_books(self) -> list[Book]: ...
    async def insert_book(self, book: Book) -> int: ...
    async def search_books(self, query: str) -> list[Book]: ...

# 実装クラス(Room の RepositoryImpl に相当)
import aiosqlite

class SQLiteBookRepository:
    def __init__(self, db_path: str):
        self.db_path = db_path

    async def get_all_books(self) -> list[Book]:
        async with aiosqlite.connect(self.db_path) as db:
            async with db.execute("SELECT * FROM books ORDER BY updated_at DESC") as cursor:
                rows = await cursor.fetchall()
                return [Book(id=r[0], title=r[1], author=r[2], status=r[3]) for r in rows]

    async def insert_book(self, book: Book) -> int:
        async with aiosqlite.connect(self.db_path) as db:
            cursor = await db.execute(
                "INSERT INTO books (title, author, status) VALUES (?, ?, ?)",
                (book.title, book.author, book.status.value)
            )
            await db.commit()
            return cursor.lastrowid

4. boto3 — AWS 操作(Lambda 経験の延長)

import boto3
from botocore.exceptions import ClientError

# Lambda で使っている requests → boto3 に切り替え
ec2 = boto3.client('ec2', region_name='ap-northeast-1')

def get_running_instances() -> list[dict]:
    """実行中のEC2一覧を取得"""
    paginator = ec2.get_paginator('describe_instances')
    instances = []
    
    for page in paginator.paginate(
        Filters=[{'Name': 'instance-state-name', 'Values': ['running']}]
    ):
        for reservation in page['Reservations']:
            for inst in reservation['Instances']:
                name = next(
                    (t['Value'] for t in inst.get('Tags', []) if t['Key'] == 'Name'),
                    'No Name'
                )
                instances.append({
                    'id': inst['InstanceId'],
                    'type': inst['InstanceType'],
                    'name': name,
                    'private_ip': inst.get('PrivateIpAddress', 'N/A'),
                })
    return instances

# エラーハンドリング(Kotlin の try-catch と同じ構造)
try:
    instances = get_running_instances()
    for inst in instances:
        print(f"{inst['name']:30} {inst['type']:15} {inst['id']}")
except ClientError as e:
    print(f"AWS Error: {e.response['Error']['Code']}: {e.response['Error']['Message']}")

5. 型ヒント実践(Kotlin の型安全に近づける)

from typing import TypeVar, Generic

T = TypeVar('T')

# Kotlin の Result<T> / sealed class に相当
class Result(Generic[T]):
    def __init__(self, value: T | None, error: Exception | None):
        self._value = value
        self._error = error

    @classmethod
    def success(cls, value: T) -> 'Result[T]':
        return cls(value, None)

    @classmethod
    def failure(cls, error: Exception) -> 'Result[T]':
        return cls(None, error)

    @property
    def is_success(self) -> bool:
        return self._error is None

    def get_or_raise(self) -> T:
        if self._error:
            raise self._error
        return self._value

# 使用例
def fetch_config(key: str) -> Result[str]:
    import os
    value = os.environ.get(key)
    if value is None:
        return Result.failure(KeyError(f"環境変数 {key} が未設定"))
    return Result.success(value)

result = fetch_config("DATABASE_URL")
if result.is_success:
    url = result.get_or_raise()

6. CLI ツール作成(click)

import click

@click.group()
def cli():
    """インフラ管理ツール"""
    pass

@cli.command()
@click.option('--region', default='ap-northeast-1', help='AWSリージョン')
@click.option('--env', type=click.Choice(['prod', 'stg', 'dev']), required=True)
def list_instances(region: str, env: str):
    """EC2インスタンス一覧を表示"""
    click.echo(f"Region: {region}, Env: {env}")
    # boto3 で取得...

@cli.command()
@click.argument('instance_id')
@click.confirmation_option(prompt='本当に停止しますか?')
def stop_instance(instance_id: str):
    """EC2インスタンスを停止"""
    click.echo(f"Stopping {instance_id}...")

if __name__ == '__main__':
    cli()
python tool.py list-instances --env prod
python tool.py stop-instance i-abc1234

🔗 次のステップ