교재 홈: 바이브 코더를 위한 코딩 기초 학습

이 부록은 본문에서 배운 내용을 파일명 변경과 가상 설문 요약에 적용하는 실습입니다. 기본 코드는 1장부터 익힌 내용을 사용합니다. 아래에서 만든 작은 가상 파일과 응답으로 예상한 결과와 실제 결과를 비교하고, 실패 상황도 확인합니다.

A. 파일명 일괄 변경

일괄 변경은 여러 항목에 같은 규칙을 한꺼번에 적용하는 일입니다. **미리보기(dry run)**는 실제로 바꾸기 전에 바뀔 계획만 보여 주는 방식입니다. 충돌은 바꾸려는 이름이 이미 있어서 두 파일이 같은 이름을 사용하려는 상황입니다. 입력과 출력 경로, 새 파일 저장 방식은 6장에서 다시 확인할 수 있습니다.

먼저 프로젝트에 rename_lab 폴더를 만들고, 그 안에 note_a.txt, note_b.txt라는 빈 파일을 직접 만듭니다. 이 폴더에는 실습 파일만 둡니다. 목표는 두 파일 앞에 practice_를 붙이는 것입니다. 곧바로 바꾸기 전에 변경 계획을 출력하고 충돌을 검사합니다.

코드의 PREFIX는 이름 앞에 붙이는 글인 접두사이고 plan은 바꿀 대응 목록입니다. iterdir()는 폴더 안 항목을 읽고 is_file()은 일반 파일인지, suffix는 확장자가 무엇인지 확인합니다. is_symlink()는 다른 위치를 가리키는 특수 파일인 심볼릭 링크인지 검사합니다. with_name()은 파일명 부분만 바꾼 경로를 만들고 rename()은 실제 이름 변경을 수행합니다. --apply는 미리보기 결과를 실제 적용하라는 명령 옵션입니다. 심볼릭 링크와 접근 범위는 12장의 「입력 검증과 권한」절에서 자세히 다룹니다.

다음 내용을 프로젝트의 rename_demo.py로 저장합니다.

import argparse
from pathlib import Path

BASE = Path(__file__).resolve().parent
FOLDER = BASE / "rename_lab"
PREFIX = "practice_"

def make_plan():
    if not FOLDER.is_dir():
        raise ValueError("rename_lab 폴더를 먼저 만드세요")
    if FOLDER.is_symlink():
        raise ValueError("실습 폴더는 심볼릭 링크가 아닌 일반 폴더여야 합니다")
    plan = []
    for source in sorted(FOLDER.iterdir()):
        if source.is_symlink():
            raise ValueError("심볼릭 링크가 있으면 실습을 중단합니다")
        if source.is_file() and source.suffix == ".txt" and not source.name.startswith(PREFIX):
            target = source.with_name(PREFIX + source.name)
            if target.exists():
                raise FileExistsError(f"이미 존재하는 이름: {target.name}")
            plan.append((source, target))
    return plan

def main():
    parser = argparse.ArgumentParser(description="고정 실습 폴더의 파일명 변경")
    parser.add_argument("--apply", action="store_true", help="계획을 실제 적용")
    args = parser.parse_args()
    plan = make_plan()
    for source, target in plan:
        print(f"{source.name} -> {target.name}")
    if not args.apply:
        print("미리보기만 했습니다. 적용하려면 --apply를 추가하세요.")
        return
    for source, target in plan:
        if target.exists():
            raise FileExistsError("대상 파일이 생겨 중단했습니다")
        source.rename(target)
    print("변경 건수:", len(plan))

if __name__ == "__main__":
    main()

python rename_demo.py는 변경 계획만 보여 줍니다. 대상이 rename_lab의 두 파일인지 확인한 뒤 python rename_demo.py --apply를 실행하면 2건이 변경됩니다. 다시 실행하면 이미 접두사가 있는 파일은 건너뛰므로 변경 건수는 0입니다.

프로세스는 실행 중인 프로그램 하나를 뜻합니다. 이 코드는 다른 프로그램이 동시에 연습 폴더를 바꾸지 않는 조건에서 사용합니다. 대상이 없다고 확인한 직후 다른 프로그램이 같은 이름의 파일을 만들면 상황이 달라질 수 있습니다. 이런 동시 변경과 중간 실패 후 복구를 모두 해결하는 일반 도구는 아닙니다. 이번 두 파일 실습은 출력한 변경 전후 이름을 보고 직접 되돌릴 수 있습니다. 실제 대량 작업에는 원본을 따로 보관하는 백업, 변경 기록, 충돌 처리, 복구 절차가 더 필요합니다.

검증 문제: note_a.txt와 practice_note_a.txt를 함께 둔 상태에서 미리보기하면 어떻게 되어야 할까요? 정답은 파일을 바꾸기 전에 충돌을 알리고 중단하는 것입니다. 실행 후 파일 수와 내용이 그대로인지도 확인합니다.

B. 가상 설문 결과 요약

**빈도(frequency)**는 어떤 답이 몇 번 나왔는지 센 수입니다. **비율(proportion)**은 기준 전체 중 일부가 차지하는 몫입니다. **퍼센트(percent, %)**는 그 비율을 전체가 100이라고 보았을 때의 수로 나타낸 것입니다. 저녁 응답이 3개 중 2개라면 2를 3으로 나눈 뒤 100을 곱해 약 66.7%로 표시합니다. 계산이 익숙하지 않으면 계산기를 사용해도 됩니다.

이때 분모는 기준으로 나누는 전체의 개수입니다. 실제 답한 사람만 셀지, 미응답까지 포함할지에 따라 분모가 달라집니다. 미응답은 답을 적지 않은 경우이고, 유효 응답은 정해 둔 선택지에 맞는 답입니다. 이 구분을 먼저 정한 뒤 비율을 계산합니다.

아래는 ‘선호하는 독서 시간대’에 대한 5개의 가상 응답입니다. None과 빈 문자열은 미응답으로 처리하고, 허용한 선택지 이외 값은 별도로 보고합니다. ‘미응답을 분모에서 뺀 비율’과 ‘전체 응답 기회를 분모로 한 비율’을 구별하는 것이 목표입니다.

responses는 응답 목록, allowed는 허용한 선택지, valid는 유효 응답, missing은 미응답 수, invalid는 형식이 맞지 않는 응답 수입니다. Counter는 값별 등장 횟수를 세는 Python 도구입니다. f"{percent:.1f}%"의 .1f는 소수점 아래 한 자리까지 표시하라는 뜻입니다. 남길 자리 다음의 숫자를 보고 가까운 표시값으로 바꾸는 것을 반올림이라고 합니다. %는 이 문자열 안에서는 퍼센트 기호이며 2장에서 배운 나머지 연산자로 실행되지 않습니다.

survey_demo.py에 저장합니다.

from collections import Counter

responses = ["아침", "저녁", "저녁", "", None]
allowed = {"아침", "낮", "저녁"}
valid = []
missing = 0
invalid = 0

for response in responses:
    if response is None or (isinstance(response, str) and response.strip() == ""):
        missing += 1
    elif not isinstance(response, str) or response.strip() not in allowed:
        invalid += 1
    else:
        valid.append(response.strip())

counts = Counter(valid)
print("전체:", len(responses), "유효:", len(valid), "미응답:", missing, "오류:", invalid)
for option in ["아침", "낮", "저녁"]:
    percent = counts[option] / len(valid) * 100 if valid else None
    text = "계산 불가" if percent is None else f"{percent:.1f}%"
    print(option, counts[option], text)

출력은 전체 5·유효 3·미응답 2·오류 0이며, 유효 응답 기준으로 아침 33.3%, 낮 0.0%, 저녁 66.7%입니다. 전체 5를 분모로 하면 저녁 응답은 40.0%입니다. 서로 모순되는 값이 아니라 분모가 다른 값입니다. 소수 첫째 자리 반올림 때문에 일반적인 자료에서는 표시 비율의 합이 정확히 100이 아닐 수 있습니다.