PYTHON · 심층 가이드
Python 기초 문법 완전 정리
타입 힌트가 런타임에 무엇을 하고 하지 않는지부터 f-string 포매팅, 컴프리헨션, match/case, Counter와 defaultdict까지 Python 문법의 실제 동작을 정리합니다.
Python의 문법은 얕아 보이지만 밑에 깔린 규칙은 다른 언어와 꽤 다릅니다. 변수는 값을 담는 상자가 아니라 객체에 붙는 이름표이고, 타입 힌트는 검사기를 위한 메타데이터일 뿐 인터프리터가 강제하지 않습니다. x: int = "문자열"은 그대로 실행됩니다. 이 사실을 모르면 힌트를 믿고 방어 코드를 빼거나, 반대로 힌트를 아예 장식으로 취급하게 됩니다. 힌트는 mypy나 pyright가 읽을 때 비로소 값을 하고, 실행 시간의 안전은 여전히 코드가 책임집니다.
이 가이드의 항목들은 세 갈래로 묶입니다. 변수 & 타입 힌트에서 시작한 흐름이 타입 별칭, Literal 타입, 유니온 타입, 그리고 3.12의 PEP 695 타입 파라미터로 이어지며 표현력을 넓힙니다. 데이터를 다루는 축에서는 슬라이싱 심화와 리스트 컴프리헨션 & 제너레이터가 짝을 이루고, 여기에 딕셔너리 심화, Counter 카운터, defaultdict가 붙어 집계 코드를 짧게 만듭니다. 제어 흐름 축은 조건문 & 반복문에서 walrus 연산자와 match/case 패턴 매칭으로 확장됩니다.
match에서 가장 많이 밟는 지뢰가 있습니다. case RED:처럼 점이 없는 단순 이름은 상수와 비교하는 게 아니라 무조건 매칭되고 그 이름에 값을 바인딩하는 캡처 패턴입니다. 아래 case들이 전부 죽은 코드가 됩니다. 상수를 비교하려면 case Color.RED:처럼 점 붙은 참조를 쓰거나 리터럴을 직접 적어야 합니다. defaultdict도 비슷하게 조용합니다. 없는 키를 읽기만 해도 기본값이 생성되어 저장되므로, 조회 목적이면 get()이나 in을 쓰는 편이 낫습니다.
01변수 & 타입 힌트
Python의 동적 타이핑과 Python 3.5+의 타입 힌트(Type Hints)로 코드 안정성을 높입니다.
Python code
# 동적 타이핑 — 타입 선언 불필요
name = "Python"
version = 3.12
is_awesome = True
count = 100
# 타입 힌트 (Python 3.5+) — 런타임 강제 아님, IDE/mypy용
salary: int = 50_000_000
user_name: str = "Alice"
ratio: float = 0.75
active: bool = True
# 여러 변수 동시 할당
x, y, z = 1, 2, 3
a = b = c = 0
# 타입 확인
print(type(name)) # <class 'str'>
print(type(version)) # <class 'float'>
print(isinstance(name, str)) # True
# 언더스코어로 숫자 가독성
population = 51_000_000
pi = 3.141_592_653
# None — null에 해당
result = None
print(result is None) # True (== 대신 is 사용)None 비교는 ==가 아닌 is/is not을 사용하세요. 일부 클래스에서 ==가 예상치 못한 결과를 줄 수 있습니다.
True/False는 대문자로 시작합니다. true/false는 NameError입니다. Java/Kotlin 개발자가 자주 하는 실수입니다.
02문자열 — f-string & 메서드
Python의 강력한 문자열 처리. f-string, 메서드 체이닝, 슬라이싱을 완전히 익힙니다.
Python code
name = " python developer "
score = 98.765
# f-string 포맷팅 (Python 3.6+)
print(f"이름: {name.strip()}")
print(f"점수: {score:.2f}") # 소수점 2자리
print(f"퍼센트: {score / 100:.1%}") # 백분율
print(f"{'중앙정렬':^20}") # 패딩/정렬
print(f"{1000000:,}") # 천단위 콤마
# 주요 문자열 메서드
s = "Hello, Python World"
print(s.upper()) # HELLO, PYTHON WORLD
print(s.lower()) # hello, python world
print(s.replace("Python", "Kotlin"))
print(s.split(", ")) # ['Hello', 'Python World']
print(s.startswith("Hello")) # True
print(s.strip()) # 앞뒤 공백 제거
print(s.find("Python")) # 7 (인덱스)
print(s.count("l")) # 3
# 슬라이싱 [start:stop:step]
text = "Hello World"
print(text[0:5]) # Hello
print(text[-5:]) # World
print(text[::-1]) # dlroW olleH (역순)
# join
words = ["Python", "is", "awesome"]
print(" ".join(words)) # Python is awesomef-string에서 ! 변환 플래그를 사용할 수 있습니다: !r(repr), !s(str), !a(ascii).
str.find()는 없으면 -1을 반환합니다. str.index()는 없으면 ValueError를 발생시킵니다. 용도에 맞게 선택하세요.
03조건문 & 반복문
Python의 if/elif/else, for, while 문법과 else 절.
Python code
score = 85
# if / elif / else
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
# 한 줄 조건식 (삼항 연산자)
result = "합격" if score >= 60 else "불합격"
# 연속 비교 (Python만의 특징)
if 80 <= score < 90:
print("B등급")
# for + range
for i in range(5): # 0 1 2 3 4
print(i, end=" ")
for i in range(1, 10, 2): # 1 3 5 7 9
print(i, end=" ")
# enumerate — 인덱스와 값 동시에
fruits = ["🍎", "🍌", "🍇"]
for idx, fruit in enumerate(fruits, start=1):
print(f"{idx}: {fruit}")
# zip — 두 리스트 병렬 순회
names = ["Alice", "Bob"]
scores = [95, 87]
for name, sc in zip(names, scores):
print(f"{name}: {sc}")
# for-else: break 없이 완료되면 else 실행
for n in range(2, 10):
if 7 % n == 0 and n != 7:
print(f"{n}는 7의 약수")
break
else:
print("7은 소수")for-else의 else는 루프가 break 없이 정상 완료됐을 때 실행됩니다. 검색 알고리즘에서 "찾지 못했을 때" 처리에 유용합니다.
Python에는 switch/case가 없었지만 3.10+에서 match/case가 추가됐습니다. 하위 버전 호환이 필요하면 딕셔너리 기반 디스패치를 사용하세요.
04리스트 컴프리헨션 & 제너레이터
Python의 가장 Pythonic한 기법. 간결하고 빠른 컬렉션 생성.
Python code
# 리스트 컴프리헨션
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 조건 필터
evens = [x for x in range(20) if x % 2 == 0]
# 중첩 컴프리헨션
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
flat = [x for row in matrix for x in row]
# 딕셔너리 컴프리헨션
words = ["python", "kotlin", "golang"]
word_len = {w: len(w) for w in words}
# {'python': 6, 'kotlin': 6, 'golang': 6}
# 세트 컴프리헨션 (중복 제거)
letters = {c for c in "programming" if c not in "aeiou"}
# 제너레이터 표현식 (메모리 효율)
gen = (x**2 for x in range(1_000_000)) # 즉시 계산 안 함
print(next(gen)) # 0
print(next(gen)) # 1
# sum()에 제너레이터 전달 (괄호 하나로)
total = sum(x**2 for x in range(100))
# walrus operator := (Python 3.8+)
data = [1, -2, 3, -4, 5]
positive = [y for x in data if (y := x * 2) > 0]
print(positive) # [2, 6, 10]제너레이터 표현식은 (), 리스트 컴프리헨션은 []입니다. 큰 데이터를 한 번만 순회하면 제너레이터가 메모리 효율적입니다.
중첩이 3단계 이상 되면 가독성이 급격히 떨어집니다. 이 경우 일반 for 루프나 함수로 분리하세요.
05match/case 패턴 매칭 (Python 3.10+)
구조적 패턴 매칭. 단순 switch 이상의 강력한 패턴 분해를 제공합니다.
Python code
# 기본 match/case
def http_status(status):
match status:
case 200: return "OK"
case 404: return "Not Found"
case 500 | 503: return "Server Error" # OR 패턴
case _: return "Unknown"
# 구조 분해 패턴
def process_command(command):
match command.split():
case ["quit"]:
return "종료"
case ["go", direction]:
return f"{direction}(으)로 이동"
case ["go", direction, speed]:
return f"{direction}으로 {speed} 속도로 이동"
case _:
return "알 수 없는 명령"
# 클래스 패턴
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def where_is(point):
match point:
case Point(x=0, y=0): return "원점"
case Point(x=0, y=y): return f"Y축 위 {y}"
case Point(x=x, y=0): return f"X축 위 {x}"
case Point(x=x, y=y): return f"({x}, {y})"
print(where_is(Point(0, 5))) # Y축 위 5
print(process_command("go north fast"))case에서 변수에 바인딩하려면 이름 앞에 아무것도 붙이지 않으면 됩니다. 상수와 비교하려면 Enum이나 클래스.속성 형태를 사용하세요.
case status_code처럼 단독 이름을 쓰면 상수 비교가 아니라 변수 바인딩이 됩니다. 상수와 비교하려면 case MyEnum.VALUE 형태를 사용하세요.
06Walrus 연산자 & 구조적 패턴
Python 3.8+ 대입 표현식(:=)으로 중복 계산을 제거합니다.
Python code
import re
# walrus := — 할당과 평가를 동시에
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# while 루프에서 유용
numbers = iter(data)
while (n := next(numbers, None)) is not None:
if n % 2 == 0:
print(f"짝수: {n}")
# 정규식 매치 결과 재사용
text = "Python 3.12 출시"
if m := re.search(r"Python (\d+\.\d+)", text):
print(f"버전: {m.group(1)}") # 3.12
# 컴프리헨션에서 중복 계산 제거
def heavy(x): return x * x
# 나쁜 예: heavy() 두 번 호출
results_bad = [heavy(x) for x in range(10) if heavy(x) > 25]
# 좋은 예: walrus로 한 번만 호출
results = [y for x in range(10) if (y := heavy(x)) > 25]
print(results) # [36, 49, 64, 81]:=는 가독성이 중요한 코드에서는 주의해서 사용하세요. 남용하면 코드를 읽기 어렵게 만들 수 있습니다.
Walrus 연산자로 할당된 변수는 컴프리헨션 바깥 스코프에도 영향을 줍니다. 의도치 않은 변수 누출에 주의하세요.
07Descriptor & __get__ __set__
Python의 속성 접근 메커니즘을 제어하는 디스크립터 프로토콜.
Python code
class Validator:
"""값 검증 디스크립터"""
def __set_name__(self, owner, name):
self.name = name
self.private = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self # 클래스에서 접근 시
return getattr(obj, self.private, None)
def __set__(self, obj, value):
self._validate(value)
setattr(obj, self.private, value)
def _validate(self, value): ...
class PositiveInt(Validator):
def _validate(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f"{self.name}은 양의 정수여야 함: {value}")
class RangeFloat(Validator):
def __init__(self, lo, hi):
self.lo, self.hi = lo, hi
def _validate(self, value):
if not (self.lo <= value <= self.hi):
raise ValueError(f"{self.name}은 {self.lo}~{self.hi} 범위: {value}")
class Product:
price = PositiveInt()
quantity = PositiveInt()
discount = RangeFloat(0.0, 1.0)
def __init__(self, price, quantity, discount):
self.price = price
self.quantity = quantity
self.discount = discount
@property
def total(self):
return self.price * self.quantity * (1 - self.discount)
p = Product(1000, 5, 0.1)
print(p.total) # 4500.0
try:
p.price = -100
except ValueError as e:
print(e)__set_name__은 Python 3.6+에서 클래스 정의 시 자동으로 호출됩니다. 디스크립터가 어느 클래스의 어떤 이름으로 사용되는지 알 수 있습니다.
__get__에서 obj is None 체크를 하지 않으면 클래스에서 직접 접근할 때(Product.price) 오류가 발생합니다.
08PEP 695 타입 파라미터 (3.12+)
type X = ... 문법으로 타입 별칭과 제네릭 정의
Python code
<span class="cm">// PEP 695 타입 파라미터 (3.12+) 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("PEP 695 타입 파라미터 (3.12+)") }PYTHON 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
09match-case 패턴 매칭 (Python 3.10+)
구조적 패턴 매칭은 값, 타입, 구조를 한 번에 분해하고 매칭합니다. 단순 switch-case를 넘어 중첩 객체, 시퀀스, 매핑 등 복잡한 데이터 구조를 우아하게 처리할 수 있습니다.
Python code
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
def describe_shape(shape):
match shape:
# 리터럴 매칭
case 0:
return "영(zero)"
# 클래스 패턴 + 가드
case Point(x=0, y=0):
return "원점"
case Point(x, y) if x == y:
return f"대각선 위의 점 ({x}, {y})"
case Point(x, y):
return f"점 ({x}, {y})"
# 중첩 패턴
case Circle(center=Point(0, 0), radius=r):
return f"원점 중심 원, 반지름={r}"
case Circle(center=c, radius=r) if r > 10:
return f"큰 원: 중심={c}, 반지름={r}"
# 시퀀스 패턴
case [x]:
return f"단일 원소: {x}"
case [x, y, *rest]:
return f"첫 둘: {x}, {y} (나머지 {len(rest)}개)"
# 매핑 패턴
case {"type": "user", "name": name, **extra}:
return f"사용자: {name}, 추가정보: {extra}"
# 와일드카드
case _:
return "알 수 없는 형태"
# 테스트
shapes = [
Point(0, 0), Point(3, 3), Point(1, 2),
Circle(Point(0, 0), 5), Circle(Point(1, 1), 15),
[42], [1, 2, 3, 4, 5],
{"type": "user", "name": "홍길동", "age": 30},
"기타",
]
for s in shapes:
print(f"{str(s):>45} → {describe_shape(s)}")case 절의 변수는 바인딩(캡처)됩니다. 상수와 비교하려면 점 표기법(Status.ACTIVE)을 사용하거나 가드(if)를 추가하세요.
case x:는 "변수 x에 바인딩"을 의미하지 "x와 비교"가 아닙니다. case 변수명:은 항상 매칭되므로 상수 비교에는 리터럴이나 case _ if val == x:를 사용하세요.
10슬라이싱 심화
Python 슬라이싱의 고급 기법으로 step 파라미터, 음수 인덱스, 다차원 슬라이싱까지 다룹니다.
Python code
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# step을 이용한 슬라이싱
print(data[::2]) # [0, 2, 4, 6, 8] 짝수 인덱스
print(data[1::2]) # [1, 3, 5, 7, 9] 홀수 인덱스
print(data[::-1]) # 역순
# slice 객체로 재사용
every_third = slice(None, None, 3)
print(data[every_third]) # [0, 3, 6, 9]
# 슬라이스 대입
data[2:5] = [20, 30, 40]
print(data) # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]
# 길이가 다른 대입 (확장/축소)
data[2:5] = [99]
print(data) # [0, 1, 99, 5, 6, 7, 8, 9]slice 객체를 변수에 저장하면 여러 시퀀스에 동일한 슬라이싱을 재사용할 수 있습니다.
data[::-1]은 새 리스트를 생성합니다. 원본을 뒤집으려면 data.reverse()를 사용하세요.
11문자열 메서드
자주 쓰이는 문자열 메서드를 정리합니다. str은 불변(immutable)이므로 항상 새 문자열을 반환합니다.
Python code
text = " Hello, Python World! "
# 공백 제거
print(text.strip()) # "Hello, Python World!"
print(text.lstrip()) # "Hello, Python World! "
# 검색
print(text.find("Python")) # 9 (인덱스)
print(text.count("l")) # 3
print("Python" in text) # True
# 변환
print(text.strip().upper()) # "HELLO, PYTHON WORLD!"
print(text.strip().title()) # "Hello, Python World!"
print(text.strip().replace("World", "세계"))
# 분할과 결합
words = "a,b,c,d".split(",")
print(words) # ['a', 'b', 'c', 'd']
print("-".join(words)) # "a-b-c-d"
# 판별 메서드
print("12345".isdigit()) # True
print("hello".isalpha()) # Truestr.removeprefix()와 str.removesuffix()는 Python 3.9+에서 사용 가능한 편리한 메서드입니다.
str.replace()는 원본을 변경하지 않고 새 문자열을 반환합니다. 결과를 변수에 재할당해야 합니다.
12숫자 포맷
f-string과 format()을 사용한 숫자 포맷팅 기법입니다. 천 단위 구분, 소수점 자릿수, 퍼센트 등을 다룹니다.
Python code
pi = 3.141592653589793
big = 1234567890
# 소수점 자릿수
print(f"{pi:.2f}") # 3.14
print(f"{pi:.4f}") # 3.1416
# 천 단위 구분
print(f"{big:,}") # 1,234,567,890
print(f"{big:_}") # 1_234_567_890
# 퍼센트
ratio = 0.8567
print(f"{ratio:.1%}") # 85.7%
# 진법 변환
n = 255
print(f"{n:b}") # 11111111 (2진)
print(f"{n:o}") # 377 (8진)
print(f"{n:x}") # ff (16진)
# 패딩과 정렬
for i in range(1, 4):
print(f"{i:>5}: {'■' * i}")= 채움 문자와 정렬을 결합하면 f"{val:*>10}"처럼 커스텀 패딩이 가능합니다.
f"{val:,}"는 float에도 적용되지만, 소수점 이하에도 쉼표가 적용될 수 있으므로 f"{val:,.2f}"처럼 명시하세요.
13날짜/시간
datetime 모듈로 날짜와 시간을 다루는 기본 방법입니다. 생성, 포맷, 연산을 포함합니다.
Python code
from datetime import datetime, date, timedelta, timezone
# 현재 시간
now = datetime.now()
print(f"현재: {now:%Y-%m-%d %H:%M:%S}")
# 특정 날짜 생성
birthday = date(2000, 3, 15)
print(f"생일: {birthday:%Y년 %m월 %d일}")
# 날짜 연산
delta = date.today() - birthday
print(f"태어난 지 {delta.days}일")
future = datetime.now() + timedelta(days=30, hours=5)
print(f"30일 5시간 후: {future:%Y-%m-%d %H:%M}")
# UTC 시간대
utc_now = datetime.now(timezone.utc)
kst = timezone(timedelta(hours=9))
kst_now = utc_now.astimezone(kst)
print(f"한국 시간: {kst_now:%H:%M:%S %Z}")시간대를 다룰 때는 항상 timezone-aware datetime을 사용하세요. naive datetime은 시간대 변환 시 버그의 원인이 됩니다.
datetime.now()는 시간대 정보가 없는 naive datetime을 반환합니다. 서버 간 통신에는 datetime.now(timezone.utc)를 사용하세요.
14enum 열거형
Enum으로 관련된 상수를 그룹화하여 타입 안전성과 가독성을 높입니다.
Python code
from enum import Enum, auto, IntEnum
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
# 사용법
print(Color.RED) # Color.RED
print(Color.RED.name) # RED
print(Color.RED.value) # 1
# 비교와 반복
if Color.RED is Color.RED:
print("같은 멤버입니다")
for c in Color:
print(f"{c.name}: {c.value}")
# IntEnum: 정수 비교 가능
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH > Priority.LOW) # True
print(Priority.HIGH == 3) # Trueauto()는 값을 자동 할당합니다. 값이 중요하지 않은 경우 사용하면 편리합니다.
일반 Enum은 정수와 비교할 수 없습니다. 정수 비교가 필요하면 IntEnum을 사용하세요.
15타입 별칭
Python 3.10+의 TypeAlias와 3.12+의 type 문으로 복잡한 타입에 별칭을 부여합니다.
Python code
from typing import TypeAlias
# Python 3.10+ TypeAlias
Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]
# 복잡한 타입에 별칭 부여
JsonValue: TypeAlias = (
str | int | float | bool
| None | list["JsonValue"]
| dict[str, "JsonValue"]
)
def scale(v: Vector, factor: float) -> Vector:
return [x * factor for x in v]
def transpose(m: Matrix) -> Matrix:
return [list(row) for row in zip(*m)]
# Python 3.12+ type 문 (더 간결)
# type Point = tuple[float, float]
# type Callback = Callable[[int], str]
v: Vector = [1.0, 2.0, 3.0]
print(scale(v, 2)) # [2.0, 4.0, 6.0]타입 별칭은 복잡한 제네릭 타입을 읽기 쉽게 만들어 코드 리뷰 시 이해도를 높입니다.
단순 할당 Vector = list[float]은 런타임에 실제 리스트를 만들 수 있으므로 TypeAlias를 명시하세요.
16TypeVar 제네릭
TypeVar로 제네릭 함수와 클래스를 만들어 타입 안전한 코드를 작성합니다.
Python code
from typing import TypeVar, Generic
T = TypeVar('T')
N = TypeVar('N', int, float) # 제한된 TypeVar
# 제네릭 함수
def first(items: list[T]) -> T:
return items[0]
print(first([1, 2, 3])) # 1 (int)
print(first(["a", "b"])) # "a" (str)
# 제한된 TypeVar
def add_nums(a: N, b: N) -> N:
return a + b
print(add_nums(1, 2)) # 3
print(add_nums(1.5, 2.3)) # 3.8
# 제네릭 클래스 (Python 3.12+는 class Box[T]: 가능)
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
def get(self) -> T:
return self.item
box = Box(42)
print(box.get()) # 42Python 3.12+에서는 def first[T](items: list[T]) -> T: 구문으로 더 간결하게 작성할 수 있습니다.
TypeVar("T")의 문자열 인자는 반드시 변수명과 같아야 합니다. T = TypeVar("U")는 혼란을 일으킵니다.
17Literal 타입
Literal로 함수 인자에 허용되는 구체적인 값을 제한하여 타입 안전성을 높입니다.
Python code
from typing import Literal, get_args
Mode = Literal["read", "write", "append"]
def open_file(path: str, mode: Mode) -> str:
return f"{path}를 {mode} 모드로 열기"
# 정상 호출
print(open_file("data.txt", "read"))
# 타입 체커가 오류 감지 (런타임은 통과)
# open_file("data.txt", "delete") # mypy 에러!
# Literal 값 추출
print(get_args(Mode)) # ('read', 'write', 'append')
# Literal과 오버로드 결합
from typing import overload
@overload
def process(action: Literal["count"]) -> int: ...
@overload
def process(action: Literal["list"]) -> list: ...
def process(action):
if action == "count":
return 42
return [1, 2, 3]
print(process("count")) # 42get_args(Mode)로 Literal에 정의된 모든 허용 값을 튜플로 추출할 수 있습니다.
Literal은 런타임에 값을 검증하지 않습니다. 런타임 검증이 필요하면 별도 조건문을 추가하세요.
18Final 상수
Final로 재할당 불가 변수를 선언하여 실수로 값이 변경되는 것을 방지합니다.
Python code
from typing import Final, final
# Final 변수 — 재할당 금지
MAX_RETRIES: Final = 3
API_URL: Final[str] = "https://api.example.com"
# mypy가 다음을 에러로 감지
# MAX_RETRIES = 5 # Cannot assign to final name
# Final과 클래스
class Config:
DEBUG: Final = False
VERSION: Final[str] = "1.0.0"
# @final 데코레이터 — 오버라이드 금지
@final
def get_version(self) -> str:
return self.VERSION
# @final 클래스 — 상속 금지
@final
class Singleton:
_instance = None
print(f"MAX_RETRIES={MAX_RETRIES}")
print(f"VERSION={Config.VERSION}")Final은 상수 의도를 명확히 하고 타입 체커가 실수를 잡아줍니다. ALL_CAPS 관례와 함께 사용하세요.
Final은 런타임에 재할당을 막지 않습니다. 실제 불변성은 타입 체커(mypy)에서만 보장됩니다.
19유니온 타입
Python 3.10+의 | 연산자로 여러 타입을 허용하는 유니온 타입을 간결하게 표현합니다.
Python code
# Python 3.10+ 파이프 문법
def process(value: int | str) -> str:
if isinstance(value, int):
return f"숫자: {value * 2}"
return f"문자: {value.upper()}"
print(process(5)) # 숫자: 10
print(process("hello")) # 문자: HELLO
# Optional은 X | None 의 단축
def find_user(user_id: int) -> str | None:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
result = find_user(1)
if result is not None:
print(result.upper()) # ALICE
# isinstance에서도 사용 (3.10+)
def check(val: object) -> str:
if isinstance(val, int | str):
return f"int 또는 str: {val}"
return "기타 타입"
print(check(42)) # int 또는 str: 42
print(check([1, 2])) # 기타 타입X | None은 Optional[X]와 동일하지만 더 직관적입니다. Python 3.10+에서는 파이프 문법을 권장합니다.
Python 3.9 이하에서는 int | str 문법을 사용할 수 없습니다. Union[int, str]을 사용하세요.
20walrus 연산자
Python 3.8+의 := (바다코끼리) 연산자로 표현식 안에서 변수에 값을 할당합니다.
Python code
# while 루프에서 활용
import re
data = ["apple 3", "banana 5", "", "cherry 2"]
results = []
for line in data:
if match := re.match(r"(\w+) (\d+)", line):
name, count = match.groups()
results.append((name, int(count)))
print(results) # [('apple', 3), ('banana', 5), ('cherry', 2)]
# 리스트 컴프리헨션에서 중복 계산 방지
values = [1, 4, 9, 16, 25]
filtered = [sqrt for v in values
if (sqrt := v ** 0.5) > 2]
print(filtered) # [3.0, 4.0, 5.0]
# any/all과 함께
names = ["Alice", "Bob", "Charlie"]
if found := next((n for n in names if len(n) > 4), None):
print(f"긴 이름 발견: {found}") # Alicewalrus 연산자는 값 계산과 조건 검사를 한 줄에 처리할 때 유용합니다.
:=를 남용하면 가독성이 떨어집니다. 단순 할당에는 일반 =을 사용하세요.
21f-string 심화
f-string의 고급 기능으로 표현식, 포맷 스펙, 디버깅 출력, 중첩 등을 다룹니다.
Python code
import datetime
name = "Python"
version = 3.12
# 디버깅 출력 (3.8+)
print(f"{name = }") # name = 'Python'
print(f"{version = :.1f}") # version = 3.1
# 표현식과 메서드 호출
items = [1, 2, 3]
print(f"합계: {sum(items)}")
print(f"대문자: {name.upper()!r}") # 'PYTHON'
# 변환 플래그
val = "hello\nworld"
print(f"{val!r}") # repr: 'hello\nworld'
print(f"{val!s}") # str: hello\nworld
print(f"{val!a}") # ascii
# 중첩 f-string (3.12+)
width = 10
align = ">"
print(f"{'hello':{align}{width}}") # hello
# 날짜 포맷
now = datetime.datetime.now()
print(f"현재: {now:%Y년 %m월 %d일}")f"{x = }" 디버깅 구문에 포맷 스펙을 추가할 수 있습니다: f"{x = :.2f}".
f-string 안에서 백슬래시(\)를 직접 사용할 수 없습니다. 변수에 먼저 할당하거나 chr()을 사용하세요.
22언패킹 심화
*와 ** 언패킹의 고급 활용법입니다. 변수 할당, 함수 호출, 병합에 사용됩니다.
Python code
# 확장 언패킹
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last) # 1 [2, 3, 4] 5
# 중첩 언패킹
data = ("Alice", (90, 85, 92))
name, (math, eng, sci) = data
print(f"{name}: 수학={math}")
# 딕셔너리 병합 (3.9+)
defaults = {"color": "red", "size": 10}
custom = {"size": 20, "weight": "bold"}
merged = {**defaults, **custom}
print(merged) # {'color': 'red', 'size': 20, 'weight': 'bold'}
# 3.9+ 파이프 연산자
merged2 = defaults | custom
print(merged2)
# 함수 인자 언패킹
def greet(name, age, city):
print(f"{name}({age})은 {city}에 삽니다")
info = {"name": "Alice", "age": 30, "city": "서울"}
greet(**info)* 언패킹은 이터러블을 리스트로, **는 딕셔너리를 키워드 인자로 풀어줍니다.
*middle은 항상 리스트를 반환합니다. 빈 경우에도 []이므로 None 체크 대신 길이를 확인하세요.
23딕셔너리 심화
딕셔너리의 고급 메서드와 패턴을 다룹니다. setdefault, get, 딕셔너리 뷰 등을 포함합니다.
Python code
# setdefault: 키가 없으면 기본값 설정 후 반환
groups: dict[str, list] = {}
for name, dept in [("Alice", "Dev"), ("Bob", "Dev"), ("Carol", "HR")]:
groups.setdefault(dept, []).append(name)
print(groups) # {'Dev': ['Alice', 'Bob'], 'HR': ['Carol']}
# dict comprehension으로 변환
prices = {"apple": 1000, "banana": 500, "cherry": 2000}
expensive = {k: v for k, v in prices.items() if v >= 1000}
print(expensive) # {'apple': 1000, 'cherry': 2000}
# 딕셔너리 뷰 활용
a = {"x": 1, "y": 2, "z": 3}
b = {"y": 20, "z": 30, "w": 40}
common_keys = a.keys() & b.keys()
print(common_keys) # {'y', 'z'}
# dict.fromkeys()
defaults = dict.fromkeys(["a", "b", "c"], 0)
print(defaults) # {'a': 0, 'b': 0, 'c': 0}
# __missing__ 활용
class AutoDict(dict):
def __missing__(self, key):
self[key] = type(self)()
return self[key]딕셔너리 뷰(.keys(), .items())는 집합 연산을 지원하여 교집합, 차집합 등을 구할 수 있습니다.
dict.fromkeys(keys, [])는 모든 키가 같은 리스트 객체를 공유합니다. 독립된 리스트가 필요하면 컴프리헨션을 사용하세요.
24세트 연산
set의 집합 연산으로 교집합, 합집합, 차집합, 대칭차를 효율적으로 수행합니다.
Python code
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}
# 기본 집합 연산
print(a & b) # 교집합: {4, 5}
print(a | b) # 합집합: {1, 2, 3, 4, 5, 6, 7, 8}
print(a - b) # 차집합: {1, 2, 3}
print(a ^ b) # 대칭차: {1, 2, 3, 6, 7, 8}
# 부분집합 / 상위집합
small = {1, 2}
print(small <= a) # True (부분집합)
print(a >= small) # True (상위집합)
print(a.isdisjoint(b)) # False (교집합 존재)
# 세트 컴프리헨션
text = "hello world"
unique_vowels = {c for c in text if c in "aeiou"}
print(unique_vowels) # {'e', 'o'}
# 변경 가능 연산
a.add(10)
a.discard(99) # 없어도 에러 안 남
a.update([20, 30])
print(a)discard()는 없는 원소를 제거해도 에러가 발생하지 않지만, remove()는 KeyError를 발생시킵니다.
빈 세트는 set()으로 만들어야 합니다. {}는 빈 딕셔너리입니다.
25frozenset 불변 세트
frozenset은 변경 불가능한 세트로, 딕셔너리 키나 다른 세트의 원소로 사용할 수 있습니다.
Python code
# frozenset 생성
fs = frozenset([1, 2, 3, 4, 5])
print(fs) # frozenset({1, 2, 3, 4, 5})
# 집합 연산은 가능 (새 frozenset 반환)
fs2 = frozenset([4, 5, 6])
print(fs & fs2) # frozenset({4, 5})
print(fs | fs2) # frozenset({1, 2, 3, 4, 5, 6})
# 딕셔너리 키로 사용 가능
permissions = {
frozenset(["read"]): "viewer",
frozenset(["read", "write"]): "editor",
frozenset(["read", "write", "admin"]): "admin",
}
user_perms = frozenset(["read", "write"])
print(permissions[user_perms]) # editor
# 세트의 세트
set_of_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozenset([1, 2]) in set_of_sets) # True설정값이나 권한 같은 변경되면 안 되는 집합 데이터에 frozenset을 사용하면 안전합니다.
frozenset에 add()나 remove()를 호출하면 AttributeError가 발생합니다.
26네임드튜플
namedtuple로 필드명이 있는 불변 튜플을 만들어 코드 가독성을 높입니다.
Python code
from collections import namedtuple
from typing import NamedTuple
# collections 방식
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(f"x={p.x}, y={p.y}")
print(f"거리: {(p.x**2 + p.y**2)**0.5:.2f}")
# typing 방식 (타입 힌트 포함)
class User(NamedTuple):
name: str
age: int
email: str = "없음" # 기본값
u = User("Alice", 30)
print(f"{u.name} ({u.age}) - {u.email}")
# _asdict(), _replace()
d = u._asdict()
print(d) # {'name': 'Alice', 'age': 30, 'email': '없음'}
u2 = u._replace(age=31)
print(u2) # User(name='Alice', age=31, email='없음')
# 언패킹 지원
name, age, email = u
print(name, age)대부분의 경우 typing.NamedTuple을 사용하세요. 타입 힌트와 기본값을 지원하며 더 읽기 좋습니다.
NamedTuple은 불변이므로 필드 값을 직접 변경할 수 없습니다. 변경이 필요하면 dataclass를 사용하세요.
27Counter 카운터
Counter로 요소의 빈도를 세고, 가장 많이 등장하는 항목을 찾습니다.
Python code
from collections import Counter
# 빈도 세기
text = "abracadabra"
counter = Counter(text)
print(counter) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# 가장 흔한 요소
print(counter.most_common(3)) # [('a', 5), ('b', 2), ('r', 2)]
# 산술 연산
c1 = Counter("aabb")
c2 = Counter("abcc")
print(c1 + c2) # Counter({'a': 3, 'b': 3, 'c': 2})
print(c1 - c2) # Counter({'a': 1, 'b': 1})
# 단어 빈도 분석
words = "the cat sat on the mat the cat".split()
word_count = Counter(words)
for word, count in word_count.most_common():
print(f"{word:>5}: {'■' * count} ({count})")
# total() (Python 3.10+)
print(f"총 글자 수: {counter.total()}")Counter는 dict의 서브클래스이므로 모든 딕셔너리 메서드를 사용할 수 있습니다.
Counter에서 존재하지 않는 키를 조회하면 KeyError 대신 0을 반환합니다. 이를 의도하지 않으면 주의하세요.
28defaultdict
defaultdict는 존재하지 않는 키에 접근할 때 자동으로 기본값을 생성합니다.
Python code
from collections import defaultdict
# 리스트 기본값
grouped = defaultdict(list)
for name, score in [("수학", 90), ("영어", 85), ("수학", 95), ("영어", 92)]:
grouped[name].append(score)
print(dict(grouped)) # {'수학': [90, 95], '영어': [85, 92]}
# int 기본값 (0)
counter = defaultdict(int)
for char in "hello world":
counter[char] += 1
print(dict(counter))
# set 기본값
tags = defaultdict(set)
tags["python"].add("언어")
tags["python"].add("프로그래밍")
tags["java"].add("언어")
print(dict(tags))
# 커스텀 팩토리
def default_user():
return {"name": "unknown", "active": False}
users = defaultdict(default_user)
print(users["new_user"]) # {'name': 'unknown', 'active': False}defaultdict를 JSON 직렬화할 때는 dict()로 변환하세요. json.dumps가 직접 처리하지 못할 수 있습니다.
defaultdict는 키에 접근만 해도 기본값이 생성됩니다. in으로 존재 여부만 확인할 때는 일반 dict를 사용하세요.
29OrderedDict
OrderedDict는 삽입 순서를 보장하며, 순서 관련 추가 기능을 제공합니다. Python 3.7+에서 일반 dict도 순서를 보장하지만 차이점이 있습니다.
Python code
from collections import OrderedDict
# OrderedDict 고유 기능
od = OrderedDict()
od["c"] = 3
od["a"] = 1
od["b"] = 2
# move_to_end: 항목을 끝으로 이동
od.move_to_end("c")
print(list(od.keys())) # ['a', 'b', 'c']
# move_to_end(last=False): 처음으로 이동
od.move_to_end("b", last=False)
print(list(od.keys())) # ['b', 'a', 'c']
# popitem: LIFO (기본) 또는 FIFO
print(od.popitem()) # ('c', 3) 마지막
print(od.popitem(last=False)) # ('b', 2) 처음
# 순서가 같아야 동등
od1 = OrderedDict([("a", 1), ("b", 2)])
od2 = OrderedDict([("b", 2), ("a", 1)])
print(od1 == od2) # False (순서 다름)
# 일반 dict는 순서 무시
print({"a": 1, "b": 2} == {"b": 2, "a": 1}) # TrueLRU 캐시 구현 시 OrderedDict의 move_to_end()가 유용합니다.
Python 3.7+ dict도 순서를 보장하지만, 동등 비교에서 순서를 고려하지 않습니다. 순서 기반 비교가 필요하면 OrderedDict를 사용하세요.
정리하며
- 타입 힌트는 실행을 막지 않습니다. 검사 효과를 보려면 mypy·pyright를 CI에 붙여야 합니다.
match의 점 없는 이름은 캡처 패턴입니다. 상수 비교는Enum.MEMBER형태로 적습니다.- 집계 코드는 수동 초기화 대신
Counter·defaultdict로 줄이되 읽기만 할 땐get()을 씁니다. - 전체를 메모리에 올릴 필요가 없으면 리스트 컴프리헨션 대신 제너레이터 표현식을 고릅니다.
더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.