Python 기본를 실무 흐름으로 이해하기
Python 설치부터 문법, 자료구조, 컴프리헨션/제너레이터, 데코레이터, 컨텍스트 매니저, 타입 힌트 심화, 동시성(threading/multiprocessing/asyncio), 패키지 관리(uv), 테스트까지 — 입문을 넘어 실무 중급 수준까지 한 번에 정리합니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
Python 설치부터 문법, 자료구조, 컴프리헨션/제너레이터, 데코레이터, 컨텍스트 매니저, 타입 힌트 심화, 동시성(threading/multiprocessing/asyncio), 패키지 관리(uv), 테스트까지 — 입문을 넘어 실무 중급 수준까지 한 번에 정리합니다.
Python 설치부터 문법, 자료구조, 컴프리헨션/제너레이터, 데코레이터, 컨텍스트 매니저, 타입 힌트 심화, 동시성(threading/multiprocessing/asyncio), 패키지 관리(uv), 테스트까지 — 입문을 넘어 실무 중급 수준까지 한 번에 정리합니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
문법보다 요청이 들어와 검증, 처리, 저장, 응답으로 이어지는 경계를 먼저 잡습니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Python 기본를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
Python 기본를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
# 버전 확인
python --version
# 프로젝트 생성
mkdir python-basic && cd python-basic
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\Activate.ps1
# 패키지 설치
python -m pip install --upgrade pip
python -m pip install requests pytest여기서는 변수와 기본 문법을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
name = "AI DevOps"
score = 87
active = True
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "C"
for index in range(3):
print(index, name, grade)
def format_user(name: str, score: int) -> str:
return f"{name}: {score}점"
print(format_user(name, score))| 개념 | 예시 | 실무 기준 |
|---|---|---|
| 문자열 | `str`, f-string | 문자열 조합은 f-string을 기본으로 사용합니다. |
| 숫자 | `int`, `float` | 금액 계산은 float 대신 Decimal을 검토합니다. |
| 조건문 | `if / elif / else` | 복잡한 조건은 함수로 분리합니다. |
| 반복문 | `for`, `while` | 인덱스가 필요하면 enumerate를 사용합니다. |
여기서는 자료구조을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
users = [
{"id": 1, "name": "Ada", "role": "admin"},
{"id": 2, "name": "Linus", "role": "developer"},
{"id": 3, "name": "Grace", "role": "developer"},
]
names = [user["name"] for user in users]
developers = [u for u in users if u["role"] == "developer"]
user_by_id = {user["id"]: user for user in users}
roles = {user["role"] for user in users}
print(names)
print(user_by_id[2]["name"])
print(sorted(roles))| 자료구조 | 특징 | 대표 용도 |
|---|---|---|
| list | 순서 있음, 중복 가능 | 목록, 정렬, 필터링 |
| tuple | 변경 불가 | 좌표, 고정된 반환값 |
| dict | 키-값 조회 | 설정, 인덱스, JSON 형태 데이터 |
| set | 중복 제거 | 태그, 권한, 교집합/차집합 |
여기서는 컴프리헨션 & 제너레이터을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# list/dict/set 컴프리헨션 — 즉시 전체를 메모리에 생성
squares = [n * n for n in range(10)]
even_squares = {n: n * n for n in range(10) if n % 2 == 0}
# 제너레이터 함수 — yield를 만날 때마다 하나씩 반환하고 멈춤
def read_large_file(path: str):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip()
# 제너레이터 표현식 — []대신 ()를 쓰면 지연 생성
total = sum(n * n for n in range(1_000_000)) # 리스트를 만들지 않고 즉시 합산
# itertools로 무한/대용량 스트림 다루기
from itertools import islice
def counter():
n = 0
while True:
yield n
n += 1
first_five = list(islice(counter(), 5))
print(first_five)| 구분 | 평가 시점 | 메모리 | 재순회 |
|---|---|---|---|
| 리스트 컴프리헨션 `[...]` | 즉시 전체 생성 | 전체 데이터 크기만큼 사용 | 여러 번 가능 |
| 제너레이터 표현식 `(...)` | 값을 요청할 때마다 하나씩 생성 | 항목 1개 분량만 사용 | 1회성 (소진되면 재사용 불가) |
여기서는 함수와 타입 힌트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from dataclasses import dataclass
@dataclass
class Order:
id: int
amount: float
paid: bool = False
def total_paid(orders: list[Order]) -> float:
return sum(order.amount for order in orders if order.paid)
def normalize_name(value: str) -> str:
return value.strip().title()
orders = [Order(1, 12000, True), Order(2, 8000, False)]
print(total_paid(orders))여기서는 데코레이터을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import time
import functools
def retry(times: int = 3):
"""인자를 받는 데코레이터는 함수를 한 겹 더 감싼 팩토리 구조입니다."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error: Exception | None = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
last_error = exc
print(f"[재시도 {attempt}/{times}] {func.__name__} 실패: {exc}")
raise last_error
return wrapper
return decorator
@retry(times=3)
def call_flaky_api() -> str:
if time.time() % 2 < 1:
raise ConnectionError("일시적 네트워크 오류")
return "ok"
# 내장 데코레이터
class Circle:
def __init__(self, radius: float) -> None:
self._radius = radius
@property
def area(self) -> float:
return 3.14159 * self._radius ** 2
@staticmethod
def unit() -> "Circle":
return Circle(1)
@functools.lru_cache(maxsize=None)
def fibonacci(n: int) -> int:
return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)| 데코레이터 | 용도 |
|---|---|
| `@property` | 메서드를 속성처럼 접근하게 만듦 (getter) |
| `@staticmethod` | self 없이 클래스 네임스페이스만 빌려 쓰는 함수 |
| `@classmethod` | 첫 인자로 클래스 자체(cls)를 받는 대체 생성자 등 |
| `@functools.lru_cache` | 동일 인자 호출 결과를 캐싱해 재계산 방지 |
| `@functools.wraps` | 커스텀 데코레이터 안에서 원본 함수의 메타데이터 보존 |
여기서는 모듈과 패키지을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
python-basic/
pyproject.toml
src/
app/
__init__.py
calculator.py
cli.py
tests/
test_calculator.pydef add(a: int, b: int) -> int:
return a + bfrom app.calculator import add
def main() -> None:
print(add(2, 3))
if __name__ == "__main__":
main()여기서는 파일 입출력을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from pathlib import Path
import json
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)
users = [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Linus"}]
path = data_dir / "users.json"
path.write_text(json.dumps(users, ensure_ascii=False, indent=2), encoding="utf-8")
loaded = json.loads(path.read_text(encoding="utf-8"))
for user in loaded:
print(user["id"], user["name"])여기서는 컨텍스트 매니저 & 자원 관리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import time
from contextlib import contextmanager
# 1) 클래스 기반 컨텍스트 매니저
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
elapsed = time.perf_counter() - self._start
print(f"소요 시간: {elapsed:.3f}s")
return False # 예외를 삼키지 않고 그대로 전파
with Timer():
time.sleep(0.1)
# 2) 제너레이터 기반 컨텍스트 매니저 — 훨씬 짧게 같은 역할 구현
@contextmanager
def timer():
start = time.perf_counter()
try:
yield
finally:
print(f"소요 시간: {time.perf_counter() - start:.3f}s")
with timer():
time.sleep(0.1)
# 3) 여러 자원을 동시에 정리
with open("in.txt", encoding="utf-8") as src, open("out.txt", "w", encoding="utf-8") as dst:
dst.write(src.read().upper())여기서는 예외 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from pathlib import Path
def read_config(path: str) -> dict[str, str]:
file = Path(path)
if not file.exists():
raise FileNotFoundError(f"config not found: {path}")
result: dict[str, str] = {}
for line in file.read_text(encoding="utf-8").splitlines():
if not line.strip() or line.startswith("#"):
continue
key, value = line.split("=", 1)
result[key.strip()] = value.strip()
return result
try:
config = read_config("app.env")
except FileNotFoundError as exc:
print(f"설정 오류: {exc}")여기서는 클래스와 객체지향을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from dataclasses import dataclass
@dataclass
class Account:
owner: str
balance: int = 0
def deposit(self, amount: int) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
self.balance += amount
def withdraw(self, amount: int) -> None:
if amount > self.balance:
raise ValueError("insufficient balance")
self.balance -= amount
account = Account("Ada")
account.deposit(10000)
account.withdraw(3000)
print(account.balance)여기서는 타입 힌트 심화을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from typing import TypedDict, Protocol, Literal
# 값이 없을 수도 있음을 명시 (Python 3.10+에서는 Optional[str] 대신 str | None)
def find_user(user_id: int) -> str | None:
return "Ada" if user_id == 1 else None
# 여러 타입 중 하나
def parse_amount(value: int | str) -> float:
return float(value)
# JSON처럼 구조가 정해진 dict 타입
class UserPayload(TypedDict):
id: int
name: str
role: Literal["admin", "developer", "viewer"]
def greet(user: UserPayload) -> str:
return f"{user['name']}({user['role']})님 환영합니다"
# 상속 없이 "이 메서드가 있으면 된다"는 구조적 타이핑
class SupportsTotal(Protocol):
def total(self) -> float: ...
def print_total(item: SupportsTotal) -> None:
print(f"합계: {item.total()}")| 도구 | 용도 |
|---|---|
| `X | None` (`Optional[X]`) | 값이 없을 수도 있음을 명시 |
| `A | B` (`Union[A, B]`) | 여러 타입 중 하나를 허용 |
| `TypedDict` | dict의 키·값 타입을 JSON 구조체처럼 명시 |
| `Protocol` | 상속 없이 필요한 메서드만 있으면 되는 구조적 타이핑 |
| `Literal["a", "b"]` | 정해진 값들 중 하나로 제한 |
여기서는 표준 라이브러리 활용을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import argparse
import csv
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
parser = argparse.ArgumentParser()
parser.add_argument("--file", default="orders.csv")
args = parser.parse_args()
path = Path(args.file)
total = 0
with path.open(encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
total += int(row["amount"])
logging.info("total amount: %s", total)여기서는 동시성 기초 (threading/multiprocessing/asyncio)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# I/O bound: 동시에 여러 스레드로 대기
def download(url: str) -> str:
time.sleep(0.5) # 네트워크 대기를 흉내
return f"downloaded: {url}"
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(download, ["a.com", "b.com", "c.com"]))
# CPU bound: 여러 프로세스로 진짜 병렬 계산
def heavy_square(n: int) -> int:
return sum(i * i for i in range(n))
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(heavy_square, [10_000_000] * 4))
# asyncio: 단일 스레드에서 수많은 대기를 동시에 처리
async def fetch(session_id: int) -> str:
await asyncio.sleep(0.5) # 반드시 asyncio용 sleep — time.sleep은 이벤트 루프를 멈춤
return f"session {session_id} done"
async def main() -> None:
results = await asyncio.gather(*(fetch(i) for i in range(10)))
print(results)
asyncio.run(main())| 상황 | 적합한 도구 | 이유 |
|---|---|---|
| 네트워크 요청을 대량으로 동시 호출 (I/O bound) | asyncio | 단일 스레드로 수천 개를 동시 대기, 컨텍스트 스위칭 비용이 가장 작음 |
| 기존 동기 라이브러리로 파일·DB I/O | threading | I/O 대기 중에는 GIL이 풀리므로 여러 스레드가 동시에 대기 가능 |
| 이미지 처리, 수치 계산 (CPU bound) | multiprocessing | 프로세스마다 별도의 인터프리터·GIL을 가져 진짜 병렬 실행 |
여기서는 패키지 관리 심화 (uv)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
[project]
name = "python-basic"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"requests>=2.32",
]
[dependency-groups]
dev = ["pytest>=8.0"]# 프로젝트 초기화 (venv + pyproject.toml 자동 생성)
uv init python-basic && cd python-basic
# 의존성 추가 (설치 + pyproject.toml + uv.lock 동시 갱신)
uv add requests
uv add --dev pytest
# lock 기준으로 정확히 동일한 환경 재현
uv sync
# venv를 직접 activate하지 않고 lock된 환경에서 바로 실행
uv run python -m app.cli
uv run pytest| 도구 | 특징 |
|---|---|
| pip + requirements.txt | 가장 단순하지만 정식 lock 파일이 없음 (pip freeze로 흉내) |
| pip-tools | requirements.in → requirements.txt로 lock 파일 생성 |
| Poetry | pyproject.toml 기반, lock 파일과 빌드·배포까지 통합 |
| uv | Rust로 작성되어 매우 빠름, venv·설치·lock을 하나의 CLI로 통합 — 최근 표준으로 빠르게 확산 |
여기서는 테스트와 프로젝트 구조을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import pytest
from app.calculator import add
def test_add():
assert add(2, 3) == 5
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
])
def test_add_cases(a: int, b: int, expected: int):
assert add(a, b) == expectedimport pytest
from app.weather import WeatherClient
@pytest.fixture
def client() -> WeatherClient:
# 각 테스트마다 새로 생성되는 재사용 가능한 준비물
return WeatherClient(api_key="test-key")
def test_parses_temperature(client: WeatherClient, monkeypatch: pytest.MonkeyPatch):
def fake_get(url: str) -> dict:
return {"temp": 21.5}
# 실제 네트워크 호출 대신 가짜 함수로 대체
monkeypatch.setattr(client, "_get", fake_get)
assert client.current_temperature("Seoul") == 21.5| 개념 | 역할 |
|---|---|
| `fixture` | 테스트마다 필요한 준비물(DB 연결, 객체)을 만들고 뒷정리까지 담당 |
| `monkeypatch` / `unittest.mock` | 외부 API·시간·환경변수처럼 통제하기 어려운 의존성을 가짜로 대체 |
| `pytest.mark.parametrize` | 같은 로직을 여러 입력값으로 반복 검증 |
| `pytest --cov=app` | coverage.py 연동으로 테스트가 커버하지 못한 코드 라인 확인 |
Python 기본 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | Python 기본 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 Python 기본 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Python 기본 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |