참고: Google Python Style Guide - Comments and Docstrings, numpydoc Style guide, Sphinx docstring tutorial
주석이란?
주석은 code를 실행하지 않고 설명을 남기기 위한 문장이다.
Python에서는 # 뒤에 작성한 내용이 주석으로 처리된다.
# 사용자 이름을 저장한다.
name = "Alice"
주석은 interpreter가 실행하지 않는다. 사람이 code를 읽을 때 맥락을 이해하도록 돕는 용도이다.
하지만 주석을 많이 쓴다고 좋은 code가 되는 것은 아니다. 좋은 주석은 code가 말하지 못하는 이유와 맥락을 설명한다.
한 줄 주석
가장 기본적인 주석은 한 줄 주석이다.
# 서버 port
port = 8000
한 줄 전체를 주석으로 사용할 수도 있고, code 뒤에 붙일 수도 있다.
timeout = 30 # seconds
다만 inline comment는 너무 길어지면 가독성이 떨어진다. 간단한 단위나 의도를 보충할 때만 사용하는 것이 좋다.
여러 줄 주석
Python에는 C/C++의 /* ... */ 같은 block comment 문법이 없다.
여러 줄 주석은 보통 각 줄마다 #를 붙인다.
# 이 함수는 외부 API를 호출한다.
# API server가 느릴 수 있으므로 timeout을 짧게 둔다.
# 실패하면 caller에서 retry 여부를 결정한다.
def fetch_user(user_id: int) -> dict:
...
editor에서 여러 줄을 선택한 뒤 comment toggle shortcut을 사용하면 편하다. VS Code 기준으로는 보통 다음 단축키를 사용한다.
macOS: Cmd + /
Windows/Linux: Ctrl + /
문자열을 주석처럼 쓰지 않기
Python에서 triple quote 문자열을 여러 줄 주석처럼 쓰는 경우가 있다.
"""
이 부분은 주석처럼 보인다.
하지만 실제로는 문자열 literal이다.
"""
이 코드는 특정 위치에서는 docstring이 될 수 있지만, 일반적인 주석 문법은 아니다. 실행 중 문자열 object가 만들어질 수 있고, 의도도 모호하다.
여러 줄 설명을 남기고 싶다면 #를 사용하는 것이 명확하다.
# 이 부분은 실제 주석이다.
# Python interpreter가 실행하지 않는다.
triple quote는 docstring을 작성할 때 사용한다고 생각하는 것이 좋다.
Docstring
docstring은 module, class, function, method의 설명을 남기는 문자열이다.
보통 triple double quotes를 사용한다.
def add(a: int, b: int) -> int:
"""Return the sum of two integers."""
return a + b
docstring은 단순 주석과 다르다.
Python object의 __doc__ attribute로 접근할 수 있다.
print(add.__doc__)
출력:
Return the sum of two integers.
즉 docstring은 실행 중에도 남아 있는 문서이다. 도구가 문서를 생성하거나 help message를 보여줄 때도 사용된다.
Module docstring
파일 맨 위에 쓰는 docstring은 module docstring이다.
"""User service module.
This module provides functions for creating and validating users.
"""
from dataclasses import dataclass
module docstring은 해당 파일이 어떤 책임을 가지는지 설명할 때 사용한다.
좋은 module docstring은 다음 질문에 답한다.
이 module은 무엇을 담당하는가?
외부에서 어떤 목적으로 사용하는가?
주의해야 할 제약이 있는가?
Function docstring
function docstring은 함수의 목적, 인자, 반환값, 예외를 설명한다.
간단한 함수는 한 줄이면 충분하다.
def normalize_name(name: str) -> str:
"""Normalize a user name for comparison."""
return name.strip().lower()
조금 복잡한 함수는 여러 줄 docstring을 사용할 수 있다.
def divide(a: int, b: int) -> float:
"""Divide a by b.
Raises:
ZeroDivisionError: If b is zero.
"""
return a / b
단순히 함수 이름을 반복하는 docstring은 큰 도움이 되지 않는다.
def get_user(user_id: int) -> User:
"""Get user."""
...
이런 경우에는 어떤 기준으로 user를 찾는지, 없을 때 어떻게 되는지 설명하는 편이 낫다.
Class docstring
class docstring은 class가 표현하는 개념과 주요 책임을 설명한다.
class UserRepository:
"""Store and retrieve users from persistent storage."""
def find_by_id(self, user_id: int) -> User | None:
...
data class에도 docstring을 사용할 수 있다.
from dataclasses import dataclass
@dataclass
class User:
"""Application user account."""
id: int
name: str
email: str
class가 단순한 data container라면 field 이름과 type hint만으로 충분할 수 있다. 하지만 domain 의미가 있거나 외부 API로 노출된다면 docstring을 두는 것이 좋다.
Docstring 스타일 종류
Python docstring은 문법 자체보다 스타일이 중요하다. 대표적으로 다음 스타일을 많이 사용한다.
| 스타일 | 특징 | 자주 쓰는 곳 |
|---|---|---|
| Google style | Args:, Returns:, Raises: section 사용 | 일반 Python project |
| NumPy style | Parameters, Returns를 underline section으로 구분 | 과학 계산, NumPy/SciPy 계열 |
| reStructuredText style | :param name:, :returns: field 사용 | Sphinx 문서화 |
| Simple style | 한 줄 또는 짧은 문단 | 작은 함수, 내부 함수 |
어떤 스타일이 절대적으로 더 좋은 것은 아니다. 중요한 것은 한 project 안에서 하나의 스타일을 일관되게 쓰는 것이다.
Google style docstring
Google style은 Python project에서 많이 쓰는 docstring 형식이다.
인자, 반환값, 예외를 Args, Returns, Raises section으로 나눈다.
def divide(a: int, b: int) -> float:
"""Divide a by b.
Args:
a: Dividend.
b: Divisor. Must not be zero.
Returns:
The result of a divided by b.
Raises:
ZeroDivisionError: If b is zero.
"""
return a / b
구조는 다음과 같다.
요약 한 줄
추가 설명
Args:
arg_name: 설명
Returns:
반환값 설명
Raises:
예외: 발생 조건
type hint를 이미 사용하고 있다면 docstring에서 type을 반복하지 않아도 된다.
def create_user(name: str, age: int) -> User:
"""Create a user.
Args:
name: User display name.
age: User age in years.
Returns:
Created user object.
"""
...
다음처럼 type hint와 docstring type을 중복으로 길게 쓰는 것은 보통 불필요하다.
def create_user(name: str, age: int) -> User:
"""Create a user.
Args:
name (str): User display name.
age (int): User age in years.
Returns:
User: Created user object.
"""
...
팀에서 type을 docstring에도 쓰기로 정했다면 맞춰도 되지만, type hint를 쓰는 codebase에서는 의미 설명에 집중하는 편이 낫다.
Google style class docstring
class docstring에서도 Google style을 사용할 수 있다.
class UserRepository:
"""Repository for loading and saving users.
This class hides database access details from service code.
Attributes:
table_name: Database table name for users.
"""
table_name = "users"
data class에서는 attribute 설명이 필요한 경우 Attributes section을 쓸 수 있다.
from dataclasses import dataclass
@dataclass
class User:
"""Application user.
Attributes:
id: Unique user identifier.
name: Display name.
email: Contact email address.
"""
id: int
name: str
email: str
하지만 field 이름과 type만으로 충분히 명확하다면 class docstring을 짧게 유지해도 된다.
NumPy style docstring
NumPy style은 과학 계산 library에서 많이 쓰는 형식이다. section 제목과 underline을 사용한다.
def divide(a: int, b: int) -> float:
"""Divide a by b.
Parameters
----------
a : int
Dividend.
b : int
Divisor. Must not be zero.
Returns
-------
float
The result of a divided by b.
Raises
------
ZeroDivisionError
If b is zero.
"""
return a / b
NumPy style은 문서로 변환했을 때 읽기 좋다. 특히 parameter가 많거나 수학적 설명이 많은 함수에 잘 맞는다.
예를 들어 numerical function은 다음처럼 쓸 수 있다.
def moving_average(values: list[float], window_size: int) -> list[float]:
"""Compute a simple moving average.
Parameters
----------
values : list of float
Input sequence.
window_size : int
Number of values in each window.
Returns
-------
list of float
Moving average values.
"""
...
NumPy style은 길이가 길어지는 편이라, 작은 application code의 모든 함수에 쓰면 부담스러울 수 있다.
reStructuredText / Sphinx style
Sphinx 문서화에서는 reStructuredText field 형식을 많이 사용한다.
def divide(a: int, b: int) -> float:
"""Divide a by b.
:param a: Dividend.
:param b: Divisor. Must not be zero.
:returns: The result of a divided by b.
:raises ZeroDivisionError: If b is zero.
"""
return a / b
type을 같이 적는 경우도 있다.
def create_user(name: str, age: int) -> User:
"""Create a user.
:param str name: User display name.
:param int age: User age in years.
:returns: Created user object.
:rtype: User
"""
...
요즘은 type hint를 사용하므로 :type:이나 :rtype:을 생략하는 project도 많다.
Sphinx extension 설정에 따라 type hint를 문서에 자동 반영할 수 있기 때문이다.
같은 함수로 스타일 비교
같은 함수를 세 스타일로 비교해보자.
먼저 Google style이다.
def find_user(user_id: int, include_inactive: bool = False) -> User | None:
"""Find a user by id.
Args:
user_id: User identifier.
include_inactive: Whether to include inactive users.
Returns:
The matched user, or None if no user exists.
"""
...
NumPy style은 다음과 같다.
def find_user(user_id: int, include_inactive: bool = False) -> User | None:
"""Find a user by id.
Parameters
----------
user_id : int
User identifier.
include_inactive : bool, default=False
Whether to include inactive users.
Returns
-------
User or None
The matched user, or None if no user exists.
"""
...
reStructuredText style은 다음과 같다.
def find_user(user_id: int, include_inactive: bool = False) -> User | None:
"""Find a user by id.
:param user_id: User identifier.
:param include_inactive: Whether to include inactive users.
:returns: The matched user, or None if no user exists.
"""
...
개인 project나 일반 backend code에서는 Google style이 간결해서 쓰기 쉽다. 과학 계산 package나 public API 문서가 중요한 library에서는 NumPy style이 잘 맞는다. Sphinx 기반 문서화를 강하게 쓰는 project에서는 reStructuredText style이 자연스럽다.
어떤 스타일을 선택할까?
선택 기준은 다음과 같다.
| 상황 | 추천 |
|---|---|
| 일반 Python application | Google style |
| FastAPI/Django service 내부 코드 | Google style 또는 짧은 docstring |
| 데이터 과학/수치 계산 library | NumPy style |
| Sphinx 문서화 중심 library | reStructuredText style |
| 작은 내부 함수 | 한 줄 docstring |
| type hint가 잘 되어 있는 codebase | type 반복보다 의미 설명 집중 |
가장 중요한 것은 일관성이다.
한 파일 안에서 Google style과 NumPy style을 섞지 않는다.
한 project 안에서는 가능한 하나의 docstring style을 정한다.
Google style을 기본으로 쓸 때 규칙
Google style을 기본으로 쓴다면 다음 규칙을 추천한다.
- 첫 줄은 함수가 무엇을 하는지 명령형 또는 설명형으로 짧게 쓴다.
- 인자가 2개 이상이거나 의미가 모호하면
Args를 쓴다. - 반환값이 명확하지 않으면
Returns를 쓴다. - caller가 처리해야 하는 예외는
Raises에 쓴다. - type hint와 같은 정보를 반복하지 않는다.
- 내부 구현보다 caller가 알아야 할 계약을 쓴다.
예시는 다음과 같다.
def parse_user(payload: dict[str, object]) -> User:
"""Parse a user from an API payload.
Args:
payload: Raw user payload returned by the external API.
Returns:
Parsed user object.
Raises:
ValueError: If required fields are missing.
"""
...
이 docstring은 caller에게 필요한 정보를 준다.
무엇을 넣어야 하는가?
무엇이 나오는가?
어떤 실패를 예상해야 하는가?
좋은 주석
좋은 주석은 code가 바로 말해주지 못하는 내용을 설명한다.
예를 들어 다음 주석은 의미가 있다.
# 외부 결제 API가 중복 요청을 가끔 거부하므로 idempotency key를 고정한다.
headers["Idempotency-Key"] = order.request_id
이 주석은 code가 왜 이렇게 작성되었는지 설명한다. code만 보면 단순히 header를 넣는 것처럼 보이지만, 실제 이유는 외부 API의 동작 제약이다.
좋은 주석은 보통 다음 내용을 담는다.
왜 이렇게 했는가?
어떤 제약 때문에 필요한가?
나중에 바꿀 때 무엇을 조심해야 하는가?
외부 시스템이나 정책과 어떤 관련이 있는가?
나쁜 주석
나쁜 주석은 code를 그대로 다시 말한다.
# count를 1 증가시킨다.
count += 1
이 주석은 code를 읽으면 바로 알 수 있는 내용을 반복한다. 이런 주석은 오히려 noise가 된다.
다음도 좋지 않다.
# user list를 반복한다.
for user in users:
...
이런 경우에는 주석을 지우고 변수 이름이나 함수 이름을 명확하게 만드는 편이 낫다.
for active_user in active_users:
...
주석이 필요하다는 것은 code 자체가 덜 명확하다는 신호일 수 있다.
주석보다 이름을 먼저 고치기
다음 code를 보자.
# 사용자가 성인인지 확인한다.
if u.age >= 19:
...
주석보다 변수 이름을 고치는 것이 먼저다.
if user.age >= ADULT_AGE:
...
또는 조건을 함수로 뺄 수 있다.
def is_adult(user: User) -> bool:
return user.age >= ADULT_AGE
if is_adult(user):
...
좋은 이름과 작은 함수는 많은 주석을 대체한다.
TODO 주석
나중에 처리해야 할 일을 남길 때는 TODO를 사용할 수 있다.
# TODO: pagination cursor 방식으로 변경하기
def list_users():
...
더 좋은 TODO는 이유와 기준을 함께 남긴다.
# TODO: user 수가 10만 명을 넘으면 offset pagination을 cursor 방식으로 변경한다.
def list_users():
...
팀 project에서는 issue 번호를 함께 남기기도 한다.
# TODO(#123): retry policy를 exponential backoff로 교체한다.
TODO를 남길 때는 언젠가 찾을 수 있게 일정한 형식을 사용하는 것이 좋다.
type hint와 주석
Python에서는 type hint가 많은 설명을 대신할 수 있다.
def send_email(to: str, subject: str, body: str) -> None:
...
이 함수에는 인자 type과 반환 type이 이미 드러나 있다. 따라서 다음 같은 주석은 필요 없다.
# to는 문자열이고, subject도 문자열이고, body도 문자열이다.
주석은 type hint가 설명하지 못하는 의미를 보충해야 한다.
def send_email(to: str, subject: str, body: str) -> None:
"""Send an email through the primary notification provider."""
...
type hint는 형태를 설명하고, docstring과 주석은 의도와 제약을 설명한다.
comment로 code 비활성화하기
개발 중 code를 잠시 비활성화할 때 comment 처리하는 경우가 있다.
# result = slow_api_call()
# print(result)
짧은 실험에서는 괜찮지만, 오래 남겨두면 codebase를 지저분하게 만든다. 버전 관리 시스템이 있다면 사용하지 않는 code는 삭제하는 편이 낫다.
필요하면 git history에서 다시 찾을 수 있다.
남겨야 하는 이유가 있다면 그냥 code를 주석 처리하지 말고 이유를 적는다.
# 외부 API migration 완료 전까지 legacy path를 유지한다.
if use_legacy_api:
...
주석 스타일 기준
일반적으로 Python 주석은 다음 기준을 따른다.
| 상황 | 방식 |
|---|---|
| 짧은 설명 | # 한 줄 주석 |
| 여러 줄 설명 | 각 줄에 # |
| module 설명 | module docstring |
| function/class 설명 | docstring |
| 내부 구현 이유 | code 근처의 주석 |
| 나중에 할 일 | TODO |
예시는 다음과 같다.
"""User validation utilities."""
ADULT_AGE = 19
def is_adult(age: int) -> bool:
"""Return whether the age is considered adult."""
return age >= ADULT_AGE
def normalize_name(name: str) -> str:
# 외부 CRM이 대소문자를 구분하지 않으므로 비교 전에 정규화한다.
return name.strip().lower()
정리
Python 주석 작성 방식의 핵심은 다음과 같다.
- 일반 주석은
#를 사용한다. - 여러 줄 주석도 각 줄에
#를 붙인다. - triple quote는 일반 주석보다 docstring 용도로 사용한다.
- docstring은 module, class, function의 문서로 남는다.
- 좋은 주석은 code가 아니라 이유와 제약을 설명한다.
- code를 그대로 반복하는 주석은 피한다.
- 주석이 필요하면 먼저 이름과 구조를 개선할 수 있는지 본다.
- type hint는 형태를 설명하고, 주석은 의도를 설명한다.
주석은 code의 부족한 부분을 덮는 장식이 아니다. 좋은 주석은 나중에 code를 읽는 사람이 같은 실수를 반복하지 않도록 맥락을 남기는 도구이다.