Skip to content

Python 치트시트

웹, 데이터, AI, 자동화를 위한 다용도의 읽기 쉬운 언어입니다.

01

시작하기

Hello World & 주석

Python은 주석에 #를 사용하고 docstring에는 삼중 따옴표를 사용합니다. print() 함수는 sep과 end 매개변수를 지원하여 출력 형식을 사용자 정의할 수 있습니다. docstring은 help()와 __doc__를 통해 접근할 수 있는 문서로 사용됩니다.

python
# This is a single-line comment
"""
This is a
multi-line comment (docstring)
"""
print("Hello, World!")        # print to stdout
print("A", "B", "C", sep="-") # A-B-C
print("No newline", end="")   # suppress newline

들여쓰기 & 코드 블록

대부분의 언어와 달리 Python은 코드 블록을 정의하기 위해 중괄호 대신 들여쓰기를 사용합니다. 일관성이 매우 중요합니다 — 탭과 공백을 혼용하면 SyntaxError가 발생합니다. PEP 8은 레벨당 4칸의 공백을 권장합니다.

python
# Python uses indentation (4 spaces) to define blocks
if True:
    print("inside if")
    if True:
        print("nested block")
print("outside block")

# No braces! Indentation IS the syntax
def func():
    x = 1
    return x + 1

입력 & 출력

input()은 stdin에서 문자열로 읽습니다 — 숫자가 필요할 때는 항상 변환하세요. 변환에는 int(), float() 등을 사용하세요. F-strings(Python 3.6+)는 문자열 형식을 지정하는 선호되는 방법입니다.

python
# input() always returns a string
name = input("Enter your name: ")
age = int(input("Enter your age: "))  # convert to int

print(f"Hello {name}, you are {age} years old")

# formatted output
print("Pi is approximately {:.2f}".format(3.14159))
print(f"{1000000:,}")  # 1,000,000 with thousands separator

다중 문장 & 줄 연속

한 줄에 여러 문장을 분리하기 위해 세미콜론을 사용합니다 (관용적인 Python에서는 드뭅니다). 긴 줄은 백슬래시로 연속할 수 있거나 (), [], {} 안에서 자동으로 연속됩니다. 가독성을 위해 암시적 연속을 선호하세요.

python
# multiple statements on one line (discouraged)
a = 1; b = 2; c = 3

# explicit line continuation
total = 1 + 2 + 3 + \
        4 + 5 + 6

# implicit continuation inside brackets
nums = [
    1, 2, 3,
    4, 5, 6
]

result = (1 + 2
          + 3 + 4)

Python 실행

Python 스크립트는 'python script.py'로 실행합니다. REPL은 대화형 실험을 허용합니다. python이 Python 2를 가리키는 시스템에서는 항상 python3를 사용하세요. shebang 줄은 Unix에서 스크립트를 실행 가능하게 만듭니다.

python
# Run a script
# $ python script.py

# Run interactively (REPL)
# $ python
# >>> 2 + 2
# 4

# Shebang line for Unix scripts
#!/usr/bin/env python3

# Check Python version
import sys
print(sys.version)
print(sys.version_info.major)  # 3
02

변수 & 데이터 타입

변수 & 동적 타이핑

Python은 동적 타이핑을 사용합니다 — 변수는 런타임에 타입을 변경할 수 있습니다. 확인에는 type()을, 검증에는 isinstance()를 사용하세요. Python 3.6+는 런타임 강제 없이 IDE 지원을 위한 타입 힌트(name: str = 'Alice')를 지원합니다.

python
# Python is dynamically typed - no declaration needed
name = "Alice"          # str
age = 30                # int
height = 5.7            # float
is_active = True        # bool
items = [1, 2, 3]       # list

# Type checking
print(type(name))       # <class 'str'>
print(isinstance(age, int))  # True

# Multiple assignment
x, y, z = 1, 2, 3
a = b = 0  # chain assignment

타입 힌트 (Python 3.6+)

타입 힌트는 코드 가독성을 높이고 IDE 자동완성과 mypy를 사용한 정적 분석을 가능하게 합니다. 런타임에 강제되지 않습니다 — Python은 여전히 동적으로 타입이 지정됩니다. None이 될 수 있는 값에는 Optional[X]를 사용하세요.

python
# Variable annotations
name: str = "Alice"
age: int = 30
scores: list[float] = [90.5, 85.0]

# Function annotations
def greet(name: str, times: int = 1) -> str:
    return (f"Hi {name}! " * times).strip()

# Optional and Union
from typing import Optional, Union
def find(id: int) -> Optional[str]:
    return "Alice" if id == 1 else None

# mypy for static type checking
# $ mypy script.py

타입 변환

Python에는 내장 변환 함수가 있습니다: int(), float(), str(), bool(), list(), tuple(), set(), dict(). 거짓 값에는 0, '', [], {}, None, False가 포함됩니다. int()는 0을 향해 자르지만 round()는 은행가 반올림을 사용합니다.

python
# String to number
num_str = str(42)          # "42"
num = int("42")            # 42
float_num = float("3.14")  # 3.14

# Number conversions
print(int(3.99))           # 3 (truncates toward zero)
print(int(-3.99))          # -3
print(round(3.14159, 2))   # 3.14

# Boolean conversion
print(bool(0))             # False
print(bool(""))            # False
print(bool([]))            # False
print(bool("anything"))    # True

# Collection conversions
print(list("abc"))         # ['a', 'b', 'c']
print(tuple([1, 2, 3]))    # (1, 2, 3)
print(set([1, 1, 2]))      # {1, 2}

숫자 타입

Python 정수는 임의 정밀도를 가집니다 (오버플로우 없음). Float는 일반적인 정밀도 문제가 있는 IEEE 754 배정도입니다. 복소수는 내장되어 있습니다. 불리언은 int의 하위 클래스입니다 (True==1, False==0). 가독성을 위해 숫자 리터럴에 밑줄을 사용하세요.

python
# Integers (arbitrary precision)
big = 10 ** 100  # no overflow
print(type(big))  # <class 'int'>

# Floats (IEEE 754 double)
pi = 3.14159
print(0.1 + 0.2)  # 0.30000000000000004

# Complex numbers
z = 3 + 4j
print(z.real, z.imag)  # 3.0 4.0
print(abs(z))          # 5.0

# Boolean is subclass of int
print(isinstance(True, int))  # True
print(True + True)            # 2

# Underscores in numbers (3.6+)
million = 1_000_000
binary = 0b_1010_1010

상수 & 명명 규칙

Python에는 const 키워드가 없습니다 — ALL_CAPS 이름은 규칙에 의한 상수일 뿐입니다 (재할당을 막지 않음). PEP 8은 명명 규칙을 정의합니다: 변수/함수에는 snake_case, 클래스에는 PascalCase, 상수에는 ALL_CAPS. 선행 밑줄은 규칙상 'private'를 의미하고 이중 밑줄은 이름 장식을 트리거합니다.

python
# Python has no true constants - convention only
MAX_SIZE = 100          # ALL_CAPS for constants
PI = 3.14159

# Naming conventions (PEP 8)
variable_name = "snake_case"      # variables, functions
ClassName = "PascalCase"          # classes
CONSTANT_VALUE = 100              # constants
_private_var = "underscore prefix" # private (convention)
__name_mangled = "double underscore" # name mangling

# dunder names (reserved)
__name__, __main__, __init__
03

문자열

문자열 메서드

문자열은 불변입니다 — 메서드는 새 문자열을 반환합니다. 찾지 못하면 find()는 -1을 반환하고 index()는 ValueError를 발생시킵니다. 검증에는 isalpha()/isdigit()/isalnum()을 사용하세요. str 클래스에는 40개 이상의 메서드가 있습니다 — dir(str)로 탐색하세요.

python
s = "Hello, World"

# Case operations
print(s.upper())          # HELLO, WORLD
print(s.lower())          # hello, world
print(s.title())          # Hello, World
print(s.capitalize())     # Hello, world
print(s.swapcase())       # hELLO, wORLD

# Search & replace
print(s.find("World"))    # 7 (index, -1 if not found)
print(s.index("World"))   # 7 (raises ValueError if not found)
print(s.replace("o", "0"))  # Hell0, W0rld
print(s.count("l"))       # 3

# Validation
print("abc".isalpha())    # True
print("123".isdigit())    # True
print("  ".isspace())     # True

문자열 형식 지정

F-strings는 현대적이고 가장 빠르며 가장 읽기 쉬운 문자열 형식 지정 방법입니다. 콜론 뒤에 형식 사양을 지원합니다: 소수 2자리는 :.2f, 너비 10 우측 정렬은 :>10, 천 단위 구분자는 :,. 새 코드에서는 %-형식 지정을 피하세요.

python
name = "Alice"
age = 30

# f-strings (Python 3.6+) - PREFERRED
print(f"Hello, {name}! You are {age}.")
print(f"{name.upper()} is {age * 365} days old")
print(f"{3.14159:.2f}")      # 3.14
print(f"{42:>10}")           # right-align
print(f"{42:<10}")           # left-align
print(f"{42:^10}")           # center
print(f"{1000000:,}")        # 1,000,000

# str.format() method
print("Hello, {}!".format(name))
print("{name} is {age}".format(name="Bob", age=25))

# Old style (avoid in new code)
print("Hello, %s!" % name)

슬라이싱 & 인덱싱

Python 슬라이싱 구문 [start:stop:step]은 강력합니다 — stop은 배타적입니다. 음수 인덱스는 끝에서부터 셉니다. s[::-1]은 문자열을 뒤집는 관용적인 방법입니다. 슬라이싱은 안전합니다 — 범위를 벗어난 인덱스는 오류를 발생시키지 않고 빈 문자열을 반환합니다.

python
s = "Hello, World"

# Indexing (0-based, negative from end)
print(s[0])        # H
print(s[-1])       # d
print(s[7])        # W

# Slicing [start:stop:step]
print(s[0:5])      # Hello
print(s[7:])       # World
print(s[:5])       # Hello
print(s[::2])      # HloWrd (every 2nd char)
print(s[::-1])     # dlroW ,olleH (reverse!)

# Length
print(len(s))      # 12

# Slicing never raises IndexError
print(s[100:200])  # '' (empty string)

분할 & 결합

split()은 문자열을 리스트로 나누고 join()은 리스트를 문자열로 결합합니다. 많은 문자열을 효율적으로 연결하려면 항상 join()을 사용하세요 — + 연산자는 중간 문자열을 생성합니다. partition()은 정확히 3부분(앞, 구분자, 뒤)으로 나눕니다.

python
# Split
csv = "a,b,c,d"
print(csv.split(","))          # ['a', 'b', 'c', 'd']
print(csv.split(",", 2))       # ['a', 'b', 'c,d'] (max 2 splits)

# Splitlines
text = "line1\nline2\nline3"
print(text.splitlines())       # ['line1', 'line2', 'line3']

# Partition (splits on first occurrence)
print("[email protected]".partition("@"))
# ('user', '@', 'domain.com')

# Join
words = ["Hello", "World"]
print(" ".join(words))         # Hello World
print("-".join(["2024", "01", "15"]))  # 2024-01-15
print("".join(["a", "b", "c"])) # abc

# String concatenation
s = "Hello" + " " + "World"
parts = ["a"]
parts += "b"  # NOT string concat - adds chars to list!

제거 & 패딩

strip()은 기본적으로 선행/후행 공백을 제거하거나 지정된 문자를 제거합니다. zfill()은 선행 0으로 패딩합니다 (ID에 유용). rjust/ljust/center는 선택적 채우기 문자로 지정된 너비까지 패딩합니다.

python
# Strip whitespace (or specified chars)
s = "  hello  "
print(s.strip())    # "hello"
print(s.lstrip())   # "hello  "
print(s.rstrip())   # "  hello"

# Strip specific characters
print("xxxhelloxxx".strip("x"))  # hello

# Padding / centering
print("42".zfill(5))      # 00042
print("hi".rjust(10))     # "        hi"
print("hi".ljust(10, "-")) # "hi--------"
print("hi".center(10, "*")) # "****hi****"

# expandtabs
print("a\tb".expandtabs(4))  # "a   b"

Raw 문자열 & 이스케이프

Raw 문자열(r'...')은 백슬래시를 문자 그대로 처리합니다 — 정규식 패턴과 Windows 파일 경로에 필수적입니다. 삼중 따옴표 문자열은 줄바꿈을 유지합니다. 문자열은 반복을 위해 * 연산자를, 연결을 위해 +를 지원합니다.

python
# Escape sequences
print("Line1\nLine2")     # newline
print("Tab\there")        # tab
print("Quote: \"hi\"")   # escaped quotes
print("Backslash: \\")   # literal backslash

# Raw strings (ignore escapes) - great for regex
path = r"C:\Users\name\file.txt"
regex = r"\d{3}-\d{4}"
print(path)  # C:\Users\name\file.txt

# Triple-quoted strings
multi = """
Multiple
lines
"""

# String multiplication
print("ab" * 3)  # ababab
04

숫자 & 수학

산술 연산자

Python에는 7개의 산술 연산자가 있습니다. /는 항상 float를 반환하고 //는 정수 나눗셈입니다 (음의 무한대를 향해 반올림). **는 거듭제곱입니다 (^가 아님 — 그것은 XOR입니다). % 연산자의 결과는 C/Java와 달리 제수의 부호를 따릅니다.

python
# Basic operators
print(7 + 3)   # 10  addition
print(7 - 3)   # 4   subtraction
print(7 * 3)   # 21  multiplication
print(7 / 3)   # 2.333...  true division (always float)
print(7 // 3)  # 2   floor division
print(7 % 3)   # 1   modulo (remainder)
print(7 ** 3)  # 343 exponentiation

# Floor division with negatives
print(-7 // 3)  # -3 (rounds toward negative infinity)
print(-7 % 3)   # 2 (result has same sign as divisor)

# Augmented assignment
x = 10
x += 5   # x = x + 5
x **= 2  # x = x ** 2

Math 모듈

math 모듈은 수학 함수와 상수를 제공합니다. 모든 삼각 함수는 라디안을 사용합니다 — math.radians()/degrees()로 변환하세요. math.gcd()는 최대공약수를 찾습니다. 복소수에는 cmath 모듈을 사용하세요.

python
import math

# Constants
print(math.pi)       # 3.141592653589793
print(math.e)        # 2.718281828459045
print(math.inf)      # inf
print(math.nan)      # nan

# Functions
print(math.sqrt(16))     # 4.0
print(math.pow(2, 10))   # 1024.0
print(math.log(100, 10)) # 2.0 (log base 10)
print(math.log(math.e))  # 1.0 (natural log)
print(math.factorial(5)) # 120
print(math.gcd(12, 8))   # 4

# Rounding
print(math.floor(3.7))   # 3
print(math.ceil(3.2))    # 4
print(math.trunc(-3.7))  # -3 (toward zero)

# Trigonometry (radians)
print(math.sin(math.pi / 2))  # 1.0
print(math.degrees(math.pi))  # 180.0

Random 모듈

random 모듈은 Mersenne Twister PRNG를 사용합니다 — 암호학적으로 안전하지 않습니다. 보안에는 secrets 모듈을 사용하세요. random.sample()은 고유한 항목을 선택하고 random.choices()는 중복을 허용합니다. 테스트에서 재현 가능한 결과를 위해 시드를 설정하세요.

python
import random

# Random integers
print(random.randint(1, 100))    # 1 to 100 inclusive
print(random.randrange(0, 10, 2)) # even number 0,2,4,6,8

# Random floats
print(random.random())           # 0.0 to 1.0
print(random.uniform(1.0, 10.0)) # random float in range

# Choice & sampling
colors = ["red", "green", "blue"]
print(random.choice(colors))           # one random item
print(random.sample(colors, 2))        # 2 unique items
print(random.choices(colors, k=5))     # 5 items (with replacement)

# Shuffle (in-place)
nums = [1, 2, 3, 4, 5]
random.shuffle(nums)
print(nums)

# Reproducible randomness
random.seed(42)  # same seed = same sequence

Decimal & Fractions

float 정밀도 오류가 허용되지 않는 재무 계산에는 Decimal을 사용하세요 (예: 돈). 정확한 유리수 연산에는 Fraction을 사용하세요. 둘 다 float보다 느리지만 반올림 오류를 피합니다. 항상 float가 아닌 문자열에서 Decimal을 생성하세요.

python
from decimal import Decimal, getcontext
from fractions import Fraction

# Float precision issues
print(0.1 + 0.2)  # 0.30000000000000004

# Decimal for exact decimal arithmetic
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)  # 0.3 (exact!)

# Set precision
getcontext().prec = 6
print(Decimal(1) / Decimal(7))  # 0.142857

# Fractions for exact rational arithmetic
f1 = Fraction(1, 3)
f2 = Fraction(1, 6)
print(f1 + f2)  # 1/2
print(float(f1))  # 0.3333...

# Fraction from string
print(Fraction("3/4"))  # 3/4

비트 연산자

비트 연산자는 정수의 개별 비트를 조작합니다. Python 정수는 임의 정밀도를 가지므로 시프트가 고정 너비 언어와 다르게 작동합니다. 일반적인 용도: 플래그, 마스크, 저수준 프로토콜 파싱. x & (x-1) == 0은 x가 2의 거듭제곱인지 확인합니다.

python
# Bitwise operators work on integers
a = 0b1010  # 10
b = 0b1100  # 12

print(a & b)   # 8  (0b1000) AND
print(a | b)   # 14 (0b1110) OR
print(a ^ b)   # 6  (0b0110) XOR
print(~a)      # -11 (NOT, two's complement)
print(a << 2)  # 40 (left shift, multiply by 4)
print(a >> 1)  # 5  (right shift, divide by 2)

# Binary representation
print(bin(10))   # 0b1010
print(hex(255))  # 0xff
print(oct(8))    # 0o10
print(int("1010", 2))  # 10 (parse binary)

# Common tricks
print(5 & 1)     # 1 (check odd: nonzero = odd)
print(8 & (8-1)) # 0 (check power of 2)
05

데이터 구조

리스트

리스트는 Python의 가장 다용도적인 데이터 구조입니다 — 순서가 있고, 가변적이며, 이질적입니다. append()는 O(1), insert(0, x)는 O(n)입니다. 양 끝에서의 빠른 작업에는 collections.deque를 사용하세요. sort()는 제자리에서 수행되고 sorted()는 새 리스트를 반환합니다.

python
# Lists are ordered, mutable sequences
nums = [1, 2, 3, 4, 5]
mixed = [1, "hello", True, 3.14]

# Adding elements
nums.append(6)           # [1,2,3,4,5,6]
nums.insert(0, 0)        # [0,1,2,3,4,5,6]
nums.extend([7, 8])      # extend with another list

# Removing elements
nums.remove(0)           # remove by value
popped = nums.pop()      # remove & return last
popped = nums.pop(0)     # remove & return by index
del nums[0]              # delete by index
nums.clear()             # remove all

# Slicing (same as strings)
nums = [1, 2, 3, 4, 5]
print(nums[1:3])    # [2, 3]
print(nums[::-1])   # [5, 4, 3, 2, 1] reverse

# Sorting
nums.sort()              # in-place sort
nums.sort(reverse=True)  # descending
sorted_nums = sorted(nums)  # returns new list

튜플

튜플은 불변이고 리스트보다 빠릅니다. 고정 컬렉션, 여러 반환 값, 사전 키(리스트는 키가 될 수 없음)에 사용하세요. Named tuple은 가독성을 위해 필드 이름을 제공합니다. 단일 요소 튜플은 후행 쉼표가 필요합니다.

python
# Tuples are ordered, IMMUTABLE sequences
point = (3, 4)
single = (42,)     # note the comma for single-element tuple
empty = ()

# Packing & unpacking
coordinates = 10, 20, 30  # packing
x, y, z = coordinates     # unpacking
x, y = y, x               # swap values!

# Multiple return values
def min_max(nums):
    return min(nums), max(nums)

lo, hi = min_max([3, 1, 4, 1, 5])

# Named tuples (readable)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y)       # 3 4
print(p[0], p[1])     # 3 4

# Tuples are immutable but can contain mutable objects
t = (1, [2, 3])
t[1].append(4)  # OK: (1, [2, 3, 4])

사전

사전은 해시 맵입니다 — 조회/삽입/삭제에 평균 O(1)입니다. 키는 해시 가능해야 합니다 (불변). Python 3.7부터 dict는 삽입 순서를 유지합니다. KeyError를 피하려면 get()을 사용하세요. dict comprehension은 dict를 우아하게 생성합니다.

python
# Dicts are key-value mappings (insertion-ordered since 3.7)
user = {"name": "Alice", "age": 30}

# Access
print(user["name"])          # Alice
print(user.get("email"))     # None (no KeyError)
print(user.get("email", "N/A"))  # N/A (default)

# Add/update
user["email"] = "[email protected]"    # add
user["age"] = 31             # update
user.setdefault("role", "user")  # set if missing

# Delete
del user["email"]
val = user.pop("age")        # remove & return
# user.clear()               # remove all

# Iteration
for key in user:             # keys
    print(key)
for k, v in user.items():    # key-value pairs
    print(k, v)
for v in user.values():      # values
    print(v)

# Dict comprehension
squares = {x: x**2 for x in range(5)}

# Merge dicts (3.9+)
merged = {"a": 1} | {"b": 2}

집합

집합은 고유하고 해시 가능한 요소의 순서 없는 컬렉션입니다. 멤버십 테스트(리스트의 O(n)에 비해 O(1))와 집합 대수(합집합, 교집합, 차집합)에 뛰어납니다. frozenset은 불변이고 해시 가능합니다. 순서는 보장되지 않습니다.

python
# Sets are unordered collections of unique elements
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# Set operations
print(a | b)    # union: {1, 2, 3, 4, 5, 6}
print(a & b)    # intersection: {3, 4}
print(a - b)    # difference: {1, 2}
print(a ^ b)    # symmetric difference: {1, 2, 5, 6}

# Methods
a.add(5)        # add element
a.discard(10)   # remove if present (no error)
a.remove(1)     # remove (KeyError if missing)
a.update([6, 7]) # add multiple

# Membership test (O(1) - faster than list)
print(3 in a)   # True

# Frozen set (immutable)
fs = frozenset([1, 2, 3])

# Common use: deduplicate
unique = list(set([1, 1, 2, 2, 3]))  # [1, 2, 3]

컴프리헨션

컴프리헨션은 간결하게 컬렉션을 생성하는 Pythonic 방법입니다. 제너레이터 표현식(대괄호 대신 괄호)은 지연 평가됩니다 — 요구에 따라 값을 생성하여 메모리를 절약합니다. 가독성을 위해 map()/filter()보다 컴프리헨션을 선호하세요.

python
# List comprehension
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]

# Dict comprehension
square_map = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension
unique_lens = {len(w) for w in ["a", "ab", "abc", "ab"]}

# Generator expression (lazy, memory-efficient)
gen = (x**2 for x in range(1000000))
print(next(gen))  # 0
print(next(gen))  # 1
total = sum(x**2 for x in range(100))  # no extra list

# Nested comprehension (matrix)
matrix = [[i * 3 + j for j in range(3)] for i in range(3)]
# [[0,1,2], [3,4,5], [6,7,8]]

Collections 모듈

collections 모듈은 특수 컨테이너를 제공합니다. Counter는 해시 가능한 항목을 셉니다. defaultdict는 누락된 키를 자동 생성합니다. deque는 양 끝에서 O(1) append/pop을 제공합니다 (리스트의 O(n)와 비교). 깨끗하고 효율적인 코드에 필수적입니다.

python
from collections import Counter, defaultdict, deque, OrderedDict

# Counter - counting
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
cnt = Counter(words)
print(cnt)  # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(cnt.most_common(2))  # [('apple', 3), ('banana', 2)]

# defaultdict - no KeyError
dd = defaultdict(list)
dd["fruits"].append("apple")
dd["fruits"].append("banana")
# dd["vegs"] automatically creates empty list

# deque - fast double-ended queue
dq = deque([1, 2, 3])
dq.appendleft(0)   # [0, 1, 2, 3]
dq.append(4)       # [0, 1, 2, 3, 4]
dq.popleft()       # 0, deque is now [1, 2, 3, 4]
dq.rotate(1)       # rotate right

# OrderedDict (less needed since 3.7, dicts are ordered)
od = OrderedDict([("a", 1), ("b", 2)])
06

제어 흐름

If / Elif / Else

Python은 if/elif/else를 사용합니다 — 'elseif'가 아닌 'elif'에 주의하세요. 들여쓰기가 블록을 정의합니다. 삼항 'x if cond else y'는 표현식입니다. Python은 빈 컬렉션, 0, None, False를 거짓으로 취급합니다 — 간결한 조건문에 유용합니다.

python
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(f"Grade: {grade}")  # Grade: B

# Conditional expression (ternary)
status = "pass" if score >= 60 else "fail"

# Truthy/falsy values
# Falsy: False, 0, 0.0, "", [], {}, (), None
# Everything else is truthy
if []:    # False
    print("never")
if [0]:   # True (non-empty list)
    print("always")

For 루프 & 반복

Python의 for 루프는 반복 가능한 모든 항목을 반복합니다. range()는 숫자를 생성합니다 (배타적 stop). enumerate()는 항목과 인덱스를 짝지습니다. zip()은 여러 시퀀스를 병렬로 반복합니다. dict 키-값 쌍을 반복하려면 .items()를 사용하세요.

python
# range(start, stop, step)
for i in range(5):        # 0, 1, 2, 3, 4
    print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8
    print(i)
for i in range(10, 0, -1): # countdown
    print(i)

# Iterate over collections
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# enumerate for index + value
for idx, fruit in enumerate(fruits):
    print(f"{idx}: {fruit}")

# zip to iterate multiple sequences
names = ["Alice", "Bob"]
ages = [30, 25]
for name, age in zip(names, ages):
    print(f"{name}: {age}")

# Iterate dict
user = {"name": "Alice", "age": 30}
for key, value in user.items():
    print(f"{key} = {value}")

While 루프 & Break/Continue

while 루프는 조건이 참인 동안 반복합니다. break는 루프를 즉시 종료하고 continue는 다음 반복으로 건너뜁니다. for/else 구문은 루프가 break되지 않은 경우에만 else 블록을 실행합니다. pass는 빈 블록의 no-op 자리표시자입니다.

python
# Basic while
count = 0
while count < 5:
    print(count)
    count += 1

# break - exit loop
while True:
    cmd = input("> ")
    if cmd == "quit":
        break
    print(f"You said: {cmd}")

# continue - skip to next iteration
for i in range(10):
    if i % 2 == 0:
        continue  # skip even numbers
    print(i)  # prints 1, 3, 5, 7, 9

# else clause (runs if no break)
for i in range(5):
    if i == 10:
        break
else:
    print("Loop completed without break")

# pass - do nothing (placeholder)
for i in range(5):
    pass  # TODO: implement

Match 문 (Python 3.10+)

match 문(Python 3.10+)은 C의 switch를 훨씬 넘어서는 강력한 구조적 패턴 매칭입니다. 시퀀스, 매핑, 클래스 인스턴스를 매칭하고 변수를 바인딩할 수 있습니다. _ 패턴은 와일드카드(기본값)입니다. 'if'가 있는 가드는 조건을 추가합니다.

python
# Structural pattern matching (like switch)
def handle_command(cmd):
    match cmd.split():
        case ["quit"]:
            return "Goodbye"
        case ["hello", name]:
            return f"Hello, {name}!"
        case ["move", direction] if direction in "NSEW":
            return f"Moving {direction}"
        case ["add", x, y]:
            return int(x) + int(y)
        case _:
            return "Unknown command"

print(handle_command("hello Alice"))  # Hello, Alice!

# Matching data structures
match point:
    case (0, 0):
        print("origin")
    case (0, y):
        print(f"on y-axis at {y}")
    case (x, 0):
        print(f"on x-axis at {x}")
    case (x, y):
        print(f"at ({x}, {y})")

이터레이터 & 제너레이터

제너레이터는 yield를 사용하여 지연 평가로 값을 생성합니다 — 모든 값을 미리 계산하지 않아 메모리를 절약합니다. 이터레이터 프로토콜(iter()와 next())을 구현합니다. 한 번 소진되면 끝입니다. 큰/무한 시퀀스, 파이프라인, 스트리밍 데이터에 제너레이터를 사용하세요.

python
# Iterator protocol
nums = [1, 2, 3]
it = iter(nums)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
# next(it)  # StopIteration

# Generator function (uses yield)
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

for num in count_up_to(5):
    print(num)  # 1, 2, 3, 4, 5

# Infinite generator
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print(next(fib))  # 0
print(next(fib))  # 1
print(next(fib))  # 1
print(next(fib))  # 2

# Generator expression
squares = (x**2 for x in range(10))
07

함수

함수 정의 & 호출

함수는 def로 정의됩니다. 기본 인수는 =를 사용합니다. Python은 명확성을 위해 키워드 인수를 지원합니다. 함수는 여러 값을 반환할 수 있습니다 (튜플로). docstring(삼중 따옴표)은 함수를 문서화하고 help()를 통해 접근할 수 있습니다.

python
# Basic function
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # Hello, Alice!

# Default arguments
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Bob"))              # Hello, Bob!
print(greet("Bob", "Hi"))        # Hi, Bob!

# Keyword arguments
print(greet(name="Carol", greeting="Hey"))

# Return multiple values (tuple)
def stats(nums):
    return min(nums), max(nums), sum(nums) / len(nums)

lo, hi, avg = stats([1, 2, 3, 4, 5])

# Docstrings
def add(a, b):
    """Add two numbers and return the result.
    
    Args:
        a: First number
        b: Second number
    Returns:
        Sum of a and b
    """
    return a + b

인수: *args & **kwargs

*args는 추가 위치 인수를 튜플로 수집하고 **kwargs는 추가 키워드 인수를 dict로 수집합니다. args/kwargs 이름은 규칙입니다. 함수 호출 시 *로 시퀀스를, **로 dict를 언팩할 수 있습니다. 순서: 위치, *args, 키워드, **kwargs.

python
# *args - variable positional arguments (tuple)
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3))       # 6
print(sum_all(1, 2, 3, 4, 5)) # 15

# **kwargs - variable keyword arguments (dict)
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=30, role="admin")

# Combining all
def func(a, b, *args, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

func(1, 2, 3, 4, x=5, y=6)
# a=1, b=2, args=(3, 4), kwargs={'x': 5, 'y': 6}

# Unpacking arguments
nums = [1, 2, 3]
print(sum_all(*nums))  # unpack list as args

opts = {"name": "Alice", "age": 30}
print_info(**opts)  # unpack dict as kwargs

Lambda & 고계 함수

Lambda는 단일 표현식으로 제한됩니다 — 복잡한 로직에는 def를 사용하세요. sorted(), map(), filter()와 같은 고계 함수의 인수로 빛을 발합니다. 하지만 리스트 컴프리헨션이 map/filter보다 종종 더 읽기 쉽습니다. reduce()는 functools에 있습니다.

python
# Lambda - anonymous function (single expression)
square = lambda x: x ** 2
print(square(5))  # 25

# Common with sorted, map, filter, reduce
students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]

# Sort by score (key function)
sorted_by_score = sorted(students, key=lambda s: s[1])
# [('Carol', 78), ('Alice', 85), ('Bob', 92)]

# map - apply function to each item
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
# [2, 4, 6, 8, 10]

# filter - keep items where function returns True
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4]

# reduce - accumulate to single value
from functools import reduce
product = reduce(lambda a, b: a * b, nums)
# 120 (1*2*3*4*5)

# Prefer comprehensions over map/filter
doubled = [x * 2 for x in nums]  # more Pythonic
evens = [x for x in nums if x % 2 == 0]

데코레이터

데코레이터는 원래 코드를 수정하지 않고 동작을 추가하기 위해 함수를 감쌉니다. @구문은 syntactic sugar입니다. 인수가 있는 데코레이터는 추가 중첩 레벨이 필요합니다. 원래 함수의 메타데이터(이름, docstring)를 보존하려면 항상 functools.wraps를 사용하세요.

python
# A decorator modifies a function's behavior
def uppercase_result(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_result
def greet(name):
    return f"hello, {name}"

print(greet("alice"))  # HELLO, ALICE

# Decorator with arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print("Hi!")

say_hi()  # prints "Hi!" three times

# Practical: timing decorator
import time
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

# Use functools.wraps to preserve metadata
from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

스코프 & 클로저

Python은 LEGB 스코프 순서로 이름을 해결합니다: Local, Enclosing, Global, Built-in. 함수 내에서 전역 변수를 재바인딩하려면 'global'을 사용하세요. 둘러싸는 스코프의 변수를 수정하려면 'nonlocal'(Python 3)을 사용하세요. 클로저는 둘러싸는 스코프를 기억합니다.

python
# LEGB rule: Local, Enclosing, Global, Built-in
x = "global"

def outer():
    x = "enclosing"
    
    def inner():
        x = "local"
        print(x)  # local
    
    inner()
    print(x)  # enclosing

outer()
print(x)  # global

# global keyword - modify global variable
count = 0
def increment():
    global count
    count += 1

# nonlocal keyword - modify enclosing variable
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3
08

OOP & 클래스

클래스 & 객체

클래스는 데이터(속성)와 동작(메서드)을 묶습니다. __init__은 생성자입니다. self는 인스턴스를 참조합니다 (다른 언어의 'this'와 같음). 클래스 변수는 공유되고 인스턴스 변수는 객체별입니다. __str__은 사용자용, __repr__은 개발자용입니다.

python
class Dog:
    # Class variable (shared by all instances)
    species = "Canis familiaris"
    
    # Constructor
    def __init__(self, name, age):
        # Instance variables
        self.name = name
        self.age = age
    
    # Instance method
    def bark(self):
        return f"{self.name} says Woof!"
    
    # String representation
    def __str__(self):
        return f"Dog({self.name}, {self.age})"
    
    # Official representation (for debugging)
    def __repr__(self):
        return f"Dog(name='{self.name}', age={self.age})"

# Create instances
buddy = Dog("Buddy", 3)
lucy = Dog("Lucy", 5)

print(buddy.bark())       # Buddy says Woof!
print(buddy.name)         # Buddy
print(buddy.species)      # Canis familiaris
print(str(buddy))         # Dog(Buddy, 3)

상속 & 다형성

상속을 통해 클래스는 동작을 재사용하고 확장할 수 있습니다. Python은 충돌을 해결하기 위해 MRO(메서드 해결 순서)로 다중 상속을 지원합니다. 다형성은 다른 타입을 균일하게 취급할 수 있게 합니다. type()이 아닌 isinstance()로 타입 검사를 하세요.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclass must implement")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Polymorphism - same interface, different behavior
def animal_sound(animal):
    print(animal.speak())

animals = [Dog("Buddy"), Cat("Whiskers")]
for a in animals:
    animal_sound(a)
# Buddy says Woof!
# Whiskers says Meow!

# Multiple inheritance
class Swimmer:
    def swim(self):
        return "swimming"

class Flyer:
    def fly(self):
        return "flying"

class Duck(Animal, Swimmer, Flyer):
    pass

duck = Duck("Donald")
print(duck.swim())  # swimming
print(duck.fly())   # flying

# Check inheritance
print(isinstance(duck, Animal))  # True
print(issubclass(Dog, Animal))   # True

프로퍼티 & 캡슐화

Python에는 진정한 private/protected가 없습니다 — 규칙을 사용합니다. 단일 밑줄 _는 '내부'를 의미합니다. 이중 밑줄 __은 이름 장식을 트리거합니다 (진정한 private가 아님). @property는 메서드를 getter/setter가 있는 속성으로 변환하여 검증과 계산된 프로퍼티를 가능하게 합니다.

python
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius  # uses setter below
    
    # Getter
    @property
    def celsius(self):
        return self._celsius
    
    # Setter
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value
    
    # Computed property
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

temp = Temperature(25)
print(temp.fahrenheit)  # 77.0
temp.fahrenheit = 100
print(temp.celsius)     # 37.78...

# Name conventions:
# _name   - protected (convention, not enforced)
# __name  - private (name mangling: _ClassName__name)
# __name__ - dunder (reserved by Python)

클래스 & 정적 메서드

@staticmethod는 클래스 네임스페이스의 함수일 뿐입니다 — 암시적 첫 번째 인수가 없습니다. @classmethod는 첫 번째 인수로 클래스(cls)를 받아들여 대체 생성자(팩토리 메서드)와 상속 인식 동작에 유용합니다. 생성자에는 classmethod를, 유틸리티 함수에는 staticmethod를 사용하세요.

python
class MathUtils:
    pi = 3.14159
    
    # Static method - no self/cls, lives in class namespace
    @staticmethod
    def add(a, b):
        return a + b
    
    # Class method - receives the class as first argument
    @classmethod
    def circle_area(cls, radius):
        return cls.pi * radius ** 2
    
    # Alternative constructor (common classmethod use)
    @classmethod
    def from_diameter(cls, diameter):
        return cls()  # would configure instance

# Static: called on class or instance, no special first arg
print(MathUtils.add(2, 3))  # 5

# Class: often used for alternative constructors
print(MathUtils.circle_area(5))  # 78.54...

# Factory pattern with classmethod
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    @classmethod
    def origin(cls):
        return cls(0, 0)
    
    @classmethod
    def from_tuple(cls, coords):
        return cls(*coords)

p1 = Point.origin()
p2 = Point.from_tuple((3, 4))

매직 메서드 (Dunder)

매직 메서드(dunder 메서드)는 연산자 오버로딩과 프로토콜 동작을 구현합니다. +는 __add__, ==는 __eq__, len()은 __len__, 반복/언팩은 __iter__입니다. 이를 통해 객체가 Python의 내장 구문과 함수로 자연스럽게 작동할 수 있습니다.

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # String representation
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    # Operator overloading
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # Equality
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Length
    def __len__(self):
        return int((self.x**2 + self.y**2) ** 0.5)
    
    # Make it iterable
    def __iter__(self):
        yield self.x
        yield self.y
    
    # Index access
    def __getitem__(self, index):
        return (self.x, self.y)[index]

v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)        # Vector(6, 8)
print(v1 * 3)         # Vector(6, 9)
print(v1 == Vector(2, 3))  # True
print(len(v1))        # 3
x, y = v1             # unpacking via __iter__
09

에러 처리

Try / Except / Finally

try/except/else/finally: try는 위험한 코드를 실행하고 except는 에러를 잡고 else는 예외가 없으면 실행되고 finally는 항상 실행됩니다 (정리). bare 'except:'가 아닌 특정 예외를 잡으세요. else 블록은 정리가 성공 시에만 발생해야 할 때 유용합니다. Exception은 대부분의 잡을 수 있는 에러의 기반입니다.

python
# Basic exception handling
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")  # division by zero
finally:
    print("This always runs")

# Multiple exception types
try:
    value = int("abc")
except (ValueError, TypeError) as e:
    print(f"Conversion error: {e}")

# Different handlers for different exceptions
try:
    f = open("nonexistent.txt")
    data = f.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("No permission")
except Exception as e:
    print(f"Unexpected: {e}")
else:
    print("No exception occurred")
    f.close()
finally:
    print("Cleanup (always runs)")

# Exception hierarchy
# BaseException
#  ├── SystemExit
#  ├── KeyboardInterrupt
#  └── Exception
#       ├── ValueError
#       ├── TypeError
#       ├── KeyError
#       └── ...

예외 발생

예외를 던지려면 raise를 사용하세요. 단독 'raise'는 (except 블록 내에서) 현재 예외를 재발생시킵니다. 'raise X from Y'는 예외를 연결하여 원래 원인을 보존합니다. 항상 특정 예외 타입을 발생시키세요. 정상적인 제어 흐름에 예외를 사용하지 마세요.

python
# Raise an exception
def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero!")
    return a / b

# Re-raise the current exception
def process(data):
    try:
        return parse(data)
    except ValueError:
        print("Logging parse error...")
        raise  # re-raises the same exception

# Raise with context (from)
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("Failed to process input") from e

# Common built-in exceptions
raise ValueError("invalid value")
raise TypeError("wrong type")
raise KeyError("missing key")
raise IndexError("out of range")
raise RuntimeError("something went wrong")
raise NotImplementedError("override this")
raise FileNotFoundError("no such file")

# Exception with custom args
class ValidationError(Exception):
    pass

raise ValidationError("field is required", "email")

사용자 정의 예외

Exception(또는 더 구체적인 내장)을 하위 클래스화하여 사용자 정의 예외를 만드세요. 호출자가 적절한 수준에서 잡을 수 있도록 계층을 설계하세요. 컨텍스트를 전달하기 위해 사용자 정의 속성을 추가하세요. BaseException이 아닌 Exception에서 상속하세요 (SystemExit/KeyboardInterrupt 포함).

python
# Custom exception hierarchy
class AppError(Exception):
    """Base exception for the application."""
    pass

class DatabaseError(AppError):
    def __init__(self, message, query=None):
        super().__init__(message)
        self.query = query

class ValidationError(AppError):
    def __init__(self, field, message):
        super().__init__(f"{field}: {message}")
        self.field = field
        self.message = message

class AuthenticationError(AppError):
    pass

# Usage
def login(username, password):
    if not username:
        raise ValidationError("username", "is required")
    if password != "secret":
        raise AuthenticationError("Invalid credentials")

# Catching by hierarchy
try:
    login("", "x")
except ValidationError as e:
    print(f"Validation failed: {e.field}")
except AppError as e:
    print(f"App error: {e}")

# Access exception info
import traceback
try:
    1 / 0
except:
    traceback.print_exc()
    print(repr(sys.exc_info()[1]))

컨텍스트 매니저 (with 문)

컨텍스트 매니저('with' 문)는 __enter__와 __exit__를 통해 정리를 보장합니다. 파일, 잠금, 데이터베이스 연결과 같은 리소스에 필수적입니다. contextlib.contextmanager는 제너레이터를 사용하여 생성을 단순화합니다. __exit__는 True를 반환하여 예외를 억제할 수 있습니다.

python
# Context managers handle setup and cleanup
with open("file.txt") as f:
    content = f.read()
# file is automatically closed, even if an error occurs

# Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
    fout.write(fin.read())

# Creating a context manager (class-based)
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.time() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False  # don't suppress exceptions

with Timer() as t:
    # code to time
    sum(range(1000000))

# contextlib for simpler context managers
from contextlib import contextmanager

@contextmanager
def open_db(url):
    db = connect(url)
    try:
        yield db
    finally:
        db.close()

with open_db("localhost") as db:
    db.query("SELECT 1")

어서션 & 로깅

assert 문은 불변 조건을 디버깅하기 위한 것입니다 — Python이 -O(최적화)로 실행될 때 제거됩니다. 입력 검증에 어서션을 사용하지 마세요. 프로덕션 코드에서는 print() 대신 logging 모듈을 사용하세요 — 레벨, 형식, 출력 대상을 지원합니다.

python
# Assertions - for debugging (removed with -O flag)
def divide(a, b):
    assert b != 0, "Divisor cannot be zero"
    return a / b

# Never use assertions for data validation (they can be disabled)
# Use them for invariant checks during development

# Logging (better than print for production)
import logging

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

logger = logging.getLogger(__name__)

logger.debug("Detailed info for debugging")
logger.info("Confirmation things are working")
logger.warning("Something unexpected happened")
logger.error("A serious problem")
logger.critical("A fatal error")

# Log exceptions with traceback
try:
    1 / 0
except:
    logger.exception("Failed to divide")  # includes traceback
10

파일 I/O

파일 읽기

항상 'with'로 파일을 여세요 — 에러가 발생해도 자동으로 닫습니다. 플랫폼 의존적 인코딩 문제를 피하기 위해 encoding='utf-8'을 지정하세요. 큰 파일의 경우 메모리를 절약하기 위해 read() 대신 줄 단위로 반복하세요. readlines()는 전체 파일을 메모리에 로드합니다.

python
# Read entire file
with open("file.txt", "r", encoding="utf-8") as f:
    content = f.read()
    print(content)

# Read line by line (memory-efficient for large files)
with open("file.txt", "r") as f:
    for line in f:
        print(line.strip())  # strip removes trailing newline

# Read all lines into a list
with open("file.txt") as f:
    lines = f.readlines()  # ['line1\n', 'line2\n', ...]

# Read specific number of characters
with open("file.txt") as f:
    chunk = f.read(100)  # first 100 chars

# File modes:
# "r"  read (default)
# "w"  write (truncate)
# "a"  append
# "x"  exclusive create (fails if exists)
# "b"  binary mode (e.g., "rb", "wb")
# "+"  read and write (e.g., "r+")

파일 쓰기

모드 'w'는 파일을 자릅니다 (내용 삭제); 추가하려면 'a'를 사용하세요. writelines()는 줄바꿈을 추가하지 않습니다 — 수동으로 추가하세요. 바이너리 파일(이미지 등)에는 'rb'/'wb'를 사용하세요. seek()는 커서를 이동하고 tell()은 위치를 반환합니다. 텍스트 파일에는 항상 인코딩을 지정하세요.

python
# Write text (overwrites existing)
with open("output.txt", "w") as f:
    f.write("First line\n")
    f.write("Second line\n")
    
    # writelines doesn't add newlines
    f.writelines(["line3\n", "line4\n"])

# Append to a file
with open("log.txt", "a") as f:
    f.write("New log entry\n")

# Write binary data
with open("data.bin", "wb") as f:
    f.write(b"\x00\x01\x02\x03")

# Read and write simultaneously
with open("file.txt", "r+") as f:
    content = f.read()
    f.seek(0)           # move to beginning
    f.write("Updated")  # overwrite
    f.truncate()        # cut off remaining

# Check if file exists
import os
if os.path.exists("file.txt"):
    print("File exists")

경로 처리 (pathlib)

pathlib(Python 3.4+)는 경로를 처리하는 현대적이고 객체 지향적인 방법입니다 — os.path보다 선호하세요. / 연산자는 플랫폼 독립적으로 경로를 결합합니다. Path 객체에는 open/close를 처리하는 read_text()/write_text() 메서드가 있습니다. rglob()는 재귀적으로 검색합니다.

python
from pathlib import Path

# Create Path objects (preferred over os.path)
p = Path("src/main.py")
home = Path.home()           # /home/user or C:\Users\user
cwd = Path.cwd()             # current working directory

# Path components
print(p.name)        # main.py
print(p.stem)        # main
print(p.suffix)      # .py
print(p.parent)      # src
print(p.parts)       # ('src', 'main.py')

# Joining paths (use / operator)
config = home / ".config" / "app" / "config.json"

# Existence and type
print(p.exists())    # True/False
print(p.is_file())
print(p.is_dir())

# Listing directories
for f in Path(".").iterdir():
    print(f)

# Glob patterns
for py_file in Path(".").rglob("*.py"):
    print(py_file)

# Create directories
Path("new/dir").mkdir(parents=True, exist_ok=True)

# Read/write (Path methods)
content = Path("file.txt").read_text()
Path("output.txt").write_text("Hello!")

JSON

json.dumps()는 문자열로 직렬화하고 json.loads()는 역직렬화합니다. dump()/load()는 파일과 작동합니다. pretty-printing에는 indent를 사용하세요. 사용자 정의 객체에는 기본 직렬 변환기가 필요합니다. JSON은 기본 타입만 지원합니다 — datetime 및 기타 복잡한 객체에는 default=를 사용하세요.

python
import json

# Python dict to JSON string
data = {"name": "Alice", "age": 30, "skills": ["Python", "SQL"]}
json_str = json.dumps(data, indent=2)
print(json_str)

# JSON string to Python dict
parsed = json.loads('{"name": "Bob", "active": true}')
print(parsed["name"])  # Bob
print(parsed["active"])  # True (Python bool)

# Write JSON to file
with open("data.json", "w") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Read JSON from file
with open("data.json") as f:
    loaded = json.load(f)

# Custom serialization (e.g., datetime)
from datetime import datetime
def json_default(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError

json.dumps({"time": datetime.now()}, default=json_default)

# Type mapping:
# JSON object  <-> Python dict
# JSON array   <-> Python list
# JSON string  <-> Python str
# JSON number  <-> Python int/float
# JSON boolean <-> Python bool
# JSON null    <-> Python None

CSV & 기타 형식

csv 모듈은 적절한 인용부호로 CSV를 처리합니다. Windows에서 CSV 파일을 열 때 newline=''을 사용하세요. DictReader/DictWriter는 열 이름으로 작동합니다. pickle은 모든 Python 객체를 직렬화할 수 있지만 Python 전용이고 안전하지 않습니다 — 신뢰할 수 없는 출처의 데이터를 unpickle하지 마세요.

python
import csv

# Write CSV
with open("data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "age", "city"])
    writer.writerows([
        ["Alice", 30, "NYC"],
        ["Bob", 25, "LA"],
    ])

# Read CSV
with open("data.csv") as f:
    reader = csv.reader(f)
    header = next(reader)  # first row
    for row in reader:
        print(row)  # ['Alice', '30', 'NYC']

# DictReader/DictWriter (column access by name)
with open("data.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["age"])

# Pickle (Python-specific, can store any object)
import pickle
with open("data.pkl", "wb") as f:
    pickle.dump({"complex": [1, 2, {"a": 3}]}, f)

with open("data.pkl", "rb") as f:
    obj = pickle.load(f)

# WARNING: pickle is insecure - never unpickle untrusted data!
11

모듈 & 패키지

모듈 가져오기

import는 모듈을 가져옵니다. 'import X'는 네임스페이스를 깨끗하게 유지합니다. 'from X import Y'는 편리하지만 이름 충돌을 일으킬 수 있습니다. 별칭(import X as Y)은 규칙이 있는 라이브러리(np, pd)에 일반적입니다. 'from X import *'는 피하세요 — 네임스페이스를 오염시킵니다.

python
# Import entire module
import math
print(math.sqrt(16))

# Import specific names
from datetime import datetime, timedelta
now = datetime.now()

# Import with alias
import numpy as np
import pandas as pd

# Import all names (discouraged - pollutes namespace)
# from os import *

# Conditional import (try/except)
try:
    import cjson as json
except ImportError:
    import json

# Check what's in a module
import os
print(dir(os))         # list all attributes
print(os.__file__)     # module location
print(os.__name__)     # module name

# Reload a module (during development)
import importlib
importlib.reload(my_module)

모듈 & 패키지 생성

모듈은 .py 파일이고 패키지는 __init__.py가 있는 디렉토리입니다. __init__.py 파일은 비어 있거나 패키지를 설정할 수 있습니다. __init__.py의 __all__은 'from package import *'가 내보내는 것을 제어합니다. 최신 Python(3.3+)은 __init__.py 없이 네임스페이스 패키지를 지원합니다.

python
# A module is just a .py file
# mymath.py
def add(a, b):
    return a + b

PI = 3.14159

# A package is a directory with __init__.py
# mypackage/
#   __init__.py
#   module1.py
#   module2.py
#   subpackage/
#     __init__.py
#     module3.py

# __init__.py can be empty or contain package initialization
# mypackage/__init__.py
from .module1 import ClassA
from .module2 import func_b
__version__ = "1.0.0"
__all__ = ["ClassA", "func_b"]

# Using the package
from mypackage import ClassA
from mypackage.subpackage import module3

# __all__ controls 'from package import *'
# Without __all__, * imports only what's in __init__.py

__name__ == '__main__'

if __name__ == '__main__' 관용구는 파일이 스크립트와 모듈 모두로 작동하게 합니다. 직접 실행하면 __name__이 '__main__'이고 가져오면 모듈 이름이 됩니다. 이 패턴은 단독으로 실행될 수도 있는 재사용 가능한 모듈을 만드는 데 필수적입니다.

python
# script.py
def main():
    print("Running main")

def helper():
    print("Helper function")

if __name__ == "__main__":
    # This code only runs when the file is executed directly
    # NOT when imported as a module
    main()

# When you run: python script.py
# __name__ is "__main__" -> main() runs

# When you: import script
# __name__ is "script" -> main() does NOT run
# This lets the module be both a script and an importable library

# Common pattern for CLI tools
def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--name", required=True)
    args = parser.parse_args()
    print(f"Hello, {args.name}!")

if __name__ == "__main__":
    main()

표준 라이브러리 하이라이트

Python의 표준 라이브러리는 거대하고 배터리 포함입니다. 시스템 상호작용에는 os/sys, 날짜에는 datetime, 특수 컨테이너에는 collections, 이터레이터 도구에는 itertools, 함수형 프로그래밍에는 functools를 사용하세요. docs.python.org/3/library/에서 문서를 탐색하세요.

python
# os - operating system interface
import os
os.getcwd()           # current directory
os.listdir(".")       # list files
os.environ.get("HOME")  # environment variables

# sys - system-specific
import sys
sys.argv              # command-line arguments
sys.exit(0)           # exit with status code
sys.path              # module search path

# datetime - date and time
from datetime import datetime, timedelta
now = datetime.now()

# collections - specialized containers
from collections import Counter, defaultdict, deque

# itertools - iterator tools
from itertools import chain, cycle, repeat, product

# functools - higher-order functions
from functools import lru_cache, reduce, partial

# typing - type hints
from typing import List, Dict, Optional, Union, Any

# pathlib - path handling
from pathlib import Path

# subprocess - run external commands
import subprocess
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)

Pip & 가상 환경

항상 가상 환경을 사용하여 프로젝트 종속성을 격리하세요. venv는 내장되어 있습니다; 대안으로 virtualenv, conda, uv가 있습니다. 재현성을 위해 requirements.txt에 버전을 고정하세요. --user나 root로 전역 패키지를 설치하지 마세요 — venv를 사용하세요.

python
# Create a virtual environment
# $ python -m venv venv

# Activate it
# Windows: venv\Scripts\activate
# Unix:    source venv/bin/activate

# Install packages
# $ pip install requests
# $ pip install requests==2.28.0
# $ pip install "requests>=2.25,<3.0"

# Install from requirements file
# $ pip install -r requirements.txt

# requirements.txt example:
# requests==2.31.0
# numpy>=1.21.0
# pandas~=2.0.0  # compatible release

# List installed packages
# $ pip list
# $ pip freeze > requirements.txt

# Uninstall
# $ pip uninstall requests

# Show package info
# $ pip show requests

# Modern alternative: uv (faster)
# $ uv pip install requests
12

날짜 & 시간

datetime 모듈

datetime 모듈은 date, time, datetime, timedelta 클래스를 제공합니다. datetime.now()는 현지 시간을 반환하고 UTC에는 datetime.now(timezone.utc)를 사용하세요. weekday()는 0-6(월-일)을 반환합니다. 모호성을 피하기 위해 프로덕션에서는 항상 timezone-aware datetime을 사용하세요.

python
from datetime import datetime, date, time, timedelta

# Current date and time
now = datetime.now()          # local time
utc_now = datetime.utcnow()   # UTC (deprecated in 3.12)
utc = datetime.now(timezone.utc)  # preferred

# Current date
today = date.today()

# Create specific date/time
dt = datetime(2024, 1, 15, 10, 30, 0)
d = date(2024, 1, 15)
t = time(10, 30, 0)

# Access components
print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)
print(now.weekday())  # 0=Monday, 6=Sunday

# From timestamp
ts = 1705315200
dt = datetime.fromtimestamp(ts)

# To timestamp
print(datetime.now().timestamp())

형식 지정 & 파싱

strftime(문자열 형식 시간)은 datetime을 문자열로 변환하고 strptime(문자열 파싱 시간)은 문자열을 datetime으로 변환합니다. ISO 8601 형식(isoformat/fromisoformat)은 날짜를 저장하는 최적의 선택입니다 — 명확하고 정렬 가능합니다. 일반적인 코드를 암기하세요: %Y %m %d %H %M %S.

python
from datetime import datetime

# Format datetime to string (strftime)
dt = datetime(2024, 1, 15, 10, 30)
print(dt.strftime("%Y-%m-%d"))         # 2024-01-15
print(dt.strftime("%Y/%m/%d %H:%M"))   # 2024/01/15 10:30
print(dt.strftime("%B %d, %Y"))        # January 15, 2024
print(dt.strftime("%A"))               # Monday

# Parse string to datetime (strptime)
dt = datetime.strptime("2024-01-15", "%Y-%m-%d")
dt = datetime.strptime("15/01/2024 10:30", "%d/%m/%Y %H:%M")

# Common format codes:
# %Y  year (2024)     %m  month (01)
# %d  day (15)        %H  hour (14)
# %M  minute (30)     %S  second (00)
# %B  month name      %b  month abbrev
# %A  weekday name    %a  weekday abbrev
# %I  12-hour         %p  AM/PM
# %j  day of year     %U  week number

# ISO format (recommended for storage)
iso = dt.isoformat()  # "2024-01-15T10:30:00"
dt = datetime.fromisoformat("2024-01-15T10:30:00")

timedelta & 산술

timedelta는 기간을 나타냅니다. datetime에서 timedelta를 더하거나 뺄 수 있고 두 datetime을 빼서 timedelta를 얻을 수 있습니다. timedelta는 정규화합니다: days=1, hours=25는 days=2, hours=1이 됩니다. total_seconds()는 전체 기간을 초 단위로 제공합니다.

python
from datetime import datetime, timedelta

now = datetime.now()

# Add/subtract time
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_2_hours = now + timedelta(hours=2)
in_90_days = now + timedelta(days=90)

# Difference between dates
date1 = datetime(2024, 1, 15)
date2 = datetime(2024, 6, 18)
diff = date2 - date1
print(diff.days)         # 155
print(diff.total_seconds())

# timedelta components
td = timedelta(days=5, hours=3, minutes=30)
print(td.days)           # 5
print(td.seconds)        # 12600 (3h 30m in seconds)
print(td.total_seconds())

# Comparisons
if now > date1:
    print("now is later")

# Business day calculation (using numpy)
# import numpy as np
# business_days = np.busday_count(date1.date(), date2.date())

시간대

항상 timezone-aware datetime을 사용하세요(Python 3.9+ ZoneInfo가 pytz보다 선호됨). 날짜는 UTC로 저장하고 표시용으로만 현지 시간으로 변환하세요. Naive datetime(tzinfo 없음)은 미묘한 버그를 일으킵니다. ZoneInfo는 IANA 시간대 데이터베이스를 사용하여 DST를 자동으로 처리합니다.

python
from datetime import datetime, timezone, timedelta

# Timezone-aware datetime
utc_time = datetime.now(timezone.utc)
print(utc_time)  # 2024-01-15 10:30:00+00:00

# Create a timezone (offset-based)
tz_ny = timezone(timedelta(hours=-5), "EST")
ny_time = datetime.now(tz_ny)

# Convert between timezones
utc_time = datetime.now(timezone.utc)
ny_time = utc_time.astimezone(timezone(timedelta(hours=-5)))
tokyo_time = utc_time.astimezone(timezone(timedelta(hours=9)))

# Use zoneinfo (Python 3.9+) for IANA timezones
from zoneinfo import ZoneInfo
tz = ZoneInfo("America/New_York")
dt = datetime.now(tz)
print(dt.tzname())  # EST or EDT

# Common IANA timezones:
# "UTC"
# "America/New_York", "America/Los_Angeles"
# "Europe/London", "Europe/Paris"
# "Asia/Tokyo", "Asia/Shanghai"
# "Australia/Sydney"

# Best practice: store UTC, convert for display
utc_stored = datetime.now(timezone.utc)
local_display = utc_stored.astimezone(ZoneInfo("Asia/Shanghai"))
13

정규 표현식

re 모듈 기본

re.search()는 어디서나 첫 번째 매칭을 찾고 re.match()는 시작 부분에서만, re.fullmatch()는 전체 문자열이 매칭되어야 합니다. 백슬래시 이스케이프 문제를 피하기 위해 패턴에 raw 문자열(r'...')을 사용하세요. 매치 객체는 group(), start(), end(), span()을 제공합니다.

python
import re

# re.search - find first match anywhere in string
m = re.search(r"\d{4}", "Order #2024 was placed")
if m:
    print(m.group())  # 2024
    print(m.start(), m.end())  # 9 13

# re.match - match at beginning of string
m = re.match(r"Hello", "Hello, World")
print(m.group())  # Hello

# re.fullmatch - entire string must match
m = re.fullmatch(r"\d+", "12345")
print(bool(m))  # True

# re.findall - all matches as list
emails = re.findall(r"\S+@\S+", text)
numbers = re.findall(r"\d+", "a1b22c333")  # ['1', '22', '333']

# re.finditer - all matches as iterator (with positions)
for m in re.finditer(r"\w+", "Hello World"):
    print(m.group(), m.span())

# Match object methods
m = re.search(r"(\w+)@(\w+)", "[email protected]")
print(m.group())   # user@domain (whole match)
print(m.group(1))  # user (first group)
print(m.group(2))  # domain (second group)
print(m.groups())  # ('user', 'domain')

패턴 구문

정규식 구문: 문자 클래스는 [], 일반 집합은 \d \w \s, 반복은 수량자(* + ? {}), 앵커는 ^ $ \b, 그룹은 (), 대안은 |. 백슬래시가 문자 그대로 되도록 raw 문자열(r'...')을 사용하세요. 탐욕적 수량자는 가능한 많이 매칭합니다; 지연 평가에는 ?를 추가하세요 (예: *?).

python
import re

# Character classes
re.findall(r"[aeiou]", "hello")      # vowels
re.findall(r"[^aeiou]", "hello")     # non-vowels
re.findall(r"[a-z]", "Hello123")     # lowercase
re.findall(r"[A-Za-z0-9]", "Hi-1!")  # alphanumeric

# Predefined classes
# .   any char except newline
# \d  digit [0-9]      \D  non-digit
# \w  word char [a-zA-Z0-9_]  \W  non-word
# \s  whitespace       \S  non-whitespace

# Quantifiers
# *     0 or more
# +     1 or more
# ?     0 or 1
# {n}   exactly n
# {n,}  n or more
# {n,m} between n and m
re.findall(r"\d{3}", "1234567")      # ['123', '456']
re.findall(r"\d{2,4}", "12345678")   # ['1234', '5678']

# Anchors
# ^  start of string    $  end of string
# \b word boundary
re.findall(r"^\w+", "Hello World")   # ['Hello']
re.findall(r"\b\w+\b", "hi there") # ['hi', 'there']

# Groups & alternation
re.findall(r"(cat|dog)", "cat and dog")  # ['cat', 'dog']
re.findall(r"(\w+)@(\w+\.\w+)", "[email protected]")

치환 & 분할

re.sub()는 매칭을 치환합니다 — 그룹을 참조하려면 역참조(\1, \2)를, 동적 치환에는 함수를 사용하세요. re.split()은 str.split()보다 강력합니다 — 정규식 패턴을 허용합니다. 패턴의 캡처 그룹은 결과에 포함됩니다. (결과, 카운트)를 얻으려면 re.subn()을 사용하세요.

python
import re

# re.sub - replace matches
result = re.sub(r"\d+", "#", "a1b22c333")
# 'a#b#c#'

# Replace with count limit
result = re.sub(r"\d", "X", "a1b2c3", count=2)
# 'aXbXc3'

# Use backreferences in replacement
result = re.sub(r"(\w+)@(\w+)", r"\2.\1", "user@domain")
# 'domain.user'

# Use function as replacement
def upper(m):
    return m.group().upper()

result = re.sub(r"\b[a-z]", upper, "hello world")
# 'Hello World' (capitalize first letter of each word)

# re.split - split by pattern
parts = re.split(r"[,;\s]+", "a, b; c d")
# ['a', 'b', 'c', 'd']

# Split with capture groups (keeps delimiters)
parts = re.split(r"([,;])", "a,b;c")
# ['a', ',', 'b', ';', 'c']

# Split with maxsplit
parts = re.split(r",", "a,b,c,d", maxsplit=2)
# ['a', 'b', 'c,d']

컴파일 & 플래그

반복적으로 사용할 때는 re.compile()로 패턴을 컴파일하세요 — 더 빠릅니다. 플래그는 동작을 수정합니다: IGNORECASE, MULTILINE, DOTALL, VERBOSE(패턴에 주석/공백 허용). 명명된 그룹(?P<name>...)은 가독성을 향상시킵니다. 룩어헤드(?=)와 룩비하인드(?<=)는 소비 없이 매칭합니다.

python
import re

# Compile pattern for reuse (faster when used many times)
email_re = re.compile(r"^[\w.+-]+@([\w-]+\.)+[\w-]+$")
print(email_re.match("[email protected]"))  # match object
print(email_re.match("invalid"))           # None

# Common flags
re.IGNORECASE  # case-insensitive
re.MULTILINE   # ^ and $ match line boundaries
re.DOTALL      # . matches newline too
re.VERBOSE     # allow whitespace & comments in pattern

# Combine flags with |
pattern = re.compile(r"""
    ^           # start of line
    (\w+)      # capture word
    \s+        # whitespace
    (\d+)      # capture number
""", re.VERBOSE | re.MULTILINE)

# Named groups (more readable)
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2024-01")
print(m.group("year"))   # 2024
print(m.group("month"))  # 01
print(m.groupdict())     # {'year': '2024', 'month': '01'}

# Lookahead/lookbehind
re.findall(r"\d+(?= dollars)", "100 dollars, 200 euros")
# ['100']  (positive lookahead)
re.findall(r"(?<=\$)\d+", "$100 and $200")
# ['100', '200']  (positive lookbehind)
14

Async & 동시성

asyncio 기본

asyncio는 Python의 비동기 I/O 프레임워크입니다. 'async def'는 코루틴을 정의하고 'await'는 결과가 준비될 때까지 일시 중단합니다. asyncio.run()은 이벤트 루프를 시작합니다. asyncio.gather()는 코루틴을 동시에 실행합니다. 코루틴은 스레드 없이 고동시성 I/O를 가능하게 합니다.

python
import asyncio

# Define a coroutine
async def greet(name, delay):
    await asyncio.sleep(delay)  # non-blocking sleep
    return f"Hello, {name}!"

# Run a coroutine
async def main():
    result = await greet("Alice", 1)
    print(result)

# Run the event loop
asyncio.run(main())

# Concurrent execution with gather
async def main():
    # Run coroutines concurrently
    results = await asyncio.gather(
        greet("Alice", 2),
        greet("Bob", 1),
        greet("Carol", 3)
    )
    print(results)  # all complete after 3 seconds (max delay)

asyncio.run(main())

# asyncio.create_task - schedule without awaiting immediately
async def main():
    task = asyncio.create_task(greet("Alice", 1))
    # do other work here
    result = await task  # await when needed
    print(result)

Async HTTP & 타임아웃

asyncio.timeout()(Python 3.11+)는 너무 오래 걸리는 작업을 취소합니다. HTTP의 경우 requests(동기) 대신 aiohttp(비동기)를 사용하세요. asyncio.Queue는 생산자-소비자 패턴을 가능하게 합니다. Async는 I/O 바운드 작업(네트워크, 디스크)에 이상적입니다 — CPU 바운드 작업에는 아닙니다.

python
import asyncio

# Async timeout
async def fetch_with_timeout(url, timeout=5):
    try:
        async with asyncio.timeout(timeout):
            # simulate async operation
            await asyncio.sleep(2)
            return f"Data from {url}"
    except asyncio.TimeoutError:
        return "Request timed out"

# Using aiohttp (third-party) for HTTP
# import aiohttp
# 
# async def fetch(url):
#     async with aiohttp.ClientSession() as session:
#         async with session.get(url) as response:
#             return await response.text()

# Producer-consumer pattern
async def producer(queue):
    for i in range(5):
        await asyncio.sleep(0.1)
        await queue.put(f"item-{i}")
    await queue.put(None)  # sentinel

async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f"Processed: {item}")
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    await asyncio.gather(producer(queue), consumer(queue))

asyncio.run(main())

스레딩

스레딩은 I/O 바운드 동시성(네트워크, 파일 I/O)을 위한 것입니다. Python의 GIL은 스레드에서 진정한 병렬 CPU 실행을 방지합니다. 경쟁 조건으로부터 공유 상태를 보호하려면 Lock을 사용하세요. CPU 바운드 작업에는 대신 multiprocessing을 사용하세요. 스레드는 메모리를 공유하고 프로세스는 그렇지 않습니다.

python
import threading
import time

# Basic threading
def worker(name, delay):
    print(f"Worker {name} starting")
    time.sleep(delay)
    print(f"Worker {name} done")

# Create and start threads
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))
t1.start()
t2.start()

# Wait for threads to complete
t1.join()
t2.join()
print("All done")

# Thread with Lock (for shared state)
counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # acquire/release lock
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Counter: {counter}")  # 500000 (correct with lock)

멀티프로세싱

멀티프로세싱은 진정한 CPU 병렬성을 위해 GIL을 우회합니다 — 각 프로세스는 자체 Python 인터프리터를 가집니다. 병렬 map 작업에는 Pool을 사용하세요. concurrent.futures는 스레드와 프로세스 모두에 대한 통합 API를 제공합니다. Windows에서는 항상 if __name__ == '__main__'으로 보호하세요.

python
from multiprocessing import Process, Pool, Queue
import os

# Basic process
def worker(name):
    print(f"Process {name} PID: {os.getpid()}")

if __name__ == "__main__":
    p = Process(target=worker, args=("A",))
    p.start()
    p.join()

# Process pool for parallel work
def square(x):
    return x * x

if __name__ == "__main__":
    with Pool(4) as pool:  # 4 worker processes
        results = pool.map(square, range(10))
        print(results)  # [0, 1, 4, 9, ..., 81]
    
    # Asynchronous map
    with Pool(4) as pool:
        result = pool.map_async(square, range(10))
        print(result.get())  # blocks until done

# concurrent.futures (higher-level API)
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

with ProcessPoolExecutor() as executor:
    results = list(executor.map(square, range(10)))

with ThreadPoolExecutor() as executor:
    futures = [executor.submit(square, i) for i in range(10)]
    results = [f.result() for f in futures]
15

데코레이터

기본 데코레이터

데코레이터는 원래 소스를 변경하지 않고 함수를 감싸 동작을 확장하거나 수정합니다. @구문은 데코레이터 호출 결과를 함수 이름에 다시 할당하는 syntactic sugar입니다. 모든 서명으로 작동하도록 래퍼에 *args, **kwargs를 사용하세요.

python
# A decorator is a function that takes a function and returns a new function
def uppercase_result(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_result
def greet(name):
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
# @uppercase_result is sugar for: greet = uppercase_result(greet)

functools.wraps (메타데이터 보존)

@wraps가 없으면 래핑된 함수는 원래 __name__, __doc__, 시그니처를 잃습니다 — 디버깅 도구와 help()는 'wrapper'를 표시합니다. 메타데이터를 보존하려면 데코레이터 내에서 항상 @functools.wraps(func)를 사용하세요. 이는 거의 보편적인 모범 사례입니다.

python
from functools import wraps

def log_calls(func):
    @wraps(func)  # copies __name__, __doc__, __module__
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}({args}, {kwargs})")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)  # 'add' (not 'wrapper')
print(add.__doc__)   # 'Add two numbers.'
help(add)            # shows original docstring

인수가 있는 데코레이터

데코레이터가 인수를 받으면 세 단계의 중첩이 필요합니다: 팩토리(인수 받음), 데코레이터(함수 받음), 래퍼(호출 인수 받음). @repeat(3)은 먼저 repeat(3)을 호출하여 데코레이터를 반환하고 이를 함수에 적용합니다.

python
# A decorator factory: returns the actual decorator
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hi(name):
    print(f"Hi, {name}!")

say_hi("Alice")
# Hi, Alice! (printed 3 times)

클래스 기반 데코레이터

클래스 데코레이터는 __init__으로 함수를 저장하고 __call__로 호출을 가로챕니다. 데코레이터가 상태를 유지해야 할 때(호출 카운터나 캐시 같은) 이상적입니다. 클래스 인스턴스가 함수를 대체하므로 호출하면 __call__이 트리거됩니다.

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
        wraps(func)(self)  # preserve metadata

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} times")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()  # count=1
say_hello()  # count=2
say_hello()  # count=3
print(say_hello.count)  # 3

내장 데코레이터 (@property, @staticmethod, @classmethod)

@property는 메서드를 계산된 속성으로 변환합니다 (괄호 없이 접근). @classmethod는 첫 번째 인수로 클래스를 받습니다 — 대체 생성자에 완벽합니다. @staticmethod는 암시적 첫 번째 인수를 받지 않습니다 — 클래스 네임스페이스에 있는 함수일 뿐입니다. 이들은 Pythonic OOP의 근간을 형성합니다.

python
class Circle:
    pi = 3.14159

    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return Circle.pi * self._radius ** 2

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @classmethod
    def from_diameter(cls, diameter):
        return cls(diameter / 2)

    @staticmethod
    def is_valid_radius(r):
        return r >= 0

c = Circle(5)
print(c.area)              # 78.54 (no parentheses!)
c.radius = 10             # uses the setter
c2 = Circle.from_diameter(20)  # alternative constructor

스택 데코레이터

데코레이터를 쌓을 때 아래에서 위로 적용됩니다 (함수에 가장 가까운 것이 먼저 실행)하지만 호출 시 위에서 아래로 실행됩니다. 따라서 @bold가 @italic이 greet를 감쌉니다. 결과는 양파 껍질처럼 중첩됩니다. 순서가 중요합니다 — 뒤집으면 출력 중첩이 변경됩니다.

python
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("World"))
# <b><i>Hello, World</i></b>
# Applied bottom-up: italic first, then bold
16

제너레이터 & 이터레이터

제너레이터 함수 (yield)

제너레이터는 yield를 사용하여 지연 평가로 값을 생성합니다 — 각 yield 후 실행을 일시 중단하고 next()가 호출되면 재개합니다. 이는 한 번에 하나의 값만 메모리에 존재하므로 크거나 무한 시퀀스에 메모리 효율적입니다. 한 번 소진되면 제너레이터는 재사용할 수 없습니다.

python
# A generator uses 'yield' to produce values lazily, one at a time
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

gen = count_up_to(5)
print(next(gen))  # 1
print(next(gen))  # 2
print(list(gen))  # [3, 4, 5] (exhausts the rest)

# Generators are memory-efficient: they don't build the whole list
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

제너레이터 표현식

제너레이터 표현식은 리스트 컴프리헨션의 지연 평가 버전입니다 — 대괄호 대신 괄호를 사용하세요. 크기에 관계없이 일정한 메모리를 사용하여 sum(), max(), any()에 이상적이거나 다른 이터레이터에 공급하기에 좋습니다. 임의 접근이 필요하지 않을 때 리스트 컴프리헨션보다 선호하세요.

python
# Like list comprehensions, but lazy (uses parentheses)
squares_list = [x ** 2 for x in range(10)]      # builds full list
squares_gen  = (x ** 2 for x in range(10))      # lazy generator

print(squares_gen)        # <generator object>
print(next(squares_gen))  # 0
print(next(squares_gen))  # 1

# Memory comparison
import sys
print(sys.getsizeof([x for x in range(10000)]))  # ~87616 bytes
print(sys.getsizeof((x for x in range(10000))))  # ~200 bytes (constant!)

# Use in sum(), list(), any() etc.
total = sum(x ** 2 for x in range(100))  # no extra list created
print(total)  # 328350

이터레이터 프로토콜 (__iter__, __next__)

이터레이터 프로토콜은 __iter__(이터레이터 반환)과 __next__(다음 값 반환 또는 StopIteration 발생)를 요구합니다. 반복 가능한 객체는 반복할 수 있고 이터레이터는 한 번에 하나의 값을 생성합니다. 재사용 가능한 반복 가능 객체의 경우 반복 가능 객체(새 이터레이터 반환)를 이터레이터(상태 보유)에서 분리하세요.

python
class Range2:
    """A custom iterator that yields even numbers."""
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self  # the object is its own iterator

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 2
        return value

r = Range2(0, 10)
for num in r:
    print(num)  # 0, 2, 4, 6, 8

# An iterable returns a fresh iterator each time __iter__ is called.
# An iterator returns itself and raises StopIteration when exhausted.

send(), throw(), close()

고급 제너레이터 메서드는 양방향 통신을 가능하게 합니다: send()는 제너레이터에 값을 전달하고(yield의 결과가 됨), throw()는 yield 지점에서 예외를 주입하고, close()는 제너레이터를 종료합니다. 보내기 전에 next()로 제너레이터를 '시동'해야 합니다. 이는 코루틴과 async 프레임워크를 구동합니다.

python
def echo():
    while True:
        received = yield  # yield without a value, receives via send()
        print(f"Echo: {received}")

gen = echo()
next(gen)          # prime the generator (advance to first yield)
gen.send("hello")  # Echo: hello
gen.send("world")  # Echo: world

# throw() injects an exception at the yield point
def safe_gen():
    try:
        while True:
            yield
    except ValueError:
        print("Caught ValueError inside generator")

g = safe_gen()
next(g)
g.throw(ValueError, "boom")  # Caught ValueError inside generator

# close() stops the generator (raises GeneratorExit)
gen.close()

제너레이터 파이프라인

제너레이터 파이프라인은 지연 평가 생산자를 연결하여 데이터가 한 번에 한 항목씩 단계를 통과하게 합니다 — 각 항목은 다음 항목이 읽히기 전에 완전히 처리됩니다. 이는 중간 리스트 구축을 피하고 스트리밍 데이터 처리의 기초입니다. Unix 파이프는 개념적으로 같은 방식으로 작동합니다.

python
# Chain generators to build data-processing pipelines
def numbers():
    for i in range(1, 11):
        yield i

def squared(seq):
    for n in seq:
        yield n ** 2

def evens(seq):
    for n in seq:
        if n % 2 == 0:
            yield n

# Each stage processes one item at a time — no intermediate lists
pipeline = evens(squared(numbers()))
print(list(pipeline))  # [4, 16, 36, 64, 100]

# Equivalent with generator expressions:
result = (n for n in (x ** 2 for x in range(1, 11)) if n % 2 == 0)
print(list(result))  # [4, 16, 36, 64, 100]

yield from (위임)

yield from은 모든 yield(및 send/throw/close)를 하위 이터레이터에 위임하여 중첩 구조를 평면화하고 코루틴을 구성합니다. 재귀 제너레이터에 특히 강력합니다 — 고전적인 사용 사례는 임의로 중첩된 리스트를 평면화하는 것입니다. async 코드에서 'await'는 같은 개념을 기반으로 합니다.

python
# yield from delegates to a sub-iterator (Python 3.3+)
def flatten(nested):
    for item in nested:
        if isinstance(item, (list, tuple)):
            yield from flatten(item)  # recursive delegation
        else:
            yield item

data = [1, [2, 3, [4, 5]], 6, [7, [8, [9]]]]
print(list(flatten(data)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# yield from also forwards send()/throw() to the sub-generator,
# making it essential for coroutine composition.
17

컨텍스트 매니저

with 문 기본

with 문은 예외가 발생해도 리소스가 해제되도록(파일 닫기, 잠금 해제, 연결 반환) 보장합니다. 시작에 __enter__를 끝에 __exit__를 호출합니다. 리소스 관리를 위해 수동 try/finally보다 항상 'with'를 선호하세요 — 더 안전하고 읽기 쉽습니다.

python
# 'with' guarantees cleanup even if an exception occurs
with open("data.txt", "r") as f:
    content = f.read()
# f.close() is called automatically here, even if read() raised

# Without 'with' you must manually close:
f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.close()  # easy to forget!

# Common built-in context managers:
with open("out.txt", "w") as f, open("in.txt") as g:
    f.write(g.read())  # both files close automatically

사용자 정의 컨텍스트 매니저 (클래스)

클래스 기반 컨텍스트 매니저는 __enter__(설정, 컨텍스트 객체 반환)와 __exit__(exc_type, exc_val, exc_tb)(정리)를 구현합니다. __exit__ 인수는 예외가 발생한 경우 예외 정보를 받습니다; True를 반환하면 억제합니다. 이 패턴은 데이터베이스 트랜잭션과 같은 복잡한 설정/해제에 이상적입니다.

python
class Timer:
    def __init__(self, label="Timer"):
        self.label = label

    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self  # value bound to 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.perf_counter() - self.start
        print(f"{self.label}: {elapsed:.4f}s")
        # Return False (or None) to propagate exceptions
        # Return True to suppress the exception
        return False

with Timer("Processing"):
    total = sum(i ** 2 for i in range(1_000_000))
# Processing: 0.1234s

contextlib.contextmanager

contextlib.contextmanager는 제너레이터 함수를 컨텍스트 매니저로 변환합니다 — yield 앞의 코드는 __enter__, yield 뒤의 코드(finally에서)는 __exit__입니다. 간단한 경우에 클래스보다 간결합니다. 'as' 변수에 값을 제공하려면 값을 yield하세요. 정리를 보장하려면 try/finally를 사용하세요.

python
from contextlib import contextmanager
import time

@contextmanager
def timer(label="Timer"):
    start = time.perf_counter()
    try:
        yield  # code inside the 'with' block runs here
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.4f}s")

with timer("My task"):
    sum(i ** 2 for i in range(1_000_000))
# My task: 0.1234s

# You can also yield a value to bind with 'as'
@contextmanager
def open_db(path):
    db = connect(path)
    try:
        yield db
    finally:
        db.close()

with open_db("app.db") as db:
    db.query("SELECT 1")

다중 컨텍스트 매니저

Python 3.10+는 더 깨끗한 구문을 위해 괄호로 묶은 여러 줄 'with' 문을 허용합니다. 동적 수의 컨텍스트 매니저의 경우 contextlib.ExitStack이 그룹으로 관리하고 역순으로 모두 해제합니다. 리소스 수가 런타임까지 알려지지 않은 경우 ExitStack이 필수적입니다.

python
# Python 3.10+ supports parenthesized context managers
with (
    open("input.txt") as fin,
    open("output.txt", "w") as fout,
):
    fout.write(fin.read())

# Pre-3.10: nest them or use contextlib.ExitStack
from contextlib import ExitStack

files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
    handles = [stack.enter_context(open(f)) for f in files]
    for h in handles:
        print(h.read())

contextlib 유틸리티 (suppress, redirect)

contextlib.suppress는 예상 예외에 대해 try/except/pass를 대체합니다 — 훨씬 더 읽기 쉽습니다. redirect_stdout/redirect_stderr는 콘솔로 갈 출력을 캡처하여 테스트나 로깅에 유용합니다. 이 유틸리티는 보일러플레이트를 피하고 의도를 명시합니다.

python
from contextlib import suppress, redirect_stdout, redirect_stderr
import io, warnings

# suppress: ignore specific exceptions (cleaner than try/except/pass)
with suppress(FileNotFoundError):
    os.remove("temp.txt")  # no error if file doesn't exist

# redirect_stdout: capture print output
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("This goes to the buffer, not console")
captured = buffer.getvalue()

# redirect_stderr: capture error/warning output
err_buf = io.StringIO()
with redirect_stderr(err_buf):
    warnings.warn("a warning")
print(err_buf.getvalue())  # the warning text

# Also: contextlib.chdir (3.11+) to temporarily change directory
# from contextlib import chdir
# with chdir("/tmp"): ...

Async 컨텍스트 매니저

Async 컨텍스트 매니저는 __aenter__/__aexit__('a' 접두사 주의)와 'async with' 문을 사용합니다. 데이터베이스 연결이나 HTTP 세션(예: aiohttp.ClientSession)과 같은 async 리소스 관리에 필수적입니다. 블록 내에서 await나 예외가 발생해도 정리가 실행됩니다.

python
import asyncio

class AsyncDB:
    async def __aenter__(self):
        print("connecting...")
        await asyncio.sleep(0.1)  # simulate async connect
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("closing...")
        await asyncio.sleep(0.1)  # simulate async close
        return False

    async def query(self, sql):
        return f"result of: {sql}"

async def main():
    async with AsyncDB() as db:
        result = await db.query("SELECT 1")
        print(result)

asyncio.run(main())
# connecting...
# result of: SELECT 1
# closing...
18

타입 힌트

기본 변수 & 함수 어노테이션

타입 힌트는 예상 타입을 문서화하지만 런타임에 강제되지 않습니다 — Python은 여전히 동적 타입입니다. 실행 전 타입 에러를 잡으려면 mypy나 pyright 같은 정적 검사기를 사용하세요. 내장 제네릭(list[str], dict[str, int])은 Python 3.9+가 필요합니다; 이전 버전은 typing.List, typing.Dict가 필요합니다.

python
# Variable annotations (Python 3.6+)
name: str = "Alice"
age: int = 30
scores: list[float] = [95.5, 88.0, 92.3]
config: dict[str, int] = {"timeout": 30}

# Function annotations
def greet(name: str, excited: bool = False) -> str:
    punctuation = "!" if excited else "."
    return f"Hello, {name}{punctuation}"

print(greet("World", excited=True))  # Hello, World!

# Annotations are optional and NOT enforced at runtime
def add(a: int, b: int) -> int:
    return a + b
add("2", "3")  # runs fine, returns "23" (no error!)

typing 모듈 (List, Dict, Tuple, Optional)

typing 모듈은 이전 Python을 위한 제네릭 별칭을 제공합니다. 3.9부터 내장 타입을 직접 사용할 수 있습니다 (List[str] 대신 list[str]). Optional[X]는 Union[X, None]의 약자입니다 — 함수가 None을 반환할 수 있음을 알려 호출자가 None 경우를 처리하도록 강제하세요.

python
from typing import List, Dict, Tuple, Set, FrozenSet

# Pre-3.9 style (still works, needed for older Python)
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 95}
point: Tuple[int, int] = (10, 20)
mixed: Tuple[str, int, float] = ("a", 1, 2.0)
variadic: Tuple[int, ...] = (1, 2, 3)  # variable-length

# Python 3.9+ built-in generics (preferred)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95}
point: tuple[int, int] = (10, 20)

# Optional means "could be None"
from typing import Optional
def find_user(uid: int) -> Optional[str]:
    if uid == 1:
        return "Alice"
    return None  # could also just 'return'

Union & Literal 타입

Union[X, Y](또는 3.10+에서 X | Y)는 값이 두 타입 중 하나일 수 있음을 의미합니다. Literal은 값을 특정 상수로 제한합니다 — enum 오버헤드 없는 문자열 enum에 훌륭하고 오버로드된 함수 디스패치에 유용합니다. mypy는 Literal을 사용하여 타입을 좁히고 검사 시 잘못된 인수를 잡습니다.

python
from typing import Union, Literal, overload

# Union: value can be one of several types
def process(data: Union[str, bytes]) -> str:
    if isinstance(data, bytes):
        return data.decode("utf-8")
    return data

# Python 3.10+ union syntax with | (preferred)
def process2(data: str | bytes) -> str:
    if isinstance(data, bytes):
        return data.decode("utf-8")
    return data

# Literal: restrict to specific constant values
def set_mode(mode: Literal["r", "w", "a"]) -> None:
    print(f"Mode set to {mode}")

set_mode("r")   # OK
# set_mode("x")  # mypy error: not a valid literal

# Literal for boolean-like flags
Direction = Literal["up", "down", "left", "right"]

TypeVar & 제네릭

TypeVar는 제네릭 타입 변수를 만들어 함수와 클래스가 타입 관계를 보존할 수 있게 합니다 (예: '입력과 같은 타입 반환'). bound=로 하위 타입으로 제한하거나 TypeVar('T', int, float)와 같은 제약 조건을 지정하세요. 제네릭 클래스는 Generic[T]를 기반으로 매개변수화된 컨테이너가 됩니다.

python
from typing import TypeVar, Generic, List

T = TypeVar("T")  # a generic type variable

def first(items: List[T]) -> T:
    return items[0]

# Type inference: T is bound to the argument's type
x: int = first([1, 2, 3])        # T = int
y: str = first(["a", "b", "c"])  # T = str

# Bounded TypeVar: T must be a subtype of Number
from typing import TypeVar
from numbers import Number
N = TypeVar("N", bound=Number)

def sum_all(values: list[N]) -> N:
    total = values[0]
    for v in values[1:]:
        total = total + v
    return total

# Generic class
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

s: Stack[int] = Stack()
s.push(1)
# s.push("x")  # mypy error

Callable, 타입 별칭 & 프로토콜

Callable[[int, str], bool]은 int와 str을 받아 bool을 반환하는 함수를 설명합니다. 타입 별칭은 복잡한 타입에 설명적인 이름을 부여합니다. Protocol은 구조적(오리) 타이핑을 가능하게 합니다 — 올바른 메서드가 있는 모든 객체가 상속 없이 프로토콜을 만족합니다. 이것이 Python의 인터페이스에 대한 답입니다.

python
from typing import Callable, Protocol, TypeAlias

# Callable signature: Callable[[ArgTypes], ReturnType]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

apply(lambda x, y: x + y, 3, 4)  # 7

# Type aliases (3.12+ uses 'type' statement; older uses assignment)
type Vector = list[float]          # 3.12+
Vector2: TypeAlias = list[float]   # 3.10+

def magnitude(v: Vector) -> float:
    return sum(x ** 2 for x in v) ** 0.5

# Protocol: structural typing (duck typing with static checks)
class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()  # any object with a draw() method works

class Circle:
    def draw(self) -> None:
        print("drawing circle")

render(Circle())  # OK — Circle has draw()

mypy로 타입 검사

mypy는 Python의 가장 인기 있는 정적 타입 검사기입니다 — 코드를 실행하지 않고 타입 힌트를 분석합니다. None 처리 버그, 잘못된 인수 타입, 누락된 반환을 잡습니다. 점진적 타이핑으로 시작하세요: 새 코드에 힌트를 추가하고 CI에서 mypy를 실행하세요. 새 프로젝트에는 포괄적인 어노테이션을 강제하려면 --strict를 사용하세요.

python
# Save as example.py, then run:  mypy example.py
from typing import Optional

def divide(a: float, b: float) -> Optional[float]:
    if b == 0:
        return None
    return a / b

result = divide(10, 0)
# Without checking, this would crash at runtime:
# print(result + 1)  # TypeError: NoneType + int

# mypy catches it:
# error: Unsupported operand types for + ("None" and "int")
# fix: check for None first
if result is not None:
    print(result + 1)

# Run strict mode for maximum safety:
#   mypy --strict example.py
# Common strict flags: --disallow-untyped-defs, --no-implicit-optional
19

데이터 클래스

기본 @dataclass

@dataclass는 어노테이션된 필드를 기반으로 __init__, __repr__, __eq__를 자동 생성합니다 — 데이터 보유 클래스의 보일러플레이트를 제거합니다. 값 객체, 구성, DTO, 레코드에 이상적입니다. Python 3.7부터 사용 가능합니다. 필드에는 타입 어노테이션이 있어야 합니다; 어노테이션이 필드를 정의합니다.

python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)

print(p1)         # Point(x=1.0, y=2.0)  — auto __repr__
print(p1 == p2)   # True  — auto __eq__ (compares fields)
print(p1.x)       # 1.0

# Without @dataclass you'd write all this boilerplate:
# class Point:
#     def __init__(self, x, y): self.x = x; self.y = y
#     def __repr__(self): ...
#     def __eq__(self, other): ...

기본값 & default_factory

가변 기본값(리스트, 사전, 집합)은 field(default_factory=list)를 사용해야 합니다 — []를 직접 사용하면 모든 인스턴스에서 하나의 리스트를 공유하게 됩니다, 전형적인 버그입니다. default_factory는 새 객체를 생성하기 위해 인스턴스당 한 번 호출됩니다. 간단한 불변 기본값(int, str, bool, None)은 직접 할당할 수 있습니다.

python
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    grade: str = "A"          # simple default
    tags: list[str] = field(default_factory=list)  # mutable default!
    scores: dict[str, int] = field(default_factory=dict)

s = Student("Alice")
print(s)  # Student(name='Alice', grade='A', tags=[], scores={})

s.tags.append("honors")
s2 = Student("Bob")
print(s2.tags)  # [] — each instance gets its own list

# NEVER use [] or {} as a direct default — all instances would share
# the same mutable object (classic Python pitfall).

frozen, eq & order

frozen=True는 dataclass를 불변으로 만듭니다 — 필드를 재할당할 수 없고 인스턴스가 해시 가능해집니다 (dict 키나 집합 멤버로 사용 가능). order=True는 정렬을 위한 비교 메서드를 추가합니다. 좌표, 색상, 버전과 같은 불변이고 정렬 가능한 값 타입에는 frozen=True와 order=True를 결합하세요.

python
from dataclasses import dataclass

# frozen=True makes instances immutable (hashable, usable as dict keys)
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

c = Color(255, 0, 0)
# c.r = 128  # FrozenInstanceError!
print(hash(c))  # works — frozen dataclasses are hashable

# order=True generates __lt__, __le__, __gt__, __ge__ for sorting
@dataclass(order=True)
class Priority:
    level: int

tasks = [Priority(3), Priority(1), Priority(2)]
tasks.sort()
print(tasks)  # [Priority(level=1), Priority(level=2), Priority(level=3)]

# Common combo: frozen + order for immutable comparable values
@dataclass(frozen=True, order=True)
class Version:
    major: int
    minor: int

__post_init__ & field 사용자 정의

__post_init__는 생성된 __init__ 후에 자동으로 실행됩니다 — 파생 필드 계산, 값 검증 또는 설정에 사용하세요. field(init=False)는 생성자에 없는 필드를 만듭니다 (계산/캐시된 값에 적합). field(repr=False, compare=False)는 repr과 동등성 검사에서 필드를 숨깁니다.

python
from dataclasses import dataclass, field

@dataclass
class User:
    email: str
    _email_normalized: str = field(init=False, repr=False)
    id: int = field(default=0)

    def __post_init__(self):
        # runs after __init__; compute derived fields here
        self._email_normalized = self.email.strip().lower()

u = User("  [email protected] ")
print(u.email)             # '  [email protected] '
print(u._email_normalized) # '[email protected]'

# field(init=False) excludes a field from __init__
# field(repr=False) hides it from the repr
# field(compare=False) excludes from __eq__/__hash__
# field(metadata={...}) attaches custom metadata

상속 & slots

Dataclass는 상속을 지원합니다 — 자식 필드는 부모 필드 뒤에 추가되고 부모 기본값을 재정의할 수 있습니다. 참고: 부모에 기본값이 있는 필드 뒤에 자식에 기본값이 없는 필드가 올 수 없습니다. slots=True(3.10+)는 임의 속성 추가를 방지하고 인스턴스당 메모리를 크게 줄입니다 — 수백만 개의 작은 객체에 이상적입니다.

python
from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    sound: str = "..."

@dataclass
class Dog(Animal):
    breed: str = "unknown"
    sound: str = "Woof"  # override parent default

d = Dog("Rex", breed="Labrador")
print(d)  # Dog(name='Rex', sound='Woof', breed='Labrador')

# Python 3.10+: slots=True saves memory (no __dict__)
@dataclass(slots=True)
class Pixel:
    r: int
    g: int
    b: int

p = Pixel(0, 128, 255)
# p.new_field = 1  # AttributeError — slots prevent arbitrary attrs
# Saves ~40-50% memory vs regular dataclass for many instances
20

Collections & Itertools

namedtuple

namedtuple은 명명된 필드가 있는 튜플 하위 클래스를 만듭니다 — 튜플만큼 메모리 효율적이지만 훨씬 더 읽기 쉽습니다. 불변이므로 수정된 복사본을 만들려면 _replace()를 사용하세요. 타입 어노테이션과 기본값을 지원하므로 새 코드에는 typing.NamedTuple을 선호하세요. 함수에서 여러 값을 반환하는 데 훌륭합니다.

python
from collections import namedtuple

# Lightweight immutable class with named fields
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y)       # 3 4  — access by name
print(p[0], p[1])     # 3 4  — also by index
print(p._asdict())    # {'x': 3, 'y': 4}

# More memory-efficient than a full class
# Use _replace to create a modified copy (immutable!)
p2 = p._replace(x=10)
print(p2)  # Point(x=10, y=4)

# Python 3.6+ typing.NamedTuple for type hints:
from typing import NamedTuple
class Point3D(NamedTuple):
    x: float
    y: float
    z: float = 0.0

Counter

Counter는 해시 가능한 객체를 계산하는 dict 하위 클래스입니다 — 빈도 분석, 히스토그램, 투표에 완벽합니다. most_common(n)은 상위 n개 항목을 반환합니다. 누락된 키는 KeyError를 발생시키지 않고 0을 반환합니다. Counter는 카운트에 대한 집합 연산을 위해 +, -, &, |를 지원합니다.

python
from collections import Counter

# Count hashable items
words = "the cat sat on the mat the cat".split()
c = Counter(words)
print(c)  # Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})

# Most common elements
print(c.most_common(2))  # [('the', 3), ('cat', 2)]

# Arithmetic on counters
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)  # Counter({'a': 4, 'b': 3})
print(c1 - c2)  # Counter({'a': 2})  (drops zero/negatives)

# Missing keys return 0 (not KeyError)
print(c["dog"])  # 0

# Update and elements
c.update(["cat", "cat"])
print(sorted(c.elements()))  # ['cat','cat','cat','cat','mat','on','sat','the','the','the']

defaultdict

defaultdict는 팩토리 함수에서 기본값으로 누락된 키를 자동 생성합니다 — 그룹화에는 list, 계산에는 int, 중복 제거에는 set. 이는 'if key not in dict' 보일러플레이트를 제거합니다. 팩토리는 모든 접근이 아닌 키가 누락된 경우에만 호출됩니다.

python
from collections import defaultdict

# Group items by key without checking if key exists
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
by_first = defaultdict(list)
for w in words:
    by_first[w[0]].append(w)
print(dict(by_first))
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}

# Counting with int (default 0)
counts = defaultdict(int)
for w in words:
    counts[w[0]] += 1
print(dict(counts))  # {'a': 2, 'b': 2, 'c': 1}

# Nested defaultdicts
tree = defaultdict(lambda: defaultdict(list))
tree["2024"]["Jan"].append("event1")

# vs regular dict: avoids the key-check boilerplate
# d = {}
# for w in words:
#     if w[0] not in d:
#         d[w[0]] = []
#     d[w[0]].append(w)

OrderedDict & deque

deque는 양 끝에서 O(1) append/pop을 제공합니다 — 리스트(list.pop(0)은 O(n)) 대신 큐, BFS, 슬라이딩 윈도우에 사용하세요. maxlen과 함께 deque는 오래된 항목을 자동으로 버려 제한된 버퍼에 완벽합니다. OrderedDict는 3.7부터 덜 필요하지만(dict가 순서 있음) move_to_end와 popitem은 여전히 LRU 캐시에 독특하게 유용합니다.

python
from collections import OrderedDict, deque

# deque: double-ended queue, O(1) append/pop at both ends
dq = deque([1, 2, 3], maxlen=5)
dq.appendleft(0)   # deque([0, 1, 2, 3])
dq.append(4)       # deque([0, 1, 2, 3, 4])
dq.append(5)       # deque([1, 2, 3, 4, 5]) — oldest dropped (maxlen!)
print(dq.popleft())  # 1
print(dq)            # deque([2, 3, 4, 5])

# deque is ideal for queues, BFS, sliding windows
from collections import deque
queue = deque(["task1", "task2"])
queue.append("task3")
next_task = queue.popleft()  # FIFO — O(1) vs list.pop(0) which is O(n)

# OrderedDict: remembers insertion order (regular dicts do too in 3.7+,
# but OrderedDict has move_to_end and equality is order-sensitive)
od = OrderedDict([("a", 1), ("b", 2)])
od.move_to_end("a")  # move to last
print(list(od))  # ['b', 'a']
od.popitem(last=False)  # pop first item (FIFO)

itertools: chain, product, combinations, permutations

itertools는 조합론을 위한 빠르고 메모리 효율적인 도구를 제공합니다. chain은 반복 가능 객체를 지연 평가로 평면화합니다. product는 데카르트 곱을 제공합니다 (중첩 for 루프 대체). combinations/permutations는 전체 리스트를 구축하지 않고 선택을 생성합니다 — 크거나 무한 입력에 필수적입니다. 모두 이터레이터를 반환하므로 보려면 list()로 감싸세요.

python
from itertools import chain, product, combinations, permutations

# chain: flatten multiple iterables
list(chain([1, 2], [3, 4], [5]))  # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]]))  # [1, 2, 3, 4]

# product: Cartesian product (nested loops)
list(product([1, 2], ["a", "b"]))
# [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product("AB", repeat=2))  # [('A','A'),('A','B'),('B','A'),('B','B')]

# combinations: unordered selections (no repeats)
list(combinations("ABC", 2))  # [('A','B'),('A','C'),('B','C')]
list(combinations("AAA", 2))  # [('A','A'),('A','A'),('A','A')]

# permutations: ordered arrangements
list(permutations("ABC", 2))  # [('A','B'),('A','C'),('B','A'),('B','C'),('C','A'),('C','B')]

# combinations_with_replacement: allow picking same element
from itertools import combinations_with_replacement
list(combinations_with_replacement("AB", 2))  # [('A','A'),('A','B'),('B','B')]

itertools: groupby, accumulate, starmap

groupby는 키를 공유하는 연속 요소를 그룹화합니다 — 키로 먼저 정렬하지 않으면 같은 키에 여러 그룹이 생깁니다. accumulate는 누적 합/곱을 생성합니다. islice, takewhile, dropwhile은 무한을 포함한 모든 이터레이터에서 작동하는 슬라이싱과 필터링의 지연 평가 대안입니다.

python
from itertools import groupby, accumulate, starmap, islice, takewhile, dropwhile

# groupby: group consecutive items by a key (sort first!)
data = [("A", 1), ("A", 2), ("B", 3), ("B", 4), ("A", 5)]
data.sort(key=lambda x: x[0])  # MUST sort by key first
for key, group in groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# A [('A',1),('A',2),('A',5)]
# B [('B',3),('B',4)]

# accumulate: running aggregate (sum by default)
list(accumulate([1, 2, 3, 4]))  # [1, 3, 6, 10]
import operator
list(accumulate([1, 2, 3, 4], operator.mul))  # [1, 2, 6, 24]

# starmap: unpack args from tuples before calling
list(starmap(pow, [(2, 3), (3, 2), (10, 3)]))  # [8, 9, 1000]

# islice: slice an iterator (doesn't support negative indices)
list(islice(range(100), 5, 10))  # [5, 6, 7, 8, 9]

# takewhile / dropwhile: filter by predicate
list(takewhile(lambda x: x < 5, [1, 4, 6, 3, 8]))  # [1, 4]
list(dropwhile(lambda x: x < 5, [1, 4, 6, 3, 8]))  # [6, 3, 8]

functools: lru_cache, partial, reduce

lru_cache는 결과를 메모이제이션합니다 — 재귀나 비싼 순수 함수에 극적인 속도 향상; cache_info()는 적중/누락 통계를 표시합니다. partial은 인수를 미리 채워 특수화된 호출 가능 객체를 만듭니다. reduce는 함수를 누적 적용합니다 (비록 sum(), any(), all()이 종종 대체합니다). cached_property는 한 번 계산 후 인스턴스에 캐시합니다.

python
from functools import lru_cache, partial, reduce
import operator

# lru_cache: memoize function results (Least Recently Used)
@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100))  # instant (without cache: impossibly slow)
print(fib.cache_info())  # CacheInfo(hits=98, misses=101, ...)

# partial: fix some arguments, create a new callable
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5))  # 25
print(cube(3))    # 27

# reduce: cumulatively apply a function, reducing to one value
product = reduce(operator.mul, [1, 2, 3, 4])  # 24
# Equivalent: ((1*2)*3)*4

# Python 3.8+: cached_property for lazy computed attributes
from functools import cached_property
class Data:
    @cached_property
    def expensive(self):
        print("computing...")
        return [i ** 2 for i in range(1000000)]
21

JSON & CSV 처리

json.dumps & json.loads

json.dumps()(문자열 덤프)는 Python 객체를 JSON 문자열로 직렬화합니다; json.loads()(문자열 로드)는 JSON을 다시 파싱합니다. 가독성을 위해 indent를, Unicode 문자를 읽기 쉽게 유지하려면 ensure_ascii=False를, 결정적 출력을 위해 sort_keys를 사용하세요. JSON 키는 문자열이어야 합니다 — int 키는 문자열이 됩니다.

python
import json

# Serialize Python object to JSON string
data = {"name": "Alice", "age": 30, "scores": [95, 88, 92]}
json_str = json.dumps(data)
print(json_str)  # {"name": "Alice", "age": 30, "scores": [95, 88, 92]}

# Pretty-print with indent
print(json.dumps(data, indent=2))
# {
#   "name": "Alice",
#   "age": 30,
#   "scores": [95, 88, 92]
# }

# Parse JSON string to Python object
parsed = json.loads(json_str)
print(parsed["name"])  # Alice
print(type(parsed["scores"]))  # <class 'list'>

# Sort keys, handle non-ASCII
print(json.dumps({"name": "Zoë"}, ensure_ascii=False, sort_keys=True))

JSON 파일 읽기 & 쓰기

json.dump()는 파일 객체에 직접 씁니다; json.load()는 읽습니다. 이식성을 위해 항상 encoding='utf-8'을 지정하세요. 타입 매핑을 기억하세요: JSON 객체는 dict가 되고, 배열은 리스트, 숫자는 int나 float가 됩니다. Datetime, 집합, 사용자 정의 객체는 기본적으로 JSON 직렬화 가능하지 않습니다.

python
import json

data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}

# Write to file
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Read from file
with open("data.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded["users"][0]["name"])  # Alice

# Type conversions to remember:
# JSON object  <-> Python dict
# JSON array   <-> Python list
# JSON string  <-> Python str
# JSON number  <-> Python int/float
# JSON true/false <-> Python True/False
# JSON null    <-> Python None

사용자 정의 JSON 인코딩 (datetime, 사용자 정의 객체)

json 모듈은 기본적으로 datetime, set, 사용자 정의 클래스를 직렬화할 수 없습니다. (직렬화 불가능 객체에 호출되는) default 함수나 JSONEncoder 하위 클래스를 제공하세요. 왕복을 위해 사용자 정의 인코더를 loads()의 object_hook와 짝지어 원래 타입을 재구성하세요. 이것이 ORM과 ORM이 모델 객체를 직렬화하는 방법입니다.

python
import json
from datetime import datetime

# Default behavior: TypeError on non-serializable types
# json.dumps({"now": datetime.now()})  # TypeError!

# Solution 1: default function for unknown types
def default_encoder(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Cannot serialize {type(obj)}")

data = {"now": datetime.now(), "tags": {"a", "b"}}
print(json.dumps(data, default=default_encoder))

# Solution 2: custom JSONEncoder subclass
class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return {"__datetime__": obj.isoformat()}
        return super().default(obj)

print(json.dumps(data, cls=MyEncoder))

# Decoding with object_hook
def decoder(dct):
    if "__datetime__" in dct:
        return datetime.fromisoformat(dct["__datetime__"])
    return dct
json.loads(json.dumps(data, cls=MyEncoder), object_hook=decoder)

CSV 파일 읽기

항상 newline=''로 CSV 파일을 열어 Windows에서 빈 줄 문제를 피하세요. csv.reader는 리스트를 반환하고; csv.DictReader는 헤더 행으로 키가 지정된 dict를 반환합니다. csv 모듈은 인용부호, 포함된 쉼표, 줄바꿈을 올바르게 처리합니다 — line.split(',')로 CSV 줄을 수동으로 분할하지 마세요. Sniffer를 사용하여 구분자를 자동 감지하세요.

python
import csv

# Basic reader: each row is a list of strings
with open("data.csv", newline="", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)  # ['name', 'age', 'city']

# DictReader: each row is a dict keyed by header
with open("data.csv", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["age"])  # access by column name

# Handle different delimiters and quoting
with open("data.tsv", newline="") as f:
    reader = csv.reader(f, delimiter="\t", quotechar='"')
    for row in reader:
        print(row)

# Sniffer to auto-detect format
with open("unknown.csv", newline="") as f:
    sample = f.read(1024)
    dialect = csv.Sniffer().sniff(sample)
    f.seek(0)
    reader = csv.reader(f, dialect)

CSV 파일 쓰기

csv.writer는 리스트를 쓰고; csv.DictWriter는 고정된 필드 이름 집합으로 dict를 씁니다. 파일을 열 때 항상 newline=''을 사용하세요. quoting 매개변수는 필드가 인용부호로 묶이는 시기를 제어합니다 — QUOTE_MINIMAL(기본값)은 필요할 때만 인용하고, QUOTE_ALL은 모든 것을 인용하며 엄격한 파서에 유용합니다.

python
import csv

rows = [
    ["name", "age", "city"],
    ["Alice", 30, "NYC"],
    ["Bob", 25, "LA"],
]

# Basic writer
with open("out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(rows)  # write multiple rows

# DictWriter: write from dicts
with open("out.csv", "w", newline="") as f:
    fieldnames = ["name", "age", "city"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30, "city": "NYC"})
    writer.writerow({"name": "Bob", "age": 25, "city": "LA"})

# Control quoting: QUOTE_MINIMAL (default), QUOTE_ALL, QUOTE_NONNUMERIC
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
# QUOTE_ALL wraps every field in quotes: "Alice","30","NYC"

JSON Lines (NDJSON) & 스트리밍

JSON Lines(NDJSON)은 줄당 하나의 JSON 객체를 둡니다 — 각 줄을 독립적으로 처리할 수 있어 로그, 이벤트 스트림, 추가 전용 파일에 이상적입니다. 거대한 단일 JSON 문서의 경우 전체 파일을 메모리에 로드하지 않고 스트림 파싱하기 위해 ijson 라이브러리를 사용하세요. NDJSON은 많은 데이터 파이프라인의 표준입니다.

python
import json

# JSON Lines: one JSON object per line (great for logs, big data)
records = [{"id": 1, "msg": "first"}, {"id": 2, "msg": "second"}]

# Write NDJSON
with open("logs.jsonl", "w") as f:
    for rec in records:
        f.write(json.dumps(rec) + "\n")

# Read NDJSON line by line (memory-efficient for huge files)
with open("logs.jsonl", "r") as f:
    for line in f:
        rec = json.loads(line)
        print(rec["id"], rec["msg"])

# Stream large JSON arrays without loading everything into memory
# Use ijson library for streaming parsing of huge JSON files:
# import ijson
# with open("huge.json", "rb") as f:
#     for item in ijson.items(f, "items.item"):
#         process(item)  # one item at a time
22

로깅 & 테스트

logging 기본

logging 모듈은 진단 출력을 내보내는 표준 방법입니다 — 레벨, 형식, 대상을 제어할 수 있어 print()보다 훨씬 낫습니다. 모듈별로 조정할 수 있도록 모듈당 logging.getLogger(__name__)을 사용하세요. logging.exception()은 자동으로 traceback을 포함합니다. 시작 시 basicConfig를 한 번 구성하세요.

python
import logging

# Basic configuration (call once at program start)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Log levels (severity ascending)
logging.debug("Detailed debug info")     # DEBUG (10)
logging.info("General information")       # INFO (20)
logging.warning("Something unexpected")   # WARNING (30)
logging.error("A real error occurred")    # ERROR (40)
logging.critical("System is down")        # CRITICAL (50)

# Logging exceptions with traceback
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Division failed")  # includes full traceback

# Get a named logger (best practice per module)
logger = logging.getLogger(__name__)
logger.info("Module-specific log")

파일 로깅 & 다중 핸들러

핸들러는 로그 레코드를 대상(콘솔, 파일, 네트워크, 이메일)으로 라우팅합니다. RotatingFileHandler는 파일 크기를 제한하고 백업을 유지하여 무제한 로그 증가를 방지합니다. 각 핸들러는 자체 레벨과 형식을 가질 수 있습니다 (예: 파일에는 상세 로그, 콘솔에는 간결한 로그). TimedRotatingFileHandler는 크기 대신 시간별로 회전합니다.

python
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# Console handler (INFO and above)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))

# Rotating file handler (DEBUG and above, max 5MB x 3 backups)
file_handler = RotatingFileHandler(
    "app.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(
    logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
)

logger.addHandler(console)
logger.addHandler(file_handler)

logger.debug("debug to file only")
logger.info("info to both console and file")
logger.error("error everywhere")

unittest 기본

unittest는 Python의 내장 테스트 프레임워크입니다 (xUnit 스타일). 테스트는 TestCase를 상속하는 클래스에 있습니다. setUp/tearDown은 격리를 위해 각 테스트 전/후에 실행됩니다. 일반적인 어서션: assertEqual, assertTrue, assertRaises, assertIn. test_*.py 파일의 자동 발견을 위해 python -m unittest로 실행하세요.

python
import unittest

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

class TestMath(unittest.TestCase):
    def setUp(self):
        # runs before each test method
        self.data = [1, 2, 3]

    def tearDown(self):
        # runs after each test method
        pass

    def test_add(self):
        self.assertEqual(add(1, 2), 3)
        self.assertEqual(add(-1, 1), 0)

    def test_add_types(self):
        self.assertEqual(add("a", "b"), "ab")

    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(1, 0)

    def test_membership(self):
        self.assertIn(2, self.data)
        self.assertTrue(3 in self.data)

if __name__ == "__main__":
    unittest.main()
# Run: python -m unittest test_file.py -v

pytest 기본

pytest는 가장 인기 있는 Python 테스트 도구입니다 — 평범한 assert 문이 풍부한 실패 보고서를 제공하여 보일러플레이트 클래스가 필요 없습니다. pytest.raises는 선택적 정규식 매칭으로 예외를 검사합니다. pytest.approx는 float 비교 부정확성을 처리합니다. pip install pytest로 설치하고 pytest -v로 상세 출력과 함께 실행하세요.

python
# test_math.py — pytest is simpler and more powerful than unittest
# Install: pip install pytest
# Run: pytest -v

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# Plain functions, no classes required
def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

def test_add_strings():
    assert add("hello", " world") == "hello world"

# Testing exceptions with pytest.raises
import pytest

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(1, 0)

# Approximate float comparison
def test_float():
    assert 0.1 + 0.2 == pytest.approx(0.3)

pytest Fixtures

Fixtures는 pytest의 의존성 주입입니다 — 매개변수 이름을 통해 테스트에 설정 데이터, 모의 객체 또는 리소스를 제공합니다. yield 기반 fixture는 설정(yield 전)과 해체(yield 후)를 모두 처리합니다. 스코프가 재사용을 제어합니다: 'session'은 전체 실행에 대해 한 번, 'module'은 파일당 한 번, 'function'(기본값)은 테스트당 한 번 fixture를 생성합니다.

python
import pytest

# A fixture provides setup data/resources to tests
@pytest.fixture
def sample_list():
    return [1, 2, 3, 4, 5]

# Use fixtures by passing their name as a parameter
def test_length(sample_list):
    assert len(sample_list) == 5

def test_sum(sample_list):
    assert sum(sample_list) == 15

# Fixture with setup AND teardown (yield)
@pytest.fixture
def db_connection():
    print("\n[setup] connecting to DB")
    conn = {"connected": True}
    yield conn  # test runs here; value passed to test
    print("\n[teardown] closing DB")
    conn["connected"] = False

def test_db(db_connection):
    assert db_connection["connected"] is True

# Fixture scopes: function (default), class, module, session
@pytest.fixture(scope="session")
def expensive_resource():
    return load_large_dataset()  # created once per test session

pytest parametrize & 모킹

parametrize는 단일 테스트 함수를 여러 입력 세트에 대해 실행합니다 — 복사-붙여넣기 테스트 코드를 제거하고 명확한 케이스별 출력을 제공합니다. unittest.mock.patch는 격리된 테스트를 위해 함수/객체를 모의 객체로 교체합니다. assert_called_once_with는 모의 객체가 올바르게 사용되었는지 검증합니다. @pytest.mark.skip과 xfail은 미완성 테스트를 우아하게 처리합니다.

python
import pytest
from unittest.mock import patch, MagicMock

# parametrize: run one test with multiple inputs
@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (-1, 1, 0),
    (0, 0, 0),
    (100, 200, 300),
])
def test_add_many(a, b, expected):
    assert add(a, b) == expected

# parametrize with IDs for readable output
@pytest.mark.parametrize("x", [1, 2, 3], ids=["one", "two", "three"])
def test_ids(x):
    assert x > 0

# Mocking: replace external dependencies
def fetch_user(uid):
    # imagine this calls a real API
    return {"id": uid, "name": "real_user"}

@patch("__main__.fetch_user")
def test_with_mock(mock_fetch):
    mock_fetch.return_value = {"id": 1, "name": "mocked"}
    result = fetch_user(1)
    assert result["name"] == "mocked"
    mock_fetch.assert_called_once_with(1)

# Skip and expected failure
@pytest.mark.skip(reason="not implemented yet")
def test_future():
    pass

@pytest.mark.xfail(reason="known bug #42")
def test_known_bug():
    assert 1 == 2
23

네트워크 프로그래밍

TCP 서버

socket 모듈을 사용하여 TCP 서버를 만듭니다. bind는 소켓을 주소에 연결하고, listen은 백로그 큐를 설정하고, accept는 클라이언트가 연결될 때까지 차단합니다. 파일 디스크립터를 해제하려면 항상 연결을 닫으세요.

python
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 8080))
server.listen(5)
conn, addr = server.accept()
data = conn.recv(1024)
conn.sendall(b'Hello')
conn.close()

TCP 클라이언트

서버에 연결하는 TCP 클라이언트를 만듭니다. connect는 연결을 설정하고, sendall은 모든 바이트를 보내고, recv는 지정된 바이트까지 읽습니다. 문자열-바이트 변환에는 encode/decode를 사용하세요.

python
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 8080))
client.sendall(b'Hello Server')
response = client.recv(1024)
print(response.decode())
client.close()

UDP 소켓

UDP는 비연결입니다: 핸드셰이크 없음, 보장된 전달 없음. recvfrom은 데이터와 발신자 주소를 모두 반환합니다. UDP에는 SOCK_DGRAM을 사용하세요. DNS, 게임, 실시간 스트리밍에 이상적입니다.

python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('localhost', 9090))
data, addr = sock.recvfrom(1024)
print(f"From {addr}: {data.decode()}")
sock.sendto(b'Reply', addr)

HTTP 서버

http.server 모듈은 간단한 HTTP 서버를 제공합니다. BaseHTTPRequestHandler를 하위 클래스화하고 do_GET, do_POST를 재정의하세요. 개발용으로만 사용; 프로덕션에는 gunicorn을 사용하세요.

python
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/html')
        self.end_headers()
        self.wfile.write(b'<h1>Hello</h1>')
HTTPServer(('localhost', 8000), Handler).serve_forever()

소켓 타임아웃

settimeout은 모든 소켓 작업에 대한 타임아웃을 설정합니다. 작업이 타임아웃을 초과하면 socket.timeout 예외가 발생합니다. 정리를 보장하려면 try/finally를 사용하세요.

python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
    sock.connect(('example.com', 80))
    data = sock.recv(1024)
except socket.timeout:
    print('Connection timed out')
finally:
    sock.close()
24

데이터베이스 (SQLite)

테이블 생성

sqlite3는 Python에 내장되어 있습니다. connect는 데이터베이스 파일을 생성하거나 엽니다. CREATE TABLE IF NOT EXISTS는 테이블이 존재해도 에러를 방지합니다. 변경사항을 저장하려면 항상 commit을 호출하세요.

python
import sqlite3
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER)''')
conn.commit()

데이터 삽입

SQL 인젝션을 방지하려면 항상 매개변수화된 쿼리(? 자리표시자)를 사용하세요. lastrowid는 자동 증가된 ID를 반환합니다. SQL 값에 문자열 형식 지정을 절대 사용하지 마세요.

python
cursor.execute(
    'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
    ('Alice', '[email protected]', 30))
conn.commit()
print(f"ID: {cursor.lastrowid}")

데이터 쿼리

fetchall은 일치하는 모든 행을 튜플 리스트로 반환합니다. fetchone은 단일 행이나 None을 반환합니다. 큰 결과 집합의 경우 커서를 직접 반복하세요.

python
cursor.execute('SELECT * FROM users WHERE age > ?', (25,))
rows = cursor.fetchall()
for row in rows:
    print(row)
cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()

업데이트 & 삭제

UPDATE는 기존 행을 수정하고 DELETE는 제거합니다. rowcount는 영향을 받은 행을 나타냅니다. DELETE에는 항상 WHERE를 사용하세요. commit은 변경사항을 영구 저장합니다.

python
cursor.execute('UPDATE users SET age = ? WHERE name = ?', (31, 'Alice'))
cursor.execute('DELETE FROM users WHERE age < ?', (18,))
conn.commit()
print(f"Affected: {cursor.rowcount} rows")

Row 팩토리

conn을 컨텍스트 매니저로 사용하면 성공 시 자동 커밋되고 예외 시 롤백됩니다. row_factory = sqlite3.Row는 열을 이름으로 접근할 수 있게 합니다.

python
conn = sqlite3.connect('example.db')
conn.row_factory = sqlite3.Row
with conn:
    conn.execute('INSERT INTO users (name, email) VALUES (?, ?)',
                 ('Bob', '[email protected]'))
for row in conn.execute('SELECT * FROM users'):
    print(row['name'], row['email'])
25

웹 스크래핑

BeautifulSoup 기본

requests는 HTML 콘텐츠를 가져오고 BeautifulSoup이 파싱합니다. html.parser는 내장되어 있고; lxml이 더 빠릅니다. 파싱 전에 항상 response.status_code를 확인하세요.

python
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://example.com')
soup = BeautifulSoup(resp.text, 'html.parser')
print(soup.title.string)
print(soup.find('h1').text)

요소 찾기

find_all은 일치하는 모든 요소를 반환하고 find는 첫 번째를 반환합니다. class_(밑줄 포함)를 사용하세요. select는 복잡한 쿼리에 CSS 선택자를 사용합니다.

python
links = soup.find_all('a')
for link in links:
    print(link.get('href'), link.text)
article = soup.find('div', class_='article')
items = soup.select('ul.list > li.item')

테이블 추출

테이블은 td(데이터) 또는 th(헤더) 셀을 포함하는 tr(행)로 구조화됩니다. strip은 공백을 제거합니다. find_all은 태그 이름 목록을 받습니다.

python
table = soup.find('table')
for row in table.find_all('tr'):
    cols = row.find_all(['td', 'th'])
    data = [col.text.strip() for col in cols]
    print(data)

페이지 매김 처리

페이지 매김은 다음 페이지 링크를 따라 처리됩니다. select_one은 첫 번째 매치나 None을 반환합니다. 요청 사이에 time.sleep을 추가하세요.

python
all_items = []
url = 'https://example.com/page/1'
while url:
    resp = requests.get(url)
    soup = BeautifulSoup(resp.text, 'html.parser')
    all_items.extend([i.text for i in soup.select('.item')])
    next_link = soup.select_one('a.next')
    url = next_link.get('href') if next_link else None

CSV로 저장

csv.DictWriter는 사전을 CSV로 씁니다. newline은 Windows에서 여분의 빈 줄을 방지합니다. encoding=utf-8은 특수 문자를 처리합니다.

python
import csv
with open('data.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=['name', 'price'])
    writer.writeheader()
    for item in scraped_data:
        writer.writerow(item)
26

Async 웹 (aiohttp)

HTTP 클라이언트

aiohttp는 async HTTP를 제공합니다. ClientSession은 연결 풀링을 관리합니다. async with는 정리를 보장합니다. asyncio.run은 코루틴을 실행합니다.

python
import aiohttp, asyncio
async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()
data = asyncio.run(fetch('https://api.example.com'))

동시 요청

asyncio.gather는 코루틴을 동시에 실행하여 총 시간을 줄입니다. 모든 요청은 같은 세션을 공유합니다. 동시성을 제한하려면 세마포어를 사용하세요.

python
async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [await r.text() for r in responses]

웹 서버

aiohttp.web은 async 웹 서버를 만듭니다. 라우트는 HTTP 메서드와 경로 패턴으로 정의됩니다. match_info는 경로 매개변수를 추출합니다.

python
from aiohttp import web
async def handle(request):
    name = request.match_info.get('name', 'World')
    return web.json_response({'message': f'Hello, {name}!'})
app = web.Application()
app.add_routes([web.get('/', handle), web.get('/{name}', handle)])
web.run_app(app, port=8080)

WebSocket 서버

WebSocket은 양방향 실시간 통신을 가능하게 합니다. WebSocketResponse는 업그레이드 핸드셰이크를 처리합니다. async for는 메시지를 반복합니다.

python
async def ws_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    async for msg in ws:
        if msg.type == aiohttp.WSMsgType.TEXT:
            await ws.send_str(f'Echo: {msg.data}')
    return ws

쿠키가 있는 세션

ClientSession은 요청 간에 쿠키를 자동으로 유지합니다. 인증된 스크래핑에 필수적입니다. 모든 요청에 단일 세션을 사용하세요.

python
async def login_and_fetch():
    async with aiohttp.ClientSession() as session:
        await session.post('https://example.com/login',
                          data={'user': 'admin', 'pass': '123'})
        resp = await session.get('https://example.com/dashboard')
        return await resp.text()
27

멀티프로세싱 심층

프로세스 풀

Pool은 워커 프로세스를 관리합니다. map은 작업을 병렬로 분배합니다. apply_async는 단일 함수를 비동기적으로 실행합니다. Windows에서는 항상 if __name__ == main 가드를 사용하세요.

python
from multiprocessing import Pool
def square(x): return x * x
if __name__ == '__main__':
    with Pool(4) as pool:
        results = pool.map(square, range(10))
        result = pool.apply_async(square, (100,))
        print(result.get(timeout=5))

공유 메모리

Value와 Array는 프로세스 간에 공유 메모리를 생성합니다. get_lock을 사용하여 접근을 동기화하고 경쟁 조건을 방지하세요.

python
from multiprocessing import Value, Array
counter = Value('i', 0)
arr = Array('d', [0.0, 1.0, 2.0])
with counter.get_lock():
    counter.value += 1

큐 통신

Queue는 프로세스 간의 안전한 통신을 가능하게 합니다. put은 항목을 추가하고 get은 검색합니다. Queue는 내부적으로 잠금을 처리하여 프로세스 안전합니다.

python
from multiprocessing import Process, Queue
def worker(q):
    q.put('Data from worker')
if __name__ == '__main__':
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    print(q.get())
    p.join()

파이프

Pipe는 양방향 통신 채널을 만듭니다. send와 recv는 피클링을 통해 Python 객체를 전송합니다. Pipe는 지점 간 통신을 위해 Queue보다 빠릅니다.

python
from multiprocessing import Process, Pipe
def worker(conn):
    conn.send(['hello', 'world'])
    msg = conn.recv()
    conn.close()
if __name__ == '__main__':
    parent, child = Pipe()
    p = Process(target=worker, args=(child,))
    p.start()
    print(parent.recv())
    parent.send('acknowledged')
    p.join()

동기화

Lock은 한 번에 하나의 프로세스만 공유 리소스에 접근하도록 보장합니다. with lock은 자동으로 획득하고 해제합니다. 기타 프리미티브: RLock, Semaphore, Event.

python
from multiprocessing import Process, Lock
def safe_print(lock, msg):
    with lock:
        print(msg)
if __name__ == '__main__':
    lock = Lock()
    procs = [Process(target=safe_print, args=(lock, f'Task {i}'))
             for i in range(5)]
    for p in procs: p.start()
    for p in procs: p.join()
28

가상 환경 심층

venv 모듈

venv는 자체 패키지 디렉토리가 있는 격리된 Python 환경을 만듭니다. 활성화는 PATH를 수정합니다. 종속성을 설치하기 전에 항상 활성화하세요.

python
# Create
python -m venv myenv
# Activate (Linux/Mac)
source myenv/bin/activate
# Activate (Windows)
myenv\Scripts\activate
# Deactivate
deactivate

requirements.txt

requirements.txt는 프로젝트 종속성을 나열합니다. ==는 정확한 버전을 고정하고 >=는 범위 내에서 업그레이드를 허용합니다. 항상 버전 관리에 커밋하세요.

python
# Generate
pip freeze > requirements.txt
# Install
pip install -r requirements.txt
# Pin versions
flask==2.3.3
requests>=2.28.0,<3.0.0

Poetry

Poetry는 현대적인 종속성 관리자입니다. pyproject.toml이 requirements.txt를 대체합니다. 가상 환경은 자동으로 관리됩니다.

python
# Initialize
poetry init
# Add dependency
poetry add flask
poetry add pytest --group dev
# Install all
poetry install
# Run command
poetry run python app.py

pipenv

pipenv는 pip와 virtualenv를 결합합니다. Pipfile은 종속성을 선언하고 Pipfile.lock은 정확한 버전을 고정합니다. --dev는 개발 종속성을 분리합니다.

python
# Create environment
pipenv install
# Add package
pipenv install requests
pipenv install pytest --dev
# Activate shell
pipenv shell
# Run command
pipenv run python app.py

Conda 환경

Conda는 Python 및 비 Python 종속성을 모두 관리합니다. environment.yml은 전체 환경을 캡처합니다. 바이너리 종속성이 있는 데이터 과학에 이상적입니다.

python
# Create
conda create -n myenv python=3.11
# Activate
conda activate myenv
# Export
conda env export > environment.yml
# Recreate
conda env create -f environment.yml
29

pip 고급 사용법

Git에서 설치

Git 저장소에서 직접 패키지를 설치합니다. 미출시 버전, 포크 또는 개인 패키지에 유용합니다. @branch 또는 @commit은 특정 버전을 고정합니다.

python
# From GitHub
pip install git+https://github.com/user/repo.git
# Specific branch
pip install git+https://github.com/user/repo.git@branch-name
# Specific commit
pip install git+https://github.com/user/repo.git@abc123

편집 가능 설치

편집 가능 설치(-e)는 복사 대신 패키지를 링크합니다. 재설치 없이 변경사항이 즉시 사용 가능합니다. 패키지 개발에 필수적입니다.

python
# Install in development mode
pip install -e .
# From a specific path
pip install -e /path/to/package
# With extras
pip install -e ".[dev,test]"

제약 조건 & 해시

제약 조건은 설치할 수 있는 버전을 제한합니다. 해시 검사는 패키지 무결성을 검증하여 공급망 공격을 방지합니다.

python
# constraints.txt
flask==2.3.3
pip install -c constraints.txt flask
# Hash checking
pip install --require-hashes -r requirements.txt

캐시 관리

pip는 다운로드한 wheel을 캐시합니다. --no-cache-dir는 강제로 새 다운로드를 합니다. 캐시가 너무 커지면 Purge가 디스크 공간을 해제합니다.

python
# Show cache info
pip cache info
# List cached packages
pip cache list
# Purge entire cache
pip cache purge
# Install with no cache
pip install --no-cache-dir flask

사용자 정의 인덱스

--index-url는 사용자 정의 패키지 저장소를 지정합니다. --extra-index-url는 대체를 추가합니다. --trusted-host는 내부 레지스트리에 대해 SSL을 우회합니다.

python
# Use custom index
pip install --index-url https://pypi.custom.com/simple/ flask
# Extra index (fallback)
pip install --extra-index-url https://pypi.custom.com/simple/ flask
# Trusted host (no SSL)
pip install --trusted-host pypi.custom.com flask
30

타입 검사 (mypy)

기본 타입 힌트

타입 힌트는 함수 매개변수와 반환 타입을 어노테이션합니다. Python 3.9+는 내장 타입을 직접 허용합니다. 힌트는 mypy로 정적 분석을 가능하게 합니다.

python
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()
def process(data: list[int]) -> dict[str, int]:
    return {str(x): x for x in data}

Optional과 Union

Optional[X]는 X | None(Python 3.10+)과 동일합니다. Union 타입은 여러 가능한 타입을 허용합니다. mypy는 모든 코드 경로가 모든 타입을 처리하는지 검사합니다.

python
from typing import Optional
def find(items: list[int], target: int) -> int | None:
    for i, v in enumerate(items):
        if v == target: return i
    return None

제네릭 타입

제네릭은 재사용 가능한 타입 안전 컨테이너를 만듭니다. TypeVar는 타입 변수를 정의하고 Generic은 클래스를 제네릭으로 만듭니다. mypy는 타입 일관성을 보장합니다.

python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

Protocol

Protocol은 구조적 하위 타이핑(타입 검사가 있는 오리 타이핑)을 정의합니다. 필요한 메서드가 있는 모든 클래스가 상속 없이 프로토콜을 만족합니다.

python
from typing import Protocol
class Closeable(Protocol):
    def close(self) -> None: ...
def cleanup(resource: Closeable) -> None:
    resource.close()
class File:
    def close(self) -> None: print("Closed")
cleanup(File())  # OK - File has close()

mypy 구성

mypy.ini는 타입 검사 엄격성을 구성합니다. strict는 모든 검사를 활성화합니다. 모듈별 재정의는 테스트나 레거시 코드에 대한 규칙을 완화합니다.

python
# mypy.ini
[mypy]
python_version = 3.11
strict = True
warn_return_any = True
disallow_untyped_defs = True
[mypy-tests.*]
ignore_errors = True
31

성능 팁

리스트 vs 제너레이터

리스트는 모든 요소를 메모리에 저장하고; 제너레이터는 요구에 따라 값을 생성합니다. 한 번 반복되는 큰 시퀀스에는 제너레이터를 사용하세요.

python
# List: all in memory
squares = [x**2 for x in range(1000000)]
# Generator: lazy evaluation
squares_gen = (x**2 for x in range(1000000))
import sys
print(sys.getsizeof(squares))      # ~8MB
print(sys.getsizeof(squares_gen))  # ~200 bytes

문자열 연결

+=로 문자열 연결은 O(n^2)입니다. join은 O(n)입니다. f-strings가 가장 빠른 보간 방법입니다.

python
# Slow: creates intermediate strings
result = ""
for s in parts:
    result += s
# Fast: join in one operation
result = "".join(parts)
# Fast: f-strings
msg = f"Hello, {name}!"

지역 변수

지역 변수 조회가 전역이나 속성 조회보다 빠릅니다. 자주 사용하는 함수를 지역에 할당하면 루프가 빨라집니다.

python
import math
# Slow: global lookup
def compute_slow(values):
    return [math.sqrt(v) for v in values]
# Fast: local reference
def compute_fast(values):
    sqrt = math.sqrt
    return [sqrt(v) for v in values]

__slots__

__slots__은 __dict__ 생성을 방지하여 인스턴스당 40-50% 메모리를 절약합니다. 수백만 개의 객체를 생성할 때 중요합니다. 나열되지 않은 속성을 추가할 수 없습니다.

python
class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
p = Point(1, 2)
# p.z = 3  # AttributeError

timeit & cProfile

timeit은 작은 스니펫의 실행 시간을 측정합니다. cProfile은 시간이 소비되는 곳을 보여줍니다. 실제 병목을 찾기 위해 최적화 전에 프로파일링을 사용하세요.

python
import timeit
t = timeit.timeit('sum(range(100))', number=10000)
print(f"{t:.4f}s")
import cProfile
cProfile.run('sum(x**2 for x in range(10000))')
32

일반적인 함정

가변 기본 인수

기본 인수 값은 정의 시점에 한 번 평가됩니다. 가변 기본값은 모든 호출에서 공유됩니다. 항상 None을 기본값으로 사용하세요.

python
# BUG: default list is shared
def add_item(item, lst=[]):
    lst.append(item)
    return lst
print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]!
# FIX: use None
def add_item(item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    return lst

후기 바인딩 클로저

클로저는 참조로 변수를 캡처합니다. lambda가 호출될 때쯤이면 루프 변수가 최종 값을 가집니다. 기본 인수는 현재 값을 캡처합니다.

python
# BUG: all print 2
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2]
# FIX: default argument
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

정수 캐싱

Python은 작은 정수를 캐시합니다. is는 식별성을 검사하고 ==는 동등성을 검사합니다. 값 비교에 is를 절대 사용하지 마세요; None, True, False에만 is를 사용하세요.

python
# Small integers cached (-5 to 256)
a = 256; b = 256
print(a is b)  # True (cached)
c = 257; d = 257
print(c is d)  # False (not cached)
print(c == d)  # True

is vs ==

is는 두 참조가 같은 객체를 가리키는지 검사합니다. ==는 두 객체가 같은 값을 가지는지 검사합니다. None, True, False에만 is를 사용하세요.

python
a = [1, 2, 3]; b = [1, 2, 3]
print(a == b)  # True (same values)
print(a is b)  # False (different objects)
# Correct usage of is
if x is None: ...
if x is not None: ...

GIL

GIL은 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있게 합니다. 스레딩은 I/O 바운드 작업에 효과적입니다. CPU 바운드 병렬성에는 multiprocessing을 사용하세요.

python
# GIL prevents true parallelism for CPU-bound tasks
import threading
def cpu_work():
    total = sum(i**2 for i in range(10**7))
# Use multiprocessing for CPU work
from multiprocessing import Pool
with Pool(4) as p:
    p.map(cpu_work, range(4))

관련 Python 스니펫

Copy-paste ready code for common tasks.

값으로 딕셔너리 정렬

Python 딕셔너리를 값 기준으로 내림차순 정렬.

리스트 컴프리헨션

리스트 컴프리헨션을 사용해 리스트를 빠르게 생성.

딕셔너리 병합

딕셔너리를 병합하는 여러 방법.

파일 읽기/쓰기

파일을 읽고 쓰는 다양한 방법.

CSV 처리

csv 모듈을 사용해 CSV 파일 읽기 및 쓰기.

JSON 처리

JSON 직렬화 및 역직렬화.

정규식 매칭

re 모듈을 사용해 정규식 매칭 수행.

날짜 처리

datetime으로 날짜와 시간 처리.

데코레이터

데코레이터 정의 및 사용.

제너레이터

제너레이터를 사용해 메모리 절약.

컨텍스트 매니저

커스텀 컨텍스트 매니저.

예외 처리

완전한 예외 처리 메커니즘.

클래스 상속

클래스 상속 및 메서드 재정의.

멀티스레딩

threading 모듈을 사용해 멀티스레딩 구현.

멀티프로세싱

멀티프로세싱으로 진정한 병렬성 달성.

asyncio 비동기 프로그래밍

asyncio로 비동기 동시성 구현.

소켓 프로그래밍

TCP 소켓 서버 및 클라이언트.

HTTP 요청

requests 라이브러리를 사용해 HTTP 요청 전송.

데이터베이스 연산

sqlite3를 사용해 데이터베이스 작업.

가상 환경

Python 가상 환경 생성 및 관리.

pip 설치

일반적인 pip 패키지 관리 명령.

환경 변수

환경 변수 읽기 및 설정.

로깅

logging 모듈 구성 및 사용.

단위 테스트

unittest를 사용해 단위 테스트 작성.

타입 힌트

타입 주석으로 코드 가독성 향상.

Dataclass

dataclass로 클래스 정의 단순화.

Enum

Enum을 사용해 enum 타입 정의.

Property 데코레이터

property로 속성 접근 제어.

매직 메서드

일반적인 매직 메서드 예제.

이터레이터

커스텀 이터레이터 구현.

코루틴

코루틴의 기본 사용법.

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.