Published on

파이썬 몫과 나머지 제대로 쓰기 // 와 % 연산자 실전 가이드

Authors

서론

파이썬에서 몫과 나머지를 구하는 //(몫, floor division)와 %(나머지, modulo)는 너무 기본이라 대충 알고 넘어가기 쉽습니다. 그런데 막상 음수 값이 섞이거나, float가 들어오거나, 인덱스/주기 계산처럼 실무 로직에 들어가면 “왜 결과가 이렇지?” 하는 순간이 자주 옵니다.

이 글에서는 //%의 정확한 정의부터, 파이썬 특유의 음수 처리 규칙, divmod() 활용, 실전 패턴(페이지네이션/시간 변환/원형 인덱스), 그리고 자주 만나는 트러블슈팅까지 한 번에 정리합니다.


1) // 와 % 는 무엇을 보장하나

파이썬에서 a // b몫을 내림(floor) 한 값이고, a % b나머지입니다. 중요한 건 이 둘이 항상 아래 관계를 만족하도록 정의되어 있다는 점입니다.

  • a == (a // b) * b + (a % b)

즉, //%는 한 쌍으로 움직입니다.

a, b = 17, 5
q = a // b
r = a % b
print(q, r)          # 3 2
print(q * b + r)     # 17

나머지의 부호 규칙

파이썬에서 %의 결과는 항상 나누는 수(b)의 부호를 따라갑니다.

  • b > 0이면 a % b0 이상 b 미만
  • b < 0이면 a % bb 초과 0 이하

이 규칙이 음수에서의 “의외의 결과”를 만들어냅니다.


2) 음수에서 결과가 달라지는 이유

많은 언어/환경에서 정수 나눗셈은 “0으로 절삭(truncation)”처럼 보이기도 합니다. 하지만 파이썬의 //내림(floor) 입니다.

print(7 // 3)     # 2
print(-7 // 3)    # -3  (내림이므로 -2가 아니라 -3)

print(7 % 3)      # 1
print(-7 % 3)     # 2

-7 % 3이 2일까요? 위의 항등식을 맞추기 위해서입니다.

  • -7 == (-3) * 3 + 2

즉, 파이썬은 //%서로 일관되게 정의합니다.

음수 모듈로가 필요한 실전 이유: 원형 인덱스

리스트에서 뒤로 한 칸 이동 같은 로직을 만들 때, 음수 인덱스가 섞여도 %가 안전하게 정규화해줍니다.

items = ["A", "B", "C", "D"]

idx = 0
idx = (idx - 1) % len(items)
print(idx, items[idx])   # 3 D

이 패턴은 원형 버퍼, 라운드 로빈, 주기적 스케줄링에서 매우 자주 등장합니다.

관련해서 인덱스를 다루는 기본기를 더 다지고 싶다면, 파이썬 리스트 슬라이싱 기초 시작 끝 간격으로 데이터 다루기도 함께 보면 좋습니다.


3) divmod()로 몫과 나머지를 한 번에

divmod(a, b)(a // b, a % b)를 동시에 반환합니다. 반복 계산을 줄이고 의도를 더 명확하게 표현할 수 있습니다.

q, r = divmod(17, 5)
print(q, r)  # 3 2

실전 예: 초를 분/초로 변환

total_seconds = 367
minutes, seconds = divmod(total_seconds, 60)
print(minutes, seconds)  # 6 7

divmod()는 시간 변환, 페이지네이션 계산, 좌표 변환(2D 인덱스) 등에서 특히 깔끔합니다.


4) 자주 쓰는 실전 패턴 5가지

4-1) 페이지네이션: offset 계산

페이지 번호가 1부터 시작한다고 하면:

page = 3
page_size = 20
offset = (page - 1) * page_size
print(offset)  # 40

여기서 “몇 페이지인지”를 반대로 구할 때 //가 핵심입니다.

index = 40
page = index // page_size + 1
print(page)  # 3

4-2) 짝수/홀수 판별

n = 42
if n % 2 == 0:
    print("even")
else:
    print("odd")

4-3) N개마다 끊기(배치 처리)

예: 10개씩 묶어서 처리할 때, 그룹 번호를 만들 수 있습니다.

batch_size = 10
for i in range(35):
    batch_id = i // batch_size
    # i=0~9 -> 0, 10~19 -> 1 ...

4-4) 1차원 인덱스를 2차원 좌표로

행렬을 1차원으로 펼쳤다가 다시 좌표로 복원할 때:

width = 5
index = 17
row, col = divmod(index, width)
print(row, col)  # 3 2

4-5) 주기적 이벤트(매 N번째)

N = 7
for day in range(1, 31):
    if day % N == 0:
        print("weekly event on day", day)

5) 트러블슈팅: float, 음수, 0 나누기

5-1) float에 // 쓰면 결과도 float가 될 수 있음

print(5 // 2)     # 2
print(5.0 // 2)   # 2.0

정수 인덱스가 필요하다면 int() 캐스팅을 명시적으로 하거나, 애초에 정수 연산만 하도록 입력을 정규화하세요.

idx = int(5.0 // 2)

5-2) float는 이진 부동소수점 오차가 있다

print(0.3 % 0.1)  # 0.09999999999999998 처럼 보일 수 있음

금액/정밀도가 중요한 값은 decimal.Decimal을 고려하거나, 정수 단위(예: 원, 센트, 밀리초)로 변환해서 %를 적용하는 것이 안전합니다.

5-3) 0으로 나누면 예외

# ZeroDivisionError
# q = 10 // 0
# r = 10 % 0

입력값 검증(guard)을 두는 게 Best Practice입니다.

def safe_divmod(a: int, b: int):
    if b == 0:
        raise ValueError("b must not be 0")
    return divmod(a, b)

5-4) 음수 나눗셈을 ‘절삭’처럼 기대했다면

만약 어떤 로직이 “0 방향으로 절삭”을 원한다면 math.trunc(a / b) 같은 방식이 필요합니다.

import math
print(math.trunc(-7 / 3))  # -2
print(-7 // 3)             # -3 (floor)

두 방식은 목적이 다르니, 요구사항에 맞게 선택해야 합니다.


6) Best Practice 정리

  1. 몫과 나머지는 한 쌍: 가능하면 divmod()로 의도를 드러내기
  2. 음수 규칙을 항상 염두: %의 부호는 나누는 수를 따른다
  3. 인덱스/주기 처리에는 %가 강력: (i + k) % n 패턴은 거의 정답
  4. float는 주의: 오차가 싫으면 정수 단위로 바꿔서 연산
  5. 0 나누기 방지: 입력 검증을 습관화

결론

파이썬의 //%는 단순 계산용 연산자가 아니라, 항등식 a = (a//b)*b + (a%b)를 항상 만족하도록 설계된 “규칙 기반 도구”입니다. 특히 음수에서 //가 내림(floor)이라는 점, %의 부호가 나누는 수를 따른다는 점을 이해하면 원형 인덱스, 주기 계산, 시간 변환 같은 실무 로직을 훨씬 안정적으로 작성할 수 있습니다.

실전에서는 divmod()를 적극 활용하고, float가 섞일 가능성이 있으면 정수 단위로 정규화하는 습관을 들이면 //%로 인한 버그를 크게 줄일 수 있습니다.