Skip to content
This page has been auto-translated and may contain errors.View in English

모듈과 표준 라이브러리

파이썬에는 바로 사용할 수 있는 방대한 도구 모음이 함께 제공됩니다. 난수, 수학 연산, 날짜, 파일 경로 등등 정말 많습니다. 이런 도구들은 모듈이라는 형태로 존재하며, import를 통해 여러분의 코드로 가져올 수 있습니다. 파일과 예외 챕터에서 이미 import json을 사용해본 적이 있습니다. 이번 챕터에서는 import를 제대로 다루고, 표준 라이브러리에서 가장 유용한 부분들을 소개합니다.

파이썬 표준 라이브러리는 흔히 마주치는 문제에 대해 검증되고 문서화된 해결책을 제공합니다. 모듈은 코드를 구성하는 단위입니다. 파일 하나가 곧 모듈이고, __init__.py가 있는 디렉터리는 패키지가 됩니다. import 시스템은 모듈을 찾아 필요하면 컴파일하고, sys.modules에 캐시해두어 한 번만 로드되도록 합니다.

import는 실제로 작업을 수행합니다. 파이썬은 모듈을 찾아 한 번 실행한 다음 그 결과를 sys.modules(이미 로드된 모든 모듈을 담은 딕셔너리)에 캐시합니다. 그래서 같은 이름을 두 번째로 import할 때는 재실행이 아니라 빠른 조회로 처리됩니다. import requests는 현재 코드 위치에서 보이는 이름들의 표인 네임스페이스requests라는 이름으로 모듈 객체를 바인딩합니다. from requests import get은 같은 로딩 과정을 거치지만 get만 바인딩합니다. 실전 프로젝트에서 진짜 도움이 되는 부분은 파이썬이 어디서 모듈을 찾는지(sys.path), 디렉터리가 어떻게 import 가능한 패키지가 되는지(__init__.py), 그리고 순환 import를 피하는 방법입니다. 아래에서 모두 다룹니다.

모듈 import하기

가장 간단한 import는 모듈 전체를 가져와서 점(dot) 표기법으로 그 안의 내용을 사용할 수 있게 해줍니다. 특정 이름만 모듈에서 가져와 접두사 없이 바로 사용할 수도 있습니다. 별칭(alias)을 쓰면 긴 이름을 줄일 수 있습니다.

import module은 현재 스코프에서 module이라는 이름에 모듈 객체를 바인딩합니다. from module import namename만 바인딩합니다. 별칭(import module as alias)은 서드파티 라이브러리에서 흔히 사용됩니다. from module import *는 피하세요. 네임스페이스를 오염시키고 이름이 어디서 왔는지 알기 어렵게 만듭니다.

from module import name이라고 해서 작업을 건너뛰는 것은 아닙니다. 파이썬은 여전히 모듈 전체를 로드하고 실행한 다음, 여러분이 요청한 그 이름 하나만 바인딩합니다. 알아둬야 할 함정은 순환 import입니다. 두 모듈이 서로를 아직 로딩하는 도중에 import하려고 하면, 한쪽은 절반만 완성된 모듈을 보게 되어 이름이 누락됩니다. 보통 해결책은 문제가 되는 import를 그것이 필요한 함수 안으로 옮기는 것(두 모듈이 로딩을 끝낸 뒤에 실행되도록)이나, 공유되는 코드를 두 모듈이 모두 import할 수 있는 세 번째 모듈로 빼내는 것입니다. 실행 시점에야 이름을 알 수 있는 모듈을 import해야 할 때는, importlib.import_module("name")이 그 이름을 문자열로 받습니다.

python
import math

math.sqrt(16)     # 4.0
math.pi           # 3.141592653589793
math.floor(3.9)   # 3
math.ceil(3.1)    # 4

모듈에서 특정 이름만 import해서 바로 사용할 수도 있습니다:

python
from math import sqrt, pi

sqrt(16)    # 4.0 ("math." 접두사 필요 없음)
pi          # 3.141592653589793

모듈이나 이름에 별칭을 붙여 줄여 쓸 수도 있습니다:

python
import math as m

m.sqrt(16)    # 4.0

from math import sqrt as square_root
square_root(25)    # 5.0

별칭은 인기 있는 서드파티 라이브러리에서 흔히 사용됩니다(import numpy as np, import pandas as pd). 표준 라이브러리 모듈은 전체 이름을 그대로 쓰는 편이 코드를 더 읽기 쉽게 만듭니다.

Juno모듈 import하기import math는 모듈 전체를 가져오고, 점을 찍어서 math.sqrt(16)처럼 안으로 들어갑니다. from math import sqrt는 이름 하나만 가져와서 접두사를 뗄 수 있게 해줍니다. as 키워드는 긴 import 이름을 바꿔주지만, 표준 라이브러리라면 전체 이름을 쓰는 편이 더 읽기 쉽습니다.
Juno모듈 import하기import module은 모듈 객체를 바인딩하고, from module import name은 이름 하나만 바인딩하며, import module as alias는 그 이름을 바꿔줍니다. from module import *는 쓰지 마세요. 모든 이름을 스코프에 쏟아부어서 각 이름이 어디서 왔는지 알 수 없게 만듭니다.
Juno모듈 import하기from module import name도 모듈 전체를 로드하는 건 마찬가지고, 이름 하나만 남겨둘 뿐입니다. 언젠가 만나게 될 함정은 순환 import입니다. 두 모듈이 로딩 도중 서로를 import하는 경우죠. 이럴 때는 그 import를 함수 안으로 옮기거나, 공유 코드를 세 번째 모듈로 분리하세요. 이름을 실행 시점에야 아는 경우에는 importlib.import_module()이 있습니다.

random

random 모듈은 난수를 생성하고 무작위 선택을 수행합니다. 게임, 시뮬레이션, 무작위 표본 추출 등 예측 불가능성이 필요한 곳이라면 어디든 사용할 수 있습니다. **시드(seed)**를 설정하면 결과를 재현할 수 있습니다. 같은 시드는 매번 같은 결과 순서를 만들어냅니다.

random은 메르센 트위스터(Mersenne Twister) 의사 난수 생성기를 사용합니다. 시드가 전체 결과 순서를 결정하며, 같은 시드는 항상 같은 출력을 만듭니다. .choice()는 하나를 선택하고, .choices()는 중복을 허용해 선택하며, .sample()은 중복 없이 선택합니다. .shuffle()은 리스트를 제자리에서 수정하고 None을 반환합니다.

random은 **의사 난수(pseudo-random)**를 만듭니다. 겉으로는 무작위처럼 보이지만 실제로는 시작값(시드)에 의해 결정되는 고정된 공식에서 나오기 때문에, 같은 시드는 어떤 컴퓨터에서든 같은 결과 순서를 만듭니다. 그래서 테스트를 재현 가능하게 만들 수 있는 것입니다. 실무에서 중요한 규칙은, 비밀번호, 세션 토큰, 비밀번호 재설정 코드처럼 사람이 악용할 수 있는 것에는 절대 random을 쓰지 말라는 것입니다. 설계상 예측 가능하기 때문에, 충분한 출력을 본 공격자는 나머지를 추측해낼 수 있습니다. 이런 경우에는 대신 운영체제의 안전한 난수 소스에서 값을 뽑는 secrets 모듈을 사용하세요. API를 바꾸고 싶지 않다면 random.SystemRandom()을 쓰면 익숙한 random 메서드 이름 뒤에서 같은 안전한 소스를 제공받을 수 있습니다.

python
import random

random.random()              # 0과 1 사이(1 제외)의 실수
random.randint(1, 10)        # 1부터 10 사이의 정수 (양쪽 다 포함)
random.uniform(1.0, 10.0)    # 1.0과 10.0 사이의 실수

colours = ["red", "green", "blue"]
random.choice(colours)       # 항목 하나를 선택
random.choices(colours, k=3) # k개 선택 (중복 허용)
random.sample(colours, k=2)  # k개 선택 (중복 없음)

numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)      # 제자리에서 섞고, None을 반환

재현 가능한 결과가 필요할 때(테스트나 데이터 과학에서 유용합니다), 생성 전에 시드를 설정하세요:

python
random.seed(42)
random.randint(1, 100)   # 시드 42라면 항상 같은 값

같은 시드는 어떤 컴퓨터에서도 매번 같은 결과 순서를 만듭니다.

Junorandomrandom.choice()는 리스트에서 항목 하나를 고르고, random.randint(1, 10)은 범위 안(끝값 포함)의 정수를 줍니다. 실행할 때마다 같은 결과를 얻고 싶다면(예: 테스트), 먼저 random.seed()를 호출하면 순서가 고정됩니다.
Junorandom.choice()는 하나를 고르고, .choices()는 중복을 허용해 고르며, .sample()은 중복 없이 고릅니다. .shuffle()은 리스트를 제자리에서 재배열하고 None을 반환하므로, nums = random.shuffle(nums)라고 쓰면 안 됩니다. 시드는 전체 결과 순서를 고정하는데, 이 방법으로 무작위 테스트를 반복 가능하게 만듭니다.
Junorandomrandom은 설계상 예측 가능한 의사 난수이므로 게임이나 테스트에는 괜찮지만, 공격자가 악용할 수 있는 곳에는 적합하지 않습니다. 토큰, 비밀번호, 재설정 코드에는 secrets를 사용하세요. random.seed()는 결과 순서를 실제로 반복하고 싶은, 재현 가능한 경우에만 쓰세요.

math

math 모듈은 숫자 챕터에서 다룬 기본 산술 연산자를 넘어서는 더 고급 수학 연산을 추가로 제공합니다. 제곱근, 거듭제곱, 로그, 삼각함수, pi나 무한대 같은 특수한 값들이 모두 여기 있습니다.

math는 표준 수학 함수를 C 수준으로 구현해 제공합니다. math.pow()는 항상 float를 반환하지만, 파이썬의 ** 연산자는 정수 밑과 지수에 대해 int를 반환한다는 점에 유의하세요. math.log(x, base)는 임의의 밑에 대한 로그를 계산하고, math.log(x)는 자연로그를 계산합니다.

실제 코드에서 발목을 잡는 세부사항은 nan("not a number"의 줄임말로, 0/0이나 파싱 실패처럼 정의되지 않은 결과에서 나오는 float 값)과 inf(무한대)입니다. nan은 자기 자신을 포함해 그 무엇과도 결코 같지 않으므로, x == float("nan")은 항상 False입니다. 즉 ==로는 검사할 수 없으니, math.isnan(x)를 사용하고 무한대에는 math.isinf(x)를 사용하세요. 또 하나 지켜둘 만한 습관은, 두 float를 비교할 때 a == b 대신 math.isclose(a, b)를 선호하는 것입니다. 부동소수점 반올림 때문에 정확한 동등 비교는 신뢰할 수 없기 때문입니다. 숫자 하나씩이 아니라 배열 전체에 대한 수학 연산이 필요하다면 numpy가 표준 도구지만, 이 가이드의 범위를 벗어납니다.

python
import math

math.sqrt(25)        # 5.0
math.pow(2, 10)      # 1024.0 (2 ** 10과 같지만 항상 float를 반환)
math.log(100, 10)    # 2.0 (밑이 10인 로그)
math.log(math.e)     # 1.0 (자연로그)

math.sin(math.pi / 2)   # 1.0
math.cos(0)             # 1.0

math.ceil(3.2)    # 4
math.floor(3.9)   # 3
math.trunc(3.9)   # 3 (양수에서는 int()와 동일)

math.inf          # 무한대
math.isnan(float("nan"))   # True
math.isinf(math.inf)       # True
Junomathmath+ - * /를 넘어서는 모든 것, 제곱근, 거듭제곱, 로그, 삼각함수, 거기에 math.pi 같은 상수까지 다룹니다. 계산이 기본 연산자만으로 부족하다 싶으면 바로 이걸 쓰세요. 그러면 공식을 직접 만들 필요가 없습니다.
Junomathmath에는 제곱근, 로그, 삼각함수가 있습니다. 주의할 점 하나: math.pow(2, 10)은 항상 float를 반환하지만, 2 ** 10은 정수 입력이면 int로 남습니다. math.log(x, base)는 임의의 밑을 받고, math.log(x)는 자연로그입니다.
Junomath 함정은 naninf입니다. nan은 자기 자신과도 같지 않으니 == 대신 math.isnan()으로 검사하세요. 그리고 float 비교는 ==보다 math.isclose()를 쓰세요. 반올림 때문에 정확한 동등 비교는 신뢰할 수 없으니까요. 배열 전체에 대한 연산이라면 그건 numpy의 영역입니다.

datetime

datetime 모듈은 날짜와 시간을 다룹니다. datetime.now()는 현재 날짜와 시간을 줍니다. strftime()은 그것을 문자열로 포맷하고, strptime()은 문자열을 datetime으로 파싱합니다. timedelta는 더하거나 뺄 수 있는 기간을 나타냅니다.

datetime, date, timedelta가 주요 클래스입니다. strftime()은 포맷 코드를 사용해 datetime을 문자열로 만들고, strptime()은 주어진 포맷 패턴에 따라 문자열을 파싱합니다. timedelta는 산술 연산을 지원합니다. 날짜에 기간을 더하거나 빼고, <, >, -로 datetime을 비교할 수 있습니다.

실무에서 문제가 되는 부분은 그냥 datetime.now()naive하다는 점입니다. 타임존 정보가 붙어있지 않아서, 실제로 세계의 어느 지역을 나타내는지 알지 못합니다. 서로 다른 컴퓨터에서 만들어진 두 naive datetime은 비교 가능해 보이지만 실제로는 그렇지 않고, 이런 모호함이 시간 몇 시간씩 어긋나는 버그를 일으킵니다. 해결책은 aware datetime(타임존을 갖고 있는 것)을 사용하는 것이고, 시스템 간에 저장하거나 전송하는 값은 UTC로 통일하는 것입니다: datetime.now(timezone.utc). 지역 시간으로 변환하는 것은 사람에게 보여주는 마지막 순간에만 하세요. "Europe/Oslo" 같은 이름이 붙은 시간대에는 zoneinfo 모듈을 사용하면 서머타임 전환까지 대신 처리해줍니다. 규칙 하나 더: datetime.now() 값의 차이로 경과 시간을 재지 마세요. 시스템 시계는 동기화나 수동 변경으로 튈 수 있기 때문입니다. 기간을 잴 때는 항상 앞으로만 흐르는 카운터인 time.perf_counter()를 사용하세요.

python
from datetime import datetime, date, timedelta

now = datetime.now()             # 현재 날짜와 시간
today = date.today()             # 현재 날짜만

print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)

# 포맷팅
print(now.strftime("%Y-%m-%d"))           # "2024-01-15"
print(now.strftime("%d %B %Y, %H:%M"))   # "15 January 2024, 09:42"

# 파싱
deadline = datetime.strptime("2024-12-31", "%Y-%m-%d")

# 산술 연산
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
diff = deadline - now
print(f"{diff.days} days until deadline")

자주 쓰는 strftime 코드:

코드의미예시
%Y4자리 연도2024
%m월 (0으로 패딩)01
%d일 (0으로 패딩)15
%H시 (24시간)09
%M42
%B전체 월 이름January
Junodatetimedatetime.now()는 현재 날짜와 시간을 줍니다. strftime()%Y-%m-%d 같은 코드로 문자열로 바꿔주고, strptime()은 그 반대로 문자열을 다시 파싱합니다. timedelta는 더하거나 뺄 수 있는 기간이라, today + timedelta(days=1)는 내일입니다.
Junodatetimedatetime, date, timedelta가 핵심입니다. strftime()은 코드로 포맷하고, strptime()은 매칭되는 패턴으로 파싱합니다. 날짜끼리는 산술 연산이 되어서, 둘을 빼면 timedelta가 나오고, timedelta를 더하면 앞이나 뒤로 이동합니다.
Junodatetime 그냥 datetime.now()는 naive해서 타임존이 없고, 두 컴퓨터가 관련되는 순간 조용히 문제가 생깁니다. 저장하고 전달할 때는 datetime.now(timezone.utc)로 UTC를 쓰고, 지역 시간 변환은 화면에 보여줄 때만 하며, 이름 붙은 시간대에는 zoneinfo를 활용하세요. 그리고 경과 시간은 now() 차이가 아니라 time.perf_counter()로 측정하세요. 시스템 시계는 튈 수 있으니까요.

os와 pathlib

pathlib은 파일 경로를 다루는 현대적인 방법입니다. Path 객체는 / 연산자를 사용해 경로를 만들고, 살펴보고, 탐색할 수 있게 해줍니다. os는 환경 변수와 더 낮은 수준의 OS 작업에 접근할 수 있게 해줍니다. 새 코드에서는 pathlib을 우선적으로 사용하세요.

pathlib.Path는 파일시스템 경로를 조회하고 탐색하는 메서드를 갖춘 객체로 표현합니다. / 연산자는 경로 구성요소를 깔끔하게 이어주고, OS별 구분자를 자동으로 처리합니다. os.environ은 환경 변수를 위한 딕셔너리 같은 객체이고, os.environ.get("KEY", "default")는 변수가 없을 때도 안전합니다.

Path는 운영체제에 맞춰 자동으로 적응하므로, 같은 코드가 어느 컴퓨터에서든 올바른 구분자로 경로를 이어붙입니다. Path("data") / "file.csv"처럼 쓰고, Windows에서 깨지는 손수 조합한 "data/" + name 같은 문자열은 절대 쓰지 마세요. 실용적으로 알아둘 점이 두 가지 있습니다. 첫째, .glob(), .rglob(), .iterdir()제너레이터를 반환합니다(항목들을 리스트로 한꺼번에 만드는 대신 하나씩 내놓습니다). 그래서 두 번 반복해야 하거나 개수를 미리 알아야 한다면 list()로 감싸세요. 거대한 디렉터리라면 지연 반복 자체가 목적입니다. 둘째, 일부 오래된 라이브러리는 여전히 Path 객체가 아니라 그냥 문자열을 기대합니다. Path를 거부하는 함수를 만나면 str(path)를 넘겨주고 넘어가세요. 환경 변수를 읽을 때는 항상 os.environ["KEY"]가 아니라 os.environ.get("KEY", "default")를 사용하세요. 인덱스 형태는 변수가 설정되지 않았을 때 KeyError를 일으키는데, 이는 새로 배포한 환경에서 흔히 벌어지는 상황이기 때문입니다.

python
from pathlib import Path

p = Path("data/reports")

p.exists()           # 경로가 존재하면 True
p.is_dir()           # 디렉터리면 True
p.is_file()          # 파일이면 True

p.mkdir(parents=True, exist_ok=True)   # 디렉터리 생성

for f in p.glob("*.csv"):              # 디렉터리 안의 모든 CSV 파일
    print(f.name)                      # 파일 이름

report = p / "report_jan.csv"          # / 연산자로 경로 연결
report.stem       # "report_jan" (확장자 없는 이름)
report.suffix     # ".csv"
report.parent     # Path("data/reports")

content = report.read_text()           # 파일 내용을 바로 읽기
report.write_text("new content\n")    # 바로 쓰기

os 모듈의 경우:

python
import os

os.getcwd()                        # 현재 작업 디렉터리
os.listdir(".")                    # 디렉터리 내용 나열
os.path.exists("data.txt")        # 경로가 존재하면 True
os.path.join("data", "file.txt")  # "data/file.txt" (크로스 플랫폼)
os.environ.get("HOME")            # 환경 변수 읽기

새 코드에서는 pathlib을 우선적으로 사용하세요. 환경 변수가 필요하거나 문자열을 기대하는 오래된 API를 다룰 때는 os를 사용하세요.

Junoos와 pathlibpathlib.Path는 파일 경로를 만지작거릴 수 있는 객체로 다룹니다: .exists(), .read_text(), .write_text(), .glob(). / 연산자는 조각들을 이어붙여서, p / "report.csv"가 실제 경로처럼 읽힙니다. 새 코드에서는 더 오래된 os.path보다 pathlib을 쓰세요.
Junoos와 pathlibPath/로 이어붙이고 OS 구분자를 알아서 처리해서, 경로가 이식성을 유지합니다. os.environ은 환경 변수를 위한 딕셔너리 같은 객체이고, os.environ.get("KEY", "default")가 안전한 읽기 방법입니다. 키가 없어도 터지지 않습니다.
Junoos와 pathlib 경로는 Path/로 만드세요. Windows에서 깨지는 문자열 이어붙이기는 절대 쓰지 말고요. .glob() 같은 것들은 제너레이터를 돌려주니, 두 번 반복해야 한다면 list()로 감싸세요. 그리고 환경 변수는 os.environ.get(key, default)로 읽으세요. 대괄호 형태는 변수가 설정되지 않은 순간 KeyError를 던지는데, 그게 바로 새로 배포한 상황에서 흔히 벌어지는 일이니까요.

timeit

timeit은 코드가 실행되는 데 걸리는 시간을 측정합니다. 두 가지 방식을 비교해서 더 빠른 쪽을 고르고 싶을 때 유용합니다. 안정적인 측정값을 얻으려면 코드를 여러 번 실행하세요.

timeit.timeit(stmt, setup, number)stmtnumber번 실행해 걸린 총 시간을 초 단위로 반환하며 시간을 측정합니다. setup 문자열은 측정 루프 전에 한 번 실행됩니다. 결과를 number로 나누면 호출당 시간을 얻습니다. 반복 횟수를 늘리면 시스템 스케줄링에 의한 노이즈가 줄어듭니다.

timeit마이크로벤치마크에 적합한 도구입니다. 작은 코드 조각 하나를 독립적으로 여러 번 실행해서, 스케줄링 노이즈가 평균으로 상쇄되도록 시간을 잽니다. 그 노이즈를 알아서 줄여주기는 하지만, 한 번의 실행은 여전히 신뢰하기 어려우니 하나의 숫자보다는 여러 번 반복한 것 중 최선의 값을 사용하세요. 피해야 할 실수는 프로그램 전체에서 느려진 부분을 찾는 데 이 도구를 쓰는 것입니다. 넘겨준 코드 조각만 재기 때문에, 실제로 어느 함수가 느린지는 알려주지 못합니다. 그럴 때는 실제 프로그램을 실행해서 시간이 어디로 흘러갔는지 보고해주는 cProfile을 사용하세요. 경험칙: 두 구현을 비교할 때는 timeit, 애초에 병목을 찾을 때는 cProfile입니다.

python
import timeit

# 문장 하나의 시간 측정
timeit.timeit("sum(range(1000))", number=10000)

# 더 복잡한 블록의 시간 측정
setup = "data = list(range(1000))"
code = "[x * 2 for x in data]"
time = timeit.timeit(code, setup=setup, number=10000)
print(f"{time:.4f} seconds for 10,000 runs")

number는 몇 번 반복할지를 나타냅니다. 반복을 늘릴수록 측정값이 더 안정적입니다.

Junotimeittimeit.timeit()은 코드를 반복해서 실행하며 시간을 재고, 그 값을 실행 횟수로 나누면 안정적인 호출당 수치가 나옵니다. "이 둘 중 어느 게 더 빠른가"를 짐작이 아니라 확인하는 방법입니다. 한 번의 실행은 너무 들쭉날쭉해서 믿을 수 없으니 반복시키세요.
Junotimeittimeit.timeit(stmt, setup, number)stmt를 그만큼 실행하고 총 걸린 초를 반환합니다. setup 문자열은 루프 전에 한 번 실행됩니다. 호출당 시간을 얻으려면 number로 나누고, 스케줄링 노이즈를 없애려면 반복 횟수를 늘리세요.
Junotimeittimeit은 마이크로벤치마크용입니다. 작은 코드 조각 두 개를 비교할 때, 노이즈 낀 단일 실행이 아니라 여러 번 중 최선의 값을 쓰세요. 넘겨준 것만 재기 때문에 실제 프로그램에서 어느 함수가 느린지는 알려주지 못합니다. 병목을 찾을 때는 cProfile을, 해결책을 비교할 때는 timeit을 쓰세요.

string

string 모듈은 문자, 숫자, 구두점을 위한 미리 만들어진 문자열 상수를 제공합니다. 문자를 검사하거나 특정 알파벳으로 무작위 문자열을 생성해야 할 때 유용합니다.

string 모듈의 상수들(ascii_letters, digits, punctuation)은 인덱싱, 반복, in 검사가 가능한 일반 문자열입니다. 이것들을 random.choices()와 결합하는 것이 무작위 토큰이나 비밀번호를 생성하는 표준적인 방법입니다.

이 상수들은 평범한 문자열이므로, char in string.ascii_letters 검사는 매번 문자열 전체를 훑습니다. 이 검사는 **O(n)**입니다. 즉 문자열 길이만큼 작업량이 늘어나므로, 한 번이라면 괜찮지만 반복문 안에서라면 낭비입니다. 소속 여부를 반복해서 검사한다면, 한 번만 set을 만들어(letters = set(string.ascii_letters)) 그것과 비교하세요. set의 조회는 항목 수가 아무리 많아도 항상 빠릅니다. 여기서 또 하나 유용한 부분은 string.Template인데, 이것은 단순한 $name 치환을 수행합니다. 사용자 입력이나 설정 파일에서 온 템플릿을 다룰 때는, f-string이나 str.format()에 신뢰할 수 없는 텍스트를 넘기는 것보다 이쪽이 더 안전합니다. 변수에 접근하거나 메서드를 호출할 수 없기 때문입니다.

python
import string

string.ascii_lowercase   # "abcdefghijklmnopqrstuvwxyz"
string.ascii_uppercase   # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
string.ascii_letters     # 둘을 합친 것
string.digits            # "0123456789"
string.punctuation       # 모든 구두점 문자

문자를 검사하거나 무작위 문자열을 생성할 때 유용합니다:

python
import string, random

chars = string.ascii_letters + string.digits
password = "".join(random.choices(chars, k=12))
Junostringstring 모듈은 미리 만들어진 문자 집합을 건네줍니다: string.ascii_letters, string.digits, string.punctuation. 이것들은 그냥 문자열이라, 반복하거나 표본을 뽑을 수 있습니다. 이미 알고 있는 알파벳을 기준으로 뭔가를 만들거나 검사해야 할 때 편리합니다.
Junostringstring.ascii_letters, string.digits 같은 것들은 인덱싱, 반복, in 검사가 가능한 평범한 문자열입니다. 이것들을 random.choices()와 짝지으면 무작위 토큰이나 비밀번호를 만드는 표준 레시피가 됩니다. 다만 진짜 비밀 정보에는 random이 안전하지 않다는 점을 기억하세요.
Junostring 이 상수들은 평범한 문자열이라, char in string.ascii_letters는 호출할 때마다 전체를 훑습니다. 반복문에서 검사한다면 set을 한 번 만들어두세요. 그리고 사용자 입력에서 온 템플릿이라면, string.Template$name이 f-string보다 안전합니다. 변수에 접근할 수 없기 때문입니다.

나만의 모듈 만들기

파이썬 파일은 그 자체로 모듈입니다. 다른 파일에서 사용하려면 확장자 .py를 뺀 파일 이름으로 import하면 됩니다. 모듈 전체를 import해서 점 표기법으로 내용을 사용할 수도 있고, 특정 이름만 직접 import할 수도 있습니다.

파이썬이 모듈을 import할 때, 파일을 위에서 아래로 한 번 실행하고 그 결과를 sys.modules에 캐시합니다. 같은 모듈을 다시 import하면 파일을 재실행하지 않고 캐시된 객체를 반환합니다. 규모가 큰 프로젝트에서는 모듈을 패키지로 구성합니다. 패키지는 __init__.py 파일이 있는 디렉터리입니다.

import utils라고 쓰면, 파이썬은 여러분이 실행한 스크립트의 폴더부터 시작해 sys.path에 있는 디렉터리들을 순서대로 검색합니다. 이것이 "내 컴퓨터에서는 되는데" 하는 흔한 import 버그의 근원입니다. 다른 작업 디렉터리에서 파일을 실행하면, 기대하는 폴더가 경로에 없을 수 있어 import가 실패합니다. 확실한 해결책은 독립된 파일을 파이썬에 직접 가리키는 대신 프로젝트를 패키지로 실행하는 것입니다(python -m mypackage.main). 패키지__init__.py 파일이 있는 모듈들의 디렉터리입니다(비어있어도 되고, 그 파일이 존재한다는 사실 자체가 파이썬에게 이 폴더를 import 가능하다고 알려줍니다). 패키지 안에서 from . import helpers처럼 쓰는 상대 import는 "이 패키지 안에서"라는 뜻이라서, 패키지 이름이 바뀌거나 위치가 옮겨져도 import가 계속 동작합니다. 한 가지 날카로운 함정: importlib.reload()는 모듈을 재실행하지만, 이미 import해둔 이름들은 여전히 예전 버전을 가리키고 있으므로, REPL에서 빠르게 실험하는 것 이상의 용도로는 reload를 신뢰할 수 없습니다.

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

PI = 3.14159
python
# main.py
import utils

utils.clamp(150, 0, 100)   # 100
utils.PI                    # 3.14159

from utils import clamp
clamp(50, 0, 100)           # 50

파이썬은 import하는 파일과 같은 디렉터리(그리고 몇 군데 다른 위치)를 살펴봐서 모듈을 찾습니다. 규모가 큰 프로젝트에서는 모듈을 패키지로 구성합니다. 패키지는 __init__.py 파일이 있는 디렉터리입니다.

Juno나만의 모듈 만들기 어떤 .py 파일이든 이미 모듈입니다. .py를 뺀 파일 이름으로 import하면, 그 안의 함수와 변수를 그대로 쓸 수 있습니다. import utils 다음 utils.clamp(...), 아니면 from utils import clamp로 이름 하나만 가져올 수도 있습니다. 이렇게 커지는 프로그램을 여러 파일로 나눕니다.
Juno나만의 모듈 만들기 모듈을 import하면 위에서 아래로 한 번 실행되고, 그 결과가 sys.modules에 캐시되어서, 이후의 import는 파일을 재실행하지 않고 같은 객체를 반환합니다. 그래서 최상위 레벨 코드는 가볍게 유지하세요. 무거운 작업은 import 시점에 실행됩니다. 관련 모듈들은 패키지로 묶으세요. __init__.py가 있는 폴더로요.
Juno나만의 모듈 만들기 import는 sys.path를 통해 해석되는데, 스크립트 자신의 폴더부터 시작합니다. 그래서 엉뚱한 디렉터리에서 독립 파일을 실행하면 import가 깨집니다. 대신 패키지로 실행하세요: python -m pkg.main. 그리고 importlib.reload()는 빠른 REPL 실험 이상으로는 믿지 마세요. 이미 import한 이름들은 여전히 예전 버전을 가리키고 있으니까요.

__name__ == "__main__"

파이썬이 파일을 직접 실행하면 __name__"__main__"으로 설정됩니다. 같은 파일이 모듈로 import되면 __name__은 모듈 이름이 됩니다. 이 패턴을 이용하면 파일을 직접 실행할 때만 실행되고, 다른 모듈이 import할 때는 건너뛰는 코드를 작성할 수 있습니다.

if __name__ == "__main__":은 실행 가능한 모듈 코드를 위한 표준적인 가드입니다. 이를 통해 모듈이 (함수를 노출하는) import 가능한 존재이면서 동시에 (테스트나 데모 코드를 포함해) 직접 실행 가능한 존재가 될 수 있습니다. 이 가드가 없다면 모듈을 import하는 순간 최상위 레벨 코드가 실행되어버리는데, 이는 거의 항상 원치 않는 일입니다.

파이썬은 실제로 실행한 파일에는 __name__"__main__"으로 설정하고, import된 것에는 각자의 모듈 이름으로 설정합니다. 이 가드는 import 시점의 부작용(모듈이 로드될 때 뭔가를 실행하는 코드, 예를 들어 인자 파싱, 연결 열기, 데모 실행 같은 것)이 다른 파일이 여러분을 import하는 순간 발동하는 것을 막기 위해 존재합니다. 견고한 패턴은 실제 작업을 main() 함수에 담고 가드 아래에서 그것을 호출하는 것입니다. 그러면 import하는 쪽은 시작 동작 없이 함수만 얻고, 파일 자체는 여전히 단독으로 깔끔하게 실행됩니다. 가드를 빠뜨리면, 테스트 스위트가 모듈을 import하는 순간 누군가의 데모 코드가 알 수 없이 실행되는 전형적인 원인이 됩니다.

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

if __name__ == "__main__":
    # 이 코드는 python utils.py로 실행할 때만 동작합니다
    # import utils로 가져올 때는 실행되지 않습니다
    print(clamp(150, 0, 100))   # 100

독립 실행 스크립트로도 쓸모 있는 모듈이라면 이 패턴이 표준입니다.

Juno__name__ == '__main__'if __name__ == "__main__":은 하나의 파일이 import 가능한 모듈이면서 동시에 실행 가능한 스크립트가 되게 해줍니다. 가드 안의 코드는 python utils.py로 실행할 때 동작하고, 다른 파일이 import utils할 때는 건너뜁니다. 모듈 맨 아래에 간단한 데모나 테스트를 붙여둘 때 편리합니다.
Juno__name__ == '__main__'__name__은 여러분이 직접 실행한 파일에서만 "__main__"입니다. 이 가드가 없으면 모듈을 import할 때 데모나 테스트 코드가 조용히 실행되어버리는데, 가드는 이를 막아줍니다. 파일 하나를 라이브러리이자 스크립트로 동작시키는 표준적인 방법입니다.
Juno__name__ == '__main__' 이 가드는 인자 파싱, 데모, 연결 열기 같은 import 시점의 부작용이 누군가 여러분을 import하는 순간 발동하는 것을 막아줍니다. 작업은 main()에 담고 가드 아래에서 호출하세요. 그러면 import하는 쪽은 함수만 얻고, 파일 자체는 여전히 단독으로 실행됩니다. 가드를 빠뜨리면 테스트 스위트가 모듈을 import하는 순간 여러분의 데모가 실행되는 걸 보게 될 겁니다.

표준 라이브러리 핵심 기능

알아두면 좋은 모듈 몇 가지가 더 있습니다. 각각은 직접 구현하려면 상당한 작업이 필요했을 흔한 문제를 해결해줍니다.

표준 라이브러리는 방대합니다. 아래 핵심 기능들은 실무 코드에서 가장 자주 마주치게 될 것들입니다. 완전한 레퍼런스가 필요하다면 docs.python.org/3/library가 공식 출처입니다.

표준 라이브러리는 잘 검증되고 문서화된 모듈들을 엄선한 모음입니다. 서드파티 패키지를 찾기 전에 표준 라이브러리에 이미 해결책이 있는지 확인하세요. functools, itertools, contextlib, dataclasses, typing, abc는 각각 서드파티 패키지들이 종종 다시 만들곤 하는 도구를 제공합니다.

collections: 특수한 컨테이너 타입들:

python
from collections import Counter, defaultdict, deque

Counter(["a", "b", "a", "c", "a"])   # Counter({'a': 3, 'b': 1, 'c': 1})
defaultdict(list)                      # 없는 키를 자동으로 만들어주는 dict
deque([1, 2, 3], maxlen=5)            # 양쪽 끝에서 빠르게 추가/제거

itertools: 이터러블을 다루는 도구들:

python
import itertools

list(itertools.chain([1, 2], [3, 4]))          # [1, 2, 3, 4]
list(itertools.islice(range(100), 5))          # [0, 1, 2, 3, 4]
list(itertools.combinations([1, 2, 3], 2))     # [(1, 2), (1, 3), (2, 3)]
list(itertools.product([0, 1], repeat=2))      # [(0,0), (0,1), (1,0), (1,1)]

sys: 파이썬 인터프리터에 접근하기:

python
import sys

sys.argv        # 명령줄 인자 리스트
sys.exit(1)     # 상태 코드와 함께 종료
sys.version     # 파이썬 버전 문자열

서드파티 패키지: 표준 라이브러리를 넘어서, pip는 커뮤니티 패키지를 설치합니다:

bash
pip install requests    # HTTP 라이브러리
pip install pandas      # 데이터 처리
pip install numpy       # 수치 계산

서드파티 패키지는 이 가이드의 범위 밖이지만, 패턴은 항상 동일합니다: pip install한 다음 import하세요.

실전 활용

random, string, datetime을 결합해서 타임스탬프가 붙은 고유한 게임 ID를 생성합니다:

python
import random
import string
from datetime import datetime

def generate_game_id(length: int = 8) -> str:
    chars = string.ascii_uppercase + string.digits
    return "".join(random.choices(chars, k=length))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

game_id = generate_game_id()
print(f"[{timestamp()}] Starting game {game_id}")

scores = [random.randint(50, 100) for _ in range(5)]
print(f"Round scores: {scores}")
print(f"Best: {max(scores)}")

pathlibdatetime을 이용해 디렉터리에서 파일을 찾고 그 크기를 보고합니다:

python
from pathlib import Path
from datetime import datetime

def find_files(directory: str, pattern: str = "*.csv") -> list[Path]:
    return sorted(Path(directory).glob(pattern))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

files = find_files(".", "*.md")[:3]
print(f"[{timestamp()}] Found {len(files)} file(s)")
for f in files:
    size = f.stat().st_size if f.exists() else 0
    print(f"  {f.name} ({size} bytes)")

타입이 지정된 기본값과 함께 환경 변수에서 앱 설정을 읽어오고, 구조화된 접속 로그 항목을 줄바꿈으로 구분된 JSON으로 기록합니다:

python
import os
import json
from datetime import datetime
from pathlib import Path

def load_env_config() -> dict:
    return {
        "debug": os.environ.get("DEBUG", "false").lower() == "true",
        "port": int(os.environ.get("PORT", "8080")),
        "log_level": os.environ.get("LOG_LEVEL", "INFO"),
    }

def write_access_log(method: str, path: str, status: int) -> None:
    log_dir = Path("logs")
    log_dir.mkdir(exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "method": method,
        "path": path,
        "status": status,
    }
    with open(log_dir / "access.jsonl", "a") as f:
        f.write(json.dumps(entry) + "\n")

config = load_env_config()
print(f"Starting on port {config['port']}, debug={config['debug']}")
write_access_log("GET", "/users", 200)

줄바꿈으로 구분된 JSON(.jsonl)은 흔한 로그 형식입니다. 각 줄이 유효한 JSON 객체라서, 파일 전체를 로드하지 않고도 한 줄씩 스트리밍하고, 덧붙이고, 파싱하기가 간단합니다.