Python ~ : )

기초부터 핵심 개념까지, 파이썬 마스터 여정

제작: ~ : )
(with Gemini)

최종 업데이트: 2025년 5월 18일

▼ 학습 시작하기 ▼

머리말

파이썬 프로그래밍의 세계로 여러분을 초대합니다! 이 가이드는 현대 소프트웨어 개발에서 가장 인기 있고 다재다능한 언어 중 하나인 파이썬의 핵심을 배우고 이해하는 데 도움을 드리고자 마련되었습니다.

파이썬은 배우기 쉬운 간결한 문법, 강력한 표준 라이브러리, 그리고 방대한 서드파티 패키지를 통해 웹 개발, 데이터 과학, 인공 지능, 자동화 등 거의 모든 분야에서 활용되고 있습니다. 이 문서를 통해 여러분은 파이썬의 기본적인 문법부터 시작하여 함수, 객체 지향 프로그래밍의 개념, 그리고 실용적인 프로그래밍 기술까지 단계적으로 학습하게 될 것입니다.

각 장은 명확한 설명과 다양한 코드 예제로 구성되어 있으며, 연습 문제를 통해 배운 내용을 직접 적용하고 점검해볼 수 있습니다. 이 가이드가 여러분의 파이썬 실력 향상에 든든한 길잡이가 되어, 아이디어를 현실로 만드는 프로그래밍의 즐거움을 느끼실 수 있기를 바랍니다.

- ~ : ) (with Gemini)

☆ 개요: 각 장 요약

이 파이썬 가이드는 프로그래밍의 기초부터 고급 개념까지 체계적으로 학습할 수 있도록 구성되었습니다. 각 장의 학습 목표는 다음과 같습니다.

제 1 장: 파이썬이란 무엇인가?

파이썬(Python)은 1991년 네덜란드의 프로그래머인 귀도 반 로섬(Guido van Rossum)에 의해 개발된 고급 프로그래밍 언어입니다. 파이썬이라는 이름은 귀도가 즐겨보던 코미디 프로그램 "몬티 파이튼의 날아다니는 서커스(Monty Python's Flying Circus)"에서 따왔다고 알려져 있습니다. "Life is too short, You need Python" (인생은 너무 짧으니, 파이썬이 필요하다)이라는 유명한 문구가 있을 정도로, 파이썬은 간결하고 명확한 문법을 통해 개발자가 적은 코드로도 많은 작업을 수행할 수 있게 하여 높은 생산성을 제공하는 것을 목표로 합니다. 파이썬은 인터프리터 언어로서 코드를 작성하고 바로 실행하여 결과를 확인할 수 있기 때문에, 프로그래밍 교육용으로도 널리 사용되며 실제 산업 현장에서도 그 강력함을 인정받고 있습니다. 파이썬 소프트웨어 재단(Python Software Foundation, PSF)이 파이썬 언어의 개발과 커뮤니티를 지원하고 있습니다.

1.1. 파이썬의 철학 (The Zen of Python)

파이썬의 설계 철학은 팀 피터스(Tim Peters)가 작성한 "파이썬의 선(The Zen of Python)"이라는 짧은 글에 잘 나타나 있습니다. 파이썬 인터프리터에서 import this를 입력하면 그 전문을 확인할 수 있습니다. 이 철학은 파이썬 커뮤니티의 핵심 가치로 여겨지며, 파이썬다운(Pythonic) 코드를 작성하는 지침이 됩니다. 몇 가지 핵심 원칙은 다음과 같습니다:

이러한 원칙들은 파이썬 코드를 단순히 동작하는 것을 넘어, 읽고 이해하기 쉬우며 장기적으로 유지보수하기 좋게 만드는 데 크게 기여합니다. 가독성을 중시하는 문화는 여러 개발자가 협업하는 프로젝트에서 특히 빛을 발합니다.

예시 코드: 파이썬의 선 확인하기

import this

예상 결과 (일부):

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
...
Readability counts.
...

1.2. 파이썬의 주요 특징

파이썬은 현대 프로그래밍 언어가 갖춰야 할 다양한 장점들을 가지고 있어 개발자들에게 많은 사랑을 받고 있습니다.

1.3. 파이썬의 활용 분야

파이썬은 그 다재다능함과 사용 편의성, 그리고 강력한 생태계 덕분에 학계와 산업계를 막론하고 매우 다양한 분야에서 핵심적인 도구로 활용되고 있습니다:

이처럼 파이썬은 배우기 쉬우면서도 강력한 기능과 유연성, 그리고 방대한 생태계를 바탕으로 초보 개발자부터 숙련된 전문가까지 모두에게 매력적인 선택지가 되고 있으며, 현대 프로그래밍 언어의 중심으로 확고히 자리 잡고 있습니다.

제 2 장: 파이썬 기본 문법

프로그래밍 언어를 배운다는 것은 해당 언어의 '문법'을 익히는 것에서 시작합니다. 이번 장에서는 파이썬 프로그램을 구성하는 가장 기본적인 요소들인 주석, 변수, 다양한 데이터 타입, 연산자, 그리고 사용자 입출력 방법에 대해 자세히 학습합니다. 이 기본 지식은 앞으로 더 복잡한 프로그램을 작성하는 데 튼튼한 기초가 될 것입니다.

2.1. 주석 (Comments)

주석은 코드에 대한 설명을 추가하는 데 사용되며, 프로그램 실행에는 영향을 주지 않습니다. 파이썬에서 주석을 작성하는 방법은 다음과 같습니다:

예시 코드: 주석 사용

# 이것은 한 줄 주석입니다.
print("Hello, Python!") # 이 부분도 주석입니다.

'''
이것은
여러 줄에 걸친
주석처럼 사용될 수 있습니다.
(실제로는 여러 줄 문자열입니다)
'''

"""
큰따옴표 세 개를 사용한
여러 줄 주석 (문자열)도 가능합니다.
"""

def my_function():
    """이것은 함수의 설명을 담은 docstring입니다."""
    pass

print("주석은 코드 실행에 영향을 주지 않습니다.")

예상 결과:

Hello, Python!
주석은 코드 실행에 영향을 주지 않습니다.

2.2. 변수 (Variables)

변수(Variable)는 데이터를 저장하기 위한 메모리 공간에 붙이는 이름입니다. 파이썬에서 변수를 사용할 때는 다음과 같은 특징과 규칙을 따릅니다.

예시 코드: 변수 사용

# 변수에 값 할당
my_integer = 100
my_float = 3.14
my_string = "Hello, World!"
is_python_fun = True

print(my_integer)
print(my_float)
print(my_string)
print(is_python_fun)

# 값 변경
my_integer = 200
print("변경된 my_integer:", my_integer)

# 다중 할당
a, b, c = 10, 20, "Python"
print(a, b, c)

# 스네이크 케이스 컨벤션
student_id = "S12345"
max_value = 9999
print(student_id, max_value)

# 파이썬 예약어 확인 (실행하면 키워드 목록 출력)
# import keyword
# print(keyword.kwlist)

예상 결과:

100
3.14
Hello, World!
True
변경된 my_integer: 200
10 20 Python
S12345 9999

2.3. 기본 데이터 타입 (Basic Data Types)

파이썬은 다양한 종류의 데이터를 처리할 수 있도록 여러 가지 내장 데이터 타입을 제공합니다.

2.3.1. 숫자형 (Numeric Types)

예시 코드: 숫자형 데이터 타입

# 정수형 (int)
age = 25
year = 2025
negative_int = -100
very_large_int = 12345678901234567890
print("나이:", age, type(age))
print("연도:", year, type(year))
print("큰 정수:", very_large_int, type(very_large_int))

# 실수형 (float)
pi = 3.14159
temperature = -5.5
print("원주율:", pi, type(pi))
print("온도:", temperature, type(temperature))
# 부동소수점 오차 예시 (항상 발생하는 것은 아님)
print("0.1 + 0.2 =", 0.1 + 0.2) # 예상: 0.3, 실제: 0.30000000000000004 와 유사한 결과

# 복소수형 (complex)
complex_num1 = 3 + 4j
complex_num2 = complex(1, -2) # complex() 함수 사용
print("복소수1:", complex_num1, type(complex_num1))
print("복소수2:", complex_num2, "실수부:", complex_num2.real, "허수부:", complex_num2.imag)

예상 결과:

나이: 25 <class 'int'>
연도: 2025 <class 'int'>
큰 정수: 12345678901234567890 <class 'int'>
원주율: 3.14159 <class 'float'>
온도: -5.5 <class 'float'>
0.1 + 0.2 = 0.30000000000000004
복소수1: (3+4j) <class 'complex'>
복소수2: (1-2j) 실수부: 1.0 허수부: -2.0

2.3.2. 문자열 (String Type - str)

문자열은 텍스트 데이터를 나타내며, 작은따옴표('), 큰따옴표("), 또는 세 개의 따옴표(''' 또는 """)로 감싸서 표현합니다. 세 개의 따옴표는 여러 줄에 걸친 문자열을 만들 때 유용합니다.

예시 코드: 문자열 데이터 타입

# 문자열 생성
s1 = 'Hello'
s2 = "Python"
s3 = """This is a
multi-line
string."""
print(s1, type(s1))
print(s2)
print(s3)

# 이스케이프 시퀀스
print("줄 바꿈: 첫째 줄\\n둘째 줄")
print("탭: 이름\\t나이")

# 인덱싱과 슬라이싱
text = "Python Programming"
print("첫 글자:", text[0])        # P
print("마지막 글자:", text[-1])     # g
print("부분 문자열 (0~5):", text[0:6]) # Python (6은 포함 안됨)
print("부분 문자열 (7부터 끝까지):", text[7:]) # Programming
print("문자열 뒤집기:", text[::-1]) # gnimmargorP nohtyP

# 문자열 연산
greeting = "안녕" + " " + "하세요"
print(greeting)
laugh = "하" * 3
print(laugh)

# 문자열 메서드
print("길이:", len(text))
print("대문자로:", text.upper())
print("소문자로:", text.lower())
words = "  공백 제거 예시  ".strip()
print(f"공백 제거: '{words}'")
word_list = "하나,둘,셋".split(',')
print("분리:", word_list)
print("결합:", "-".join(word_list))

# f-string 포매팅
name = "Alice"
age = 30
print(f"이름: {name}, 나이: {age}세, 내년 나이: {age + 1}세")

# 불변성
original_string = "abc"
# original_string[0] = 'x' # TypeError 발생!
new_string = original_string.replace('a', 'x')
print("원본:", original_string)
print("변경 후 (새 문자열):", new_string)

예상 결과:

Hello <class 'str'>
Python
This is a
multi-line
string.
줄 바꿈: 첫째 줄
둘째 줄
탭: 이름	나이
첫 글자: P
마지막 글자: g
부분 문자열 (0~5): Python
부분 문자열 (7부터 끝까지): Programming
문자열 뒤집기: gnimmargorP nohtyP
안녕 하세요
하하하
길이: 18
대문자로: PYTHON PROGRAMMING
소문자로: python programming
공백 제거: '공백 제거 예시'
분리: ['하나', '둘', '셋']
결합: 하나-둘-셋
이름: Alice, 나이: 30세, 내년 나이: 31세
원본: abc
변경 후 (새 문자열): xbc

2.3.3. 불리언 (Boolean Type - bool)

불리언(또는 불린, bool) 타입은 참(True)과 거짓(False) 두 가지 값만을 가집니다. 이 값들은 조건문이나 반복문에서 조건을 판단하는 데 주로 사용됩니다.

파이썬에서는 다양한 값들이 불리언 문맥에서 참 또는 거짓으로 평가될 수 있습니다 (Truthiness and Falsiness):

예시 코드: 불리언 데이터 타입

is_active = True
is_greater = 5 > 3 # True
is_equal = (1 + 2 == 4) # False

print("활성 상태:", is_active, type(is_active))
print("5 > 3 ?", is_greater)
print("1 + 2 == 4 ?", is_equal)

# Truthy와 Falsy 값 확인
print("bool(0):", bool(0))         # False
print("bool(10):", bool(10))       # True
print("bool(''):", bool(''))       # False
print("bool('Hello'):", bool('Hello')) # True
print("bool([]):", bool([]))       # False
print("bool([1, 2]):", bool([1, 2])) # True
print("bool(None):", bool(None))     # False

if 100: # 100은 True로 평가됨
    print("100은 참입니다.")
if "": # 빈 문자열은 False로 평가됨
    print("이 문장은 실행되지 않습니다.")
else:
    print("빈 문자열은 거짓입니다.")

예상 결과:

활성 상태: True <class 'bool'>
5 > 3 ? True
1 + 2 == 4 ? False
bool(0): False
bool(10): True
bool(''): False
bool('Hello'): True
bool([]): False
bool([1, 2]): True
bool(None): False
100은 참입니다.
빈 문자열은 거짓입니다.

2.4. 타입 변환 (Type Casting / Type Conversion)

때로는 한 데이터 타입을 다른 데이터 타입으로 변환해야 할 필요가 있습니다. 파이썬은 다음과 같은 내장 함수를 통해 타입 변환을 지원합니다.

예시 코드: 타입 변환

num_str = "123"
num_int = int(num_str)
print(f"문자열 '{num_str}' -> 정수: {num_int}, 타입: {type(num_int)}")

price_float = 99.99
price_int = int(price_float) # 소수점 이하 버림
print(f"실수 {price_float} -> 정수: {price_int}, 타입: {type(price_int)}")

# 정수를 실수로
age_int = 30
age_float = float(age_int)
print(f"정수 {age_int} -> 실수: {age_float}, 타입: {type(age_float)}")

# 숫자를 문자열로
item_code = 404
code_str = str(item_code)
print(f"정수 {item_code} -> 문자열: '{code_str}', 타입: {type(code_str)}")

# 문자열을 불리언으로
empty_str_bool = bool("")
non_empty_str_bool = bool("Python")
print(f"bool('') -> {empty_str_bool}")
print(f"bool('Python') -> {non_empty_str_bool}")

# 변환 불가능한 경우 (ValueError 발생)
# invalid_str_to_int = int("abc")
# print(invalid_str_to_int)

예상 결과:

문자열 '123' -> 정수: 123, 타입: <class 'int'>
실수 99.99 -> 정수: 99, 타입: <class 'int'>
정수 30 -> 실수: 30.0, 타입: <class 'float'>
정수 404 -> 문자열: '404', 타입: <class 'str'>
bool('') -> False
bool('Python') -> True

2.5. 연산자 (Operators)

연산자는 변수나 값에 대해 특정 연산을 수행하도록 하는 기호입니다. 파이썬은 다양한 종류의 연산자를 제공합니다.

2.5.1. 산술 연산자 (Arithmetic Operators)

연산자설명예시 (a=7, b=3)결과
+덧셈a + b10
-뺄셈a - b4
*곱셈a * b21
/나눗셈 (실수 결과)a / b2.333...
//정수 나눗셈 (몫)a // b2
%나머지a % b1
**거듭제곱a ** b343 (7의 3제곱)

2.5.2. 할당 연산자 (Assignment Operators)

연산자설명예시동일 표현
=할당x = 5
+=덧셈 후 할당x += 3x = x + 3
-=뺄셈 후 할당x -= 3x = x - 3
*=곱셈 후 할당x *= 3x = x * 3
/=나눗셈 후 할당x /= 3x = x / 3
%=나머지 연산 후 할당x %= 3x = x % 3
//=정수 나눗셈 후 할당x //= 3x = x // 3
**=거듭제곱 후 할당x **= 3x = x ** 3

2.5.3. 비교 연산자 (Comparison Operators)

두 값을 비교하여 True 또는 False를 반환합니다.

연산자설명예시 (a=7, b=3)결과
==같다a == bFalse
!=다르다a != bTrue
>크다a > bTrue
<작다a < bFalse
>=크거나 같다a >= 7True
<=작거나 같다b <= 3True

2.5.4. 논리 연산자 (Logical Operators)

불리언 값들 간의 논리 연산을 수행합니다.

연산자설명예시 (x=True, y=False)결과
and논리곱 (두 피연산자 모두 True일 때 True)x and yFalse
or논리합 (두 피연산자 중 하나라도 True일 때 True)x or yTrue
not논리 부정 (피연산자의 반대 값을 반환)not xFalse

단축 평가 (Short-circuit Evaluation): 논리 연산자는 왼쪽 피연산자부터 평가하며, 전체 결과가 확정되면 오른쪽 피연산자는 평가하지 않습니다. 예를 들어 True or some_function()에서 some_function()은 호출되지 않습니다.

2.5.5. 멤버십 연산자 (Membership Operators)

특정 값이 시퀀스(문자열, 리스트, 튜플 등)나 컬렉션(세트, 딕셔너리의 키)에 포함되어 있는지 확인합니다.

연산자설명예시결과
in피연산자가 시퀀스에 포함되어 있으면 True'P' in 'Python'True
not in피연산자가 시퀀스에 포함되어 있지 않으면 True'Java' not in ['Python', 'C++']True

2.5.6. 식별 연산자 (Identity Operators)

두 변수가 같은 객체(메모리 주소)를 참조하는지 확인합니다. ==가 값의 동등성을 비교하는 반면, is는 객체의 정체성(identity)을 비교합니다.

연산자설명예시
is두 변수가 같은 객체를 참조하면 Truex = [1,2]; y = x; x is y # True
is not두 변수가 다른 객체를 참조하면 Truex = [1,2]; z = [1,2]; x is not z # True (내용은 같지만 다른 객체)

참고: 작은 정수나 짧은 문자열의 경우 파이썬 내부 최적화로 인해 is 연산이 예기치 않게 True를 반환할 수 있으므로, 값 비교에는 항상 ==를 사용하는 것이 안전합니다.

2.5.7. 연산자 우선순위 (Operator Precedence)

하나의 표현식에 여러 연산자가 사용될 경우, 연산자 우선순위에 따라 계산 순서가 결정됩니다. 예를 들어 곱셈(*)은 덧셈(+)보다 먼저 계산됩니다. 괄호(())를 사용하면 우선순위를 명시적으로 지정할 수 있으며, 가독성을 위해서도 괄호 사용이 권장됩니다.

일반적인 우선순위 (높은 순 -> 낮은 순): () > ** > *, /, //, % > +, - > 비교 연산자 > is, in > not > and > or. (자세한 내용은 공식 문서를 참고하세요.)

예시 코드: 다양한 연산자

# 산술 연산
a = 10
b = 3
print(f"{a} + {b} = {a + b}")
print(f"{a} / {b} = {a / b}")   # 실수 나눗셈
print(f"{a} // {b} = {a // b}") # 정수 나눗셈 (몫)
print(f"{a} % {b} = {a % b}")   # 나머지
print(f"{a} ** {b} = {a ** b}") # 거듭제곱

# 할당 연산
x = 5
x += 2 # x = x + 2
print("x (5 += 2):", x)

# 비교 연산
print("a > b:", a > b)
print("a == 10:", a == 10)

# 논리 연산
is_adult = True
has_ticket = False
can_enter = is_adult and has_ticket
print("입장 가능 (성인 and 티켓 소지):", can_enter)
can_enter_if_either = is_adult or has_ticket
print("입장 가능 (성인 or 티켓 소지):", can_enter_if_either)
print("is_adult is not False:", not False == is_adult) # True

# 멤버십 연산
my_list = [1, 2, 3, 'Python']
print("3 in my_list:", 3 in my_list)
print("'Java' not in my_list:", 'Java' not in my_list)

# 식별 연산
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

print("list1 == list2 (값 비교):", list1 == list2) # True
print("list1 is list2 (객체 비교):", list1 is list2) # False (다른 메모리 주소)
print("list1 is list3 (객체 비교):", list1 is list3) # True (같은 메모리 주소)

# 연산자 우선순위
result = 2 + 3 * 4 # 2 + 12 = 14
print("2 + 3 * 4 =", result)
result_with_paren = (2 + 3) * 4 # 5 * 4 = 20
print("(2 + 3) * 4 =", result_with_paren)

예상 결과:

10 + 3 = 13
10 / 3 = 3.3333333333333335
10 // 3 = 3
10 % 3 = 1
10 ** 3 = 1000
x (5 += 2): 7
a > b: True
a == 10: True
입장 가능 (성인 and 티켓 소지): False
입장 가능 (성인 or 티켓 소지): True
is_adult is not False: True
3 in my_list: True
'Java' not in my_list: True
list1 == list2 (값 비교): True
list1 is list2 (객체 비교): False
list1 is list3 (객체 비교): True
2 + 3 * 4 = 14
(2 + 3) * 4 = 20

2.6. 사용자 입출력 (Input/Output)

프로그램은 사용자와 상호작용하기 위해 데이터를 입력받거나 결과를 출력해야 합니다.

2.6.1. 입력 (Input)

input() 함수를 사용하여 사용자로부터 키보드 입력을 받을 수 있습니다. input() 함수는 항상 입력된 값을 문자열(str) 형태로 반환합니다. 따라서 숫자로 사용하려면 적절한 타입 변환(예: int(), float())이 필요합니다.

input() 함수에 문자열 인자를 전달하면, 입력받기 전에 해당 문자열을 프롬프트 메시지로 화면에 표시합니다.

2.6.2. 출력 (Output)

print() 함수를 사용하여 화면에 데이터를 출력합니다. print() 함수는 하나 이상의 인자를 받을 수 있으며, 각 인자는 공백으로 구분되어 출력됩니다.

예시 코드: 사용자 입출력

# 사용자 입력
user_name = input("이름을 입력하세요: ")
print(f"안녕하세요, {user_name}님!")

age_str = input("나이를 입력하세요: ")
# input()은 항상 문자열을 반환하므로, 숫자로 사용하려면 타입 변환 필요
try:
    age_num = int(age_str)
    print(f"{user_name}님은 {age_num}세 이시군요.")
    print(f"10년 후에는 {age_num + 10}세가 됩니다.")
except ValueError:
    print("나이는 숫자로 입력해주세요.")

# print() 함수의 sep과 end 인자 활용
print("사과", "바나나", "딸기", sep=", ", end=".\n") # , 로 구분하고 .으로 끝냄
print("이것은", end=" ")
print("한 줄에", end=" ")
print("이어집니다.")

예상 결과 (사용자 입력에 따라 달라짐):

이름을 입력하세요: (사용자가 '홍길동' 입력)
안녕하세요, 홍길동님!
나이를 입력하세요: (사용자가 '30' 입력)
홍길동님은 30세 이시군요.
10년 후에는 40세가 됩니다.
사과, 바나나, 딸기.
이것은 한 줄에 이어집니다.

이것으로 파이썬의 가장 기본적인 문법 요소들에 대한 학습을 마칩니다. 변수를 사용하여 데이터를 저장하고, 다양한 데이터 타입과 연산자를 활용하여 데이터를 가공하며, 입출력을 통해 사용자와 상호작용하는 방법을 익혔습니다. 이 지식은 다음 장에서 배울 제어 흐름과 자료 구조를 이해하는 데 필수적입니다.

제 3 장: 제어 흐름

프로그램은 일반적으로 코드가 작성된 순서대로 위에서 아래로 실행됩니다. 하지만 때로는 특정 조건에 따라 다른 코드를 실행하거나, 특정 작업을 여러 번 반복해야 할 필요가 있습니다. 이처럼 프로그램의 실행 흐름을 제어하는 구조를 '제어 흐름문'이라고 합니다. 이번 장에서는 파이썬의 주요 제어 흐름문인 조건문(if)과 반복문(while, for), 그리고 이러한 루프의 흐름을 바꾸는 제어문(break, continue, pass)에 대해 자세히 알아봅니다.

3.1. 조건문 (Conditional Statements)

조건문은 주어진 조건식(conditional expression)의 결과가 참(True)인지 거짓(False)인지에 따라 서로 다른 코드 블록을 실행하도록 합니다.

3.1.1. if

가장 기본적인 조건문으로, 특정 조건이 참(True)일 경우에만 코드 블록을 실행합니다.

if 조건식:
    # 조건식이 True일 때 실행될 코드 블록
    # 이 부분은 반드시 들여쓰기(indentation) 되어야 합니다.
    명령문1
    명령문2
# if 문 다음 코드 (들여쓰기 없음)

파이썬에서 코드 블록은 들여쓰기로 구분됩니다. 보통 공백 4칸 또는 탭(Tab) 1개를 사용하며, 한 파일 내에서는 일관된 방식을 사용하는 것이 중요합니다.

3.1.2. if-else

if 문의 조건식이 거짓(False)일 경우 실행할 코드 블록을 지정할 수 있습니다.

if 조건식:
    # 조건식이 True일 때 실행될 코드 블록
    명령문_A
else:
    # 조건식이 False일 때 실행될 코드 블록
    명령문_B

3.1.3. if-elif-else

여러 개의 조건을 순차적으로 검사하여, 처음으로 참(True)이 되는 조건의 코드 블록을 실행합니다. 모든 조건이 거짓이면 else 블록(선택 사항)이 실행됩니다.

if 조건식1:
    # 조건식1이 True일 때 실행
    명령문_1
elif 조건식2:
    # 조건식1이 False이고 조건식2가 True일 때 실행
    명령문_2
elif 조건식3:
    # 앞의 조건들이 모두 False이고 조건식3이 True일 때 실행
    명령문_3
else:
    # 위의 모든 조건식들이 False일 때 실행 (선택 사항)
    명령문_Else

elif는 "else if"의 줄임말이며, 여러 개를 사용할 수 있습니다.

예시 코드: if, if-else, if-elif-else

# if 문
age = 20
if age >= 19:
    print("성인입니다.")

# if-else 문
temperature = 15
if temperature <= 10:
    print("날씨가 춥습니다. 외투를 챙기세요.")
else:
    print("날씨가 춥지 않습니다.")

# if-elif-else 문
score = 85
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
else:
    grade = "F"
print(f"점수: {score}, 학점: {grade}")

# 조건식에는 비교 연산자, 논리 연산자, 멤버십 연산자 등 사용 가능
num = 7
if num > 0 and num % 2 == 0:
    print(f"{num}은 양의 짝수입니다.")
elif num > 0 and num % 2 != 0:
    print(f"{num}은 양의 홀수입니다.")
else:
    print(f"{num}은 0 또는 음수입니다.")

예상 결과:

성인입니다.
날씨가 춥지 않습니다.
점수: 85, 학점: B
7은 양의 홀수입니다.

3.1.4. 중첩 if 문 (Nested if statements)

if, elif, else 블록 내부에 또 다른 if 문을 포함시켜 더 복잡한 조건을 만들 수 있습니다. 다만, 과도한 중첩은 코드의 가독성을 해칠 수 있으므로 주의해야 합니다.

예시 코드: 중첩 if

user_id = "admin"
user_pw = "1234"

if user_id == "admin":
    print("관리자 계정입니다.")
    if user_pw == "1234":
        print("로그인 성공!")
    else:
        print("비밀번호가 틀렸습니다.")
else:
    print("존재하지 않는 아이디입니다.")

예상 결과:

관리자 계정입니다.
로그인 성공!

3.1.5. 간결한 조건 표현 (Conditional Expressions / Ternary Operator)

간단한 if-else 구조는 한 줄로 표현할 수 있는 '조건부 표현식'을 사용할 수 있습니다. (다른 언어의 삼항 연산자와 유사합니다.)

참일_때의_값 if 조건식 else 거짓일_때의_값

예시 코드: 조건부 표현식

score = 75
result_message = "합격" if score >= 70 else "불합격"
print(f"점수 {score}점은 {result_message}입니다.")

number = -5
abs_value = number if number >= 0 else -number
print(f"{number}의 절댓값은 {abs_value}입니다.")

예상 결과:

점수 75점은 합격입니다.
-5의 절댓값은 5입니다.

3.2. 반복문 (Looping Statements)

반복문은 특정 코드 블록을 여러 번 반복해서 실행하고자 할 때 사용합니다. 파이썬에는 while 문과 for 문 두 가지 주요 반복문이 있습니다.

3.2.1. while

while 문은 주어진 조건식이 참(True)인 동안 코드 블록을 계속해서 반복 실행합니다. 조건식이 거짓(False)이 되면 반복을 멈춥니다.

while 조건식:
    # 조건식이 True인 동안 반복 실행될 코드 블록
    # 이 부분도 반드시 들여쓰기 되어야 합니다.
    명령문1
    명령문2
    # 루프 내에서 조건식의 상태를 변경하는 코드가 필요할 수 있음 (무한 루프 방지)
# while 문 다음 코드

만약 while 문의 조건식이 항상 참이 되도록 작성되거나, 루프 내에서 조건식의 상태가 변경되지 않아 거짓이 될 수 없다면 '무한 루프(infinite loop)'에 빠지게 됩니다. 이 경우 보통 Ctrl + C (또는 개발 환경의 중지 버튼)로 강제 종료해야 합니다.

예시 코드: while 문 기본 사용

# 1부터 5까지 출력
count = 1
while count <= 5:
    print(count)
    count += 1 # count 값을 1씩 증가시켜 언젠가 조건이 False가 되도록 함
print("루프 종료")

# 사용자 입력을 받아 특정 단어가 입력될 때까지 반복
user_input = ""
while user_input.lower() != "exit": # 대소문자 구분 없이 "exit" 입력 시 종료
    user_input = input("메시지를 입력하세요 ('exit' 입력 시 종료): ")
    if user_input.lower() != "exit":
        print(f"입력하신 메시지: {user_input}")
print("프로그램을 종료합니다.")

예상 결과 (두 번째 예시는 사용자 입력에 따라 달라짐):

1
2
3
4
5
루프 종료
메시지를 입력하세요 ('exit' 입력 시 종료): (사용자 입력)
입력하신 메시지: (사용자 입력 내용)
...
메시지를 입력하세요 ('exit' 입력 시 종료): exit
프로그램을 종료합니다.

3.2.2. for

for 문은 시퀀스(sequence) 자료형 (예: 리스트, 튜플, 문자열)이나 다른 반복 가능한 객체(iterable)의 항목들을 순서대로 하나씩 가져와 코드 블록을 반복 실행합니다. 정해진 횟수만큼 반복하는 데 유용합니다.

for 변수 in 반복_가능한_객체:
    # 객체의 각 항목에 대해 반복 실행될 코드 블록
    # '변수'에는 매 반복마다 객체의 다음 항목이 할당됨
    명령문1
    명령문2
# for 문 다음 코드
range() 함수와 함께 사용

range() 함수는 특정 범위의 숫자 시퀀스를 생성하여 for 문과 함께 자주 사용됩니다.

예시 코드: for 문 기본 사용

# 리스트 항목 순회
fruits = ["사과", "바나나", "딸기"]
for fruit in fruits:
    print(fruit)

# 문자열 문자 순회
message = "Hello"
for char in message:
    print(char)

# range() 함수 사용
print("range(5):")
for i in range(5): # 0, 1, 2, 3, 4
    print(i)

print("range(2, 6):")
for i in range(2, 6): # 2, 3, 4, 5
    print(i)

print("range(1, 10, 2):")
for i in range(1, 10, 2): # 1, 3, 5, 7, 9
    print(i)

# 1부터 10까지의 합 구하기
total_sum = 0
for i in range(1, 11): # 1부터 10까지
    total_sum += i
print(f"1부터 10까지의 합: {total_sum}")

예상 결과:

사과
바나나
딸기
H
e
l
l
o
range(5):
0
1
2
3
4
range(2, 6):
2
3
4
5
range(1, 10, 2):
1
3
5
7
9
1부터 10까지의 합: 55

3.2.3. 중첩 반복문 (Nested Loops)

하나의 반복문 내부에 다른 반복문을 포함시켜 사용할 수 있습니다. 바깥쪽 루프가 한 번 반복될 때마다 안쪽 루프는 전체를 반복합니다. 구구단 출력이나 2차원 데이터를 다룰 때 유용합니다.

예시 코드: 중첩 for 문 (구구단 2단~3단 출력)

for i in range(2, 4):      # 바깥쪽 루프 (2단, 3단)
    print(f"--- {i}단 ---")
    for j in range(1, 10):   # 안쪽 루프 (곱하는 수 1~9)
        print(f"{i} x {j} = {i*j}")
    print() # 단 사이 줄바꿈

예상 결과:

--- 2단 ---
2 x 1 = 2
2 x 2 = 4
...
2 x 9 = 18

--- 3단 ---
3 x 1 = 3
3 x 2 = 6
...
3 x 9 = 27

3.2.4. 반복문의 else

파이썬의 반복문(while, for)은 특이하게도 else 절을 가질 수 있습니다. 이 else 절은 루프가 중간에 break 문으로 중단되지 않고, 모든 반복을 정상적으로 마쳤을 때 실행됩니다.

예시 코드: 반복문의 else

# for 문과 else
print("for-else 예제:")
for i in range(1, 6): # 1부터 5까지
    print(i)
    if i == 30: # 이 조건은 절대 참이 될 수 없음 (break 실행 안됨)
        print("break 실행됨")
        break
else:
    print("루프가 정상적으로 완료되었습니다 (break 만나지 않음).")

print("\\nwhile-else 예제:")
count = 0
while count < 5:
    print(count)
    count += 1
    # if count == 3:
    #     print("break로 중단")
    #     break
else:
    print("while 루프가 정상적으로 완료되었습니다.")

예상 결과:

for-else 예제:
1
2
3
4
5
루프가 정상적으로 완료되었습니다 (break 만나지 않음).

while-else 예제:
0
1
2
3
4
while 루프가 정상적으로 완료되었습니다.

3.3. 루프 제어문 (Loop Control Statements)

반복문 실행 중에 특정 조건에 따라 루프의 흐름을 변경할 수 있는 제어문들입니다.

3.3.1. break

break 문은 현재 실행 중인 가장 안쪽의 반복문(for 또는 while)을 즉시 중단하고 빠져나옵니다. 반복문의 else 절은 break로 루프가 중단될 경우 실행되지 않습니다.

예시 코드: break

# 1부터 10까지 출력하되, 5를 만나면 중단
for i in range(1, 11):
    if i == 5:
        print("5를 만나 루프를 중단합니다.")
        break
    print(i)
else:
    print("이 문장은 break로 인해 실행되지 않습니다.") # 실행 안됨

# 무한 루프와 break
while True: # 무한 루프
    command = input("명령을 입력하세요 ('q' 입력 시 종료): ")
    if command == 'q':
        print("종료합니다.")
        break
    print(f"입력된 명령: {command}")

예상 결과 (두 번째 예시는 사용자 입력에 따라 달라짐):

1
2
3
4
5를 만나 루프를 중단합니다.
명령을 입력하세요 ('q' 입력 시 종료): (사용자 입력)
입력된 명령: (사용자 입력 내용)
...
명령을 입력하세요 ('q' 입력 시 종료): q
종료합니다.

3.3.2. continue

continue 문은 현재 반복에서 루프의 나머지 코드 실행을 건너뛰고, 즉시 다음 반복을 시작합니다 (while 문의 경우 조건 검사로, for 문의 경우 다음 항목으로 이동).

예시 코드: continue

# 1부터 10까지의 수 중에서 짝수만 출력 (홀수는 건너뜀)
for i in range(1, 11):
    if i % 2 != 0: # 홀수이면
        continue   # 다음 반복으로 넘어감
    print(i)       # 짝수일 때만 실행

예상 결과:

2
4
6
8
10

3.3.3. pass

pass 문은 아무 작업도 하지 않는 문장입니다. 문법적으로 문장이 필요하지만 프로그램이 특별히 할 일이 없을 때 사용합니다. 주로 나중에 구현할 코드의 위치를 표시하거나, 조건문이나 함수, 클래스의 빈 본문을 만들 때 사용됩니다.

예시 코드: pass

# 나중에 구현할 함수의 placeholder
def my_complex_function(data):
    pass # TODO: 나중에 여기에 복잡한 로직을 구현해야 함

# 조건문에서 특정 조건에 아무 작업도 하지 않을 때
number = 5
if number > 0:
    print("양수입니다.")
elif number < 0:
    # 음수인 경우 특별한 처리를 하지 않음 (하지만 문법상 블록이 필요)
    pass
else:
    print("0입니다.")

# 빈 클래스 정의
class MyEmptyClass:
    pass

my_complex_function(None) # 오류 없이 실행됨
print("pass 예제 완료")

예상 결과:

양수입니다.
pass 예제 완료

이번 장에서는 파이썬에서 프로그램의 실행 흐름을 제어하는 다양한 방법을 배웠습니다. 조건에 따라 실행 경로를 바꾸는 if 문, 특정 작업을 반복하는 whilefor 문, 그리고 이들의 흐름을 미세 조정하는 break, continue, pass 문을 익혔습니다. 이러한 제어 구조는 복잡한 로직을 구현하고 문제를 해결하는 데 핵심적인 역할을 합니다. 다음 장에서는 여러 데이터를 효과적으로 관리할 수 있는 파이썬의 자료 구조에 대해 알아보겠습니다.

제 4 장: 자료 구조

프로그래밍에서 자료 구조(Data Structure)는 여러 데이터를 효과적으로 저장, 관리하고 사용하기 위한 체계적인 방식입니다. 어떤 자료 구조를 선택하느냐에 따라 프로그램의 성능과 코드의 간결성이 크게 달라질 수 있습니다. 파이썬은 사용하기 쉽고 강력한 여러 내장 자료 구조를 제공하여 개발자가 다양한 문제를 효율적으로 해결할 수 있도록 돕습니다. 이번 장에서는 파이썬의 핵심 내장 자료 구조인 리스트(List), 튜플(Tuple), 딕셔너리(Dictionary), 그리고 세트(Set)에 대해 자세히 살펴보겠습니다.

4.1. 리스트 (List - list)

리스트는 파이썬에서 가장 유연하고 널리 사용되는 시퀀스(sequence) 자료형 중 하나입니다. 여러 개의 값을 순서대로 저장하고, 이 값들을 변경할 수 있습니다.

4.1.1. 리스트의 특징

4.1.2. 리스트 생성

리스트는 대괄호([])를 사용하여 생성하며, 각 요소는 쉼표(,)로 구분합니다. list() 생성자를 사용하여 다른 반복 가능한(iterable) 객체로부터 리스트를 만들 수도 있습니다.

예시 코드: 리스트 생성

# 빈 리스트 생성
empty_list1 = []
empty_list2 = list()
print("빈 리스트1:", empty_list1, type(empty_list1))
print("빈 리스트2:", empty_list2, type(empty_list2))

# 다양한 요소를 가진 리스트
mixed_list = [1, "Python", 3.14, True, [10, 20]]
print("혼합 리스트:", mixed_list)

# 문자열로부터 리스트 생성
char_list = list("hello")
print("문자열로부터 생성:", char_list)

# range()로부터 리스트 생성
num_list = list(range(1, 6)) # 1, 2, 3, 4, 5
print("range로부터 생성:", num_list)

예상 결과:

빈 리스트1: [] <class 'list'>
빈 리스트2: [] <class 'list'>
혼합 리스트: [1, 'Python', 3.14, True, [10, 20]]
문자열로부터 생성: ['h', 'e', 'l', 'l', 'o']
range로부터 생성: [1, 2, 3, 4, 5]

4.1.3. 인덱싱(Indexing)과 슬라이싱(Slicing)

리스트의 각 요소는 고유한 인덱스(0부터 시작하는 정수)를 가지며, 이를 통해 특정 요소에 접근할 수 있습니다. 슬라이싱을 사용하면 리스트의 일부분을 새로운 리스트로 추출할 수 있습니다.

예시 코드: 리스트 인덱싱 및 슬라이싱

numbers = [10, 20, 30, 40, 50, 60]
print("원본 리스트:", numbers)

# 인덱싱
print("numbers[0]:", numbers[0])    # 10
print("numbers[2]:", numbers[2])    # 30
print("numbers[-1]:", numbers[-1])   # 60 (마지막 요소)
print("numbers[-3]:", numbers[-3])   # 40

# 슬라이싱
print("numbers[1:4]:", numbers[1:4])  # [20, 30, 40] (인덱스 1, 2, 3)
print("numbers[:3]:", numbers[:3])   # [10, 20, 30] (처음부터 인덱스 2까지)
print("numbers[3:]:", numbers[3:])   # [40, 50, 60] (인덱스 3부터 끝까지)
print("numbers[::2]:", numbers[::2]) # [10, 30, 50] (처음부터 끝까지 2칸 간격으로)
print("numbers[::-1]:", numbers[::-1])# [60, 50, 40, 30, 20, 10] (리스트 뒤집기)

예상 결과:

원본 리스트: [10, 20, 30, 40, 50, 60]
numbers[0]: 10
numbers[2]: 30
numbers[-1]: 60
numbers[-3]: 40
numbers[1:4]: [20, 30, 40]
numbers[:3]: [10, 20, 30]
numbers[3:]: [40, 50, 60]
numbers[::2]: [10, 30, 50]
numbers[::-1]: [60, 50, 40, 30, 20, 10]

4.1.4. 리스트 주요 연산

연산설명예시 (L1=[1,2], L2=[3,4])결과
len(list)리스트의 길이(요소 개수) 반환len(L1)2
list1 + list2두 리스트 연결L1 + L2[1, 2, 3, 4]
list * n리스트를 n번 반복L1 * 3[1, 2, 1, 2, 1, 2]
item in list리스트에 item이 있으면 True2 in L1True
item not in list리스트에 item이 없으면 True5 not in L1True

4.1.5. 리스트 주요 메서드

리스트는 다양한 내장 메서드를 통해 요소를 추가, 삭제, 수정, 검색하는 등의 작업을 편리하게 수행할 수 있습니다.

얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy):
copy() 메서드나 슬라이싱(new_list = old_list[:])은 얕은 복사를 수행합니다. 이는 리스트 자체는 복사되지만, 리스트 내부의 요소가 또 다른 리스트와 같은 변경 가능한 객체일 경우, 해당 내부 객체는 참조만 복사됩니다. 따라서 복사본의 내부 객체를 변경하면 원본의 내부 객체도 함께 변경될 수 있습니다. 내부 객체까지 완전히 독립적으로 복사하려면 copy 모듈의 deepcopy() 함수를 사용해야 합니다.

예시 코드: 리스트 메서드 활용

my_list = [10, 20, 30]
print("초기 리스트:", my_list)

# 요소 추가
my_list.append(40)
print("append(40):", my_list) # [10, 20, 30, 40]
my_list.insert(1, 15) # 인덱스 1에 15 삽입
print("insert(1, 15):", my_list) # [10, 15, 20, 30, 40]
my_list.extend([50, 60])
print("extend([50, 60]):", my_list) # [10, 15, 20, 30, 40, 50, 60]

# 요소 삭제
my_list.remove(15)
print("remove(15):", my_list) # [10, 20, 30, 40, 50, 60]
popped_item = my_list.pop(2) # 인덱스 2 (값 30) 삭제 및 반환
print("pop(2) 후 리스트:", my_list, "삭제된 항목:", popped_item) # [10, 20, 40, 50, 60], 30
del my_list[0] # 인덱스 0 (값 10) 삭제
print("del my_list[0]:", my_list) # [20, 40, 50, 60]

# 요소 탐색
numbers = [1, 2, 3, 2, 4, 2]
print("numbers 리스트:", numbers)
print("2의 개수:", numbers.count(2)) # 3
print("3의 인덱스:", numbers.index(3)) # 2
# print("5의 인덱스:", numbers.index(5)) # ValueError 발생

# 정렬 및 순서 변경
unsorted_list = [3, 1, 4, 1, 5, 9, 2, 6]
print("원본:", unsorted_list)
unsorted_list.sort() # 제자리 정렬
print("sort() 후:", unsorted_list) # [1, 1, 2, 3, 4, 5, 6, 9]
unsorted_list.sort(reverse=True)
print("sort(reverse=True) 후:", unsorted_list) # [9, 6, 5, 4, 3, 2, 1, 1]
unsorted_list.reverse() # 제자리 반전
print("reverse() 후:", unsorted_list) # [1, 1, 2, 3, 4, 5, 6, 9] (다시 오름차순 정렬된 상태)

# 내장 함수 sorted()는 원본을 변경하지 않고 새 정렬된 리스트 반환
another_list = [50, 10, 30]
sorted_new_list = sorted(another_list)
print("원본 another_list:", another_list)
print("sorted(another_list):", sorted_new_list)

# 리스트 비우기
my_list.clear()
print("clear() 후:", my_list) # []

예상 결과:

초기 리스트: [10, 20, 30]
append(40): [10, 20, 30, 40]
insert(1, 15): [10, 15, 20, 30, 40]
extend([50, 60]): [10, 15, 20, 30, 40, 50, 60]
remove(15): [10, 20, 30, 40, 50, 60]
pop(2) 후 리스트: [10, 20, 40, 50, 60] 삭제된 항목: 30
del my_list[0]: [20, 40, 50, 60]
numbers 리스트: [1, 2, 3, 2, 4, 2]
2의 개수: 3
3의 인덱스: 2
원본: [3, 1, 4, 1, 5, 9, 2, 6]
sort() 후: [1, 1, 2, 3, 4, 5, 6, 9]
sort(reverse=True) 후: [9, 6, 5, 4, 3, 2, 1, 1]
reverse() 후: [1, 1, 2, 3, 4, 5, 6, 9]
원본 another_list: [50, 10, 30]
sorted(another_list): [10, 30, 50]
clear() 후: []

4.1.6. 리스트와 for

for 문을 사용하여 리스트의 각 요소를 순회할 수 있습니다. enumerate() 함수를 사용하면 인덱스와 값을 함께 가져올 수 있습니다.

예시 코드: 리스트와 for

items = ["펜", "노트", "지우개"]
for item in items:
    print(item)

print("\\n인덱스와 함께 출력 (enumerate):")
for index, item in enumerate(items):
    print(f"인덱스 {index}: {item}")

예상 결과:

펜
노트
지우개

인덱스와 함께 출력 (enumerate):
인덱스 0: 펜
인덱스 1: 노트
인덱스 2: 지우개

리스트 컴프리헨션은 리스트를 간결하게 생성하는 강력한 방법으로, 10장에서 자세히 다룹니다. 간단한 예시는 squares = [x**2 for x in range(10)]와 같습니다.

4.2. 튜플 (Tuple - tuple)

튜플은 리스트와 매우 유사하지만, 한 번 생성된 후에는 그 내용을 변경할 수 없는 '변경 불가능한(immutable)' 시퀀스 자료형입니다.

4.2.1. 튜플의 특징

4.2.2. 튜플 생성

튜플은 소괄호(())를 사용하여 생성하며, 각 요소는 쉼표(,)로 구분합니다. 소괄호는 생략 가능하지만, 명확성을 위해 사용하는 것이 좋습니다. 단일 요소를 가진 튜플을 생성할 때는 요소 뒤에 반드시 쉼표를 붙여야 합니다 (예: (item,)).

예시 코드: 튜플 생성

# 빈 튜플 생성
empty_tuple1 = ()
empty_tuple2 = tuple()
print("빈 튜플1:", empty_tuple1, type(empty_tuple1))

# 요소를 가진 튜플
my_tuple1 = (1, 2, "a", "b")
my_tuple2 = 10, 20, 30 # 괄호 생략 가능
print("튜플1:", my_tuple1)
print("튜플2:", my_tuple2, type(my_tuple2))

# 단일 요소 튜플 (쉼표 주의!)
single_item_tuple = (100,) # 쉼표가 없으면 그냥 정수 100으로 인식
not_a_tuple = (100)
print("단일 요소 튜플:", single_item_tuple, type(single_item_tuple))
print("이것은 튜플이 아님:", not_a_tuple, type(not_a_tuple))

# 문자열로부터 튜플 생성
char_tuple = tuple("hello")
print("문자열로부터 생성:", char_tuple)

예상 결과:

빈 튜플1: () <class 'tuple'>
튜플1: (1, 2, 'a', 'b')
튜플2: (10, 20, 30) <class 'tuple'>
단일 요소 튜플: (100,) <class 'tuple'>
이것은 튜플이 아님: 100 <class 'int'>
문자열로부터 생성: ('h', 'e', 'l', 'l', 'o')

4.2.3. 인덱싱과 슬라이싱

튜플의 인덱싱과 슬라이싱 방법은 리스트와 완전히 동일합니다.

4.2.4. 튜플 주요 연산

len(), + (연결), * (반복), in, not in 등 리스트와 유사한 연산을 지원합니다. 단, 튜플은 변경 불가능하므로 요소를 변경하는 연산은 불가능합니다.

4.2.5. 튜플 주요 메서드

튜플은 변경 불가능하므로 요소를 추가하거나 삭제하는 메서드는 없습니다. 주로 검색 관련 메서드만 제공합니다.

예시 코드: 튜플 메서드

point = (10, 20, 10, 30, 10)
print("튜플:", point)
print("10의 개수:", point.count(10))    # 3
print("20의 인덱스:", point.index(20))  # 1
# point.append(40) # AttributeError 발생 (튜플은 변경 불가)

예상 결과:

튜플: (10, 20, 10, 30, 10)
10의 개수: 3
20의 인덱스: 1

4.2.6. 튜플의 사용 사례

예시 코드: 튜플 패킹과 언패킹

# 패킹
coordinates = (10.5, 20.3, 5.0) # 또는 coordinates = 10.5, 20.3, 5.0

# 언패킹 (변수의 개수와 튜플 요소의 개수가 일치해야 함)
x, y, z = coordinates
print(f"x: {x}, y: {y}, z: {z}")

# 함수에서 여러 값 반환 (튜플로 반환됨)
def get_user_info():
    name = "Alice"
    age = 30
    return name, age # (name, age) 튜플 반환

user_name, user_age = get_user_info() # 언패킹
print(f"이름: {user_name}, 나이: {user_age}")

예상 결과:

x: 10.5, y: 20.3, z: 5.0
이름: Alice, 나이: 30

4.3. 딕셔너리 (Dictionary - dict)

딕셔너리는 '키(Key)'와 '값(Value)'을 하나의 쌍으로 묶어 저장하는 자료 구조입니다. 각 키는 고유해야 하며, 이 키를 통해 해당 값에 빠르게 접근할 수 있습니다. JSON이나 해시 테이블(Hash Table)과 유사한 구조입니다.

4.3.1. 딕셔너리의 특징

4.3.2. 딕셔너리 생성

딕셔너리는 중괄호({})를 사용하며, 각 키와 값은 콜론(:)으로 구분하고, 각 키-값 쌍은 쉼표(,)로 구분합니다. dict() 생성자를 사용하여 다양한 방식으로 딕셔너리를 만들 수도 있습니다.

예시 코드: 딕셔너리 생성

# 빈 딕셔너리 생성
empty_dict1 = {}
empty_dict2 = dict()
print("빈 딕셔너리1:", empty_dict1, type(empty_dict1))

# 키-값 쌍으로 생성
person = {
    "name": "홍길동",
    "age": 30,
    "city": "서울",
    "occupation": "개발자"
}
print("person 딕셔너리:", person)

# dict() 생성자 사용 (키워드 인자 방식 - 키는 문자열이어야 함)
student = dict(name="이몽룡", grade=2, major="컴퓨터공학")
print("student 딕셔너리:", student)

# dict() 생성자 사용 (튜플 리스트/리스트 튜플 방식)
items_info = dict([('apple', 1000), ('banana', 1500), ('orange', 1200)])
print("items_info 딕셔너리:", items_info)

예상 결과:

빈 딕셔너리1: {} <class 'dict'>
person 딕셔너리: {'name': '홍길동', 'age': 30, 'city': '서울', 'occupation': '개발자'}
student 딕셔너리: {'name': '이몽룡', 'grade': 2, 'major': '컴퓨터공학'}
items_info 딕셔너리: {'apple': 1000, 'banana': 1500, 'orange': 1200}

4.3.3. 요소 접근 및 수정

예시 코드: 딕셔너리 요소 접근 및 수정

car = {"brand": "Hyundai", "model": "Sonata", "year": 2023}
print("원본 딕셔너리:", car)

# 키로 값 접근
print("브랜드:", car["brand"]) # Hyundai
# print(car["color"]) # KeyError 발생 (존재하지 않는 키)

# get() 메서드로 접근
print("모델:", car.get("model")) # Sonata
print("색상 (get):", car.get("color")) # None (키가 없으므로 기본값 None)
print("색상 (get with default):", car.get("color", "정보 없음")) # 정보 없음

# 요소 수정
car["year"] = 2024
print("연도 수정 후:", car)

# 새 요소 추가
car["color"] = "Black"
car["owner"] = "Alice"
print("요소 추가 후:", car)

예상 결과:

원본 딕셔너리: {'brand': 'Hyundai', 'model': 'Sonata', 'year': 2023}
브랜드: Hyundai
모델: Sonata
색상 (get): None
색상 (get with default): 정보 없음
연도 수정 후: {'brand': 'Hyundai', 'model': 'Sonata', 'year': 2024}
요소 추가 후: {'brand': 'Hyundai', 'model': 'Sonata', 'year': 2024, 'color': 'Black', 'owner': 'Alice'}

4.3.4. 요소 삭제

4.3.5. 딕셔너리 주요 메서드

예시 코드: 딕셔너리 메서드 활용

scores = {"math": 90, "english": 85, "science": 92}
print("점수 딕셔너리:", scores)

# keys(), values(), items()
print("과목들 (keys):", list(scores.keys())) # 리스트로 변환하여 출력
print("점수들 (values):", list(scores.values()))
print("항목들 (items):", list(scores.items()))

# 요소 삭제 (pop, del)
english_score = scores.pop("english")
print("pop('english') 후:", scores, "삭제된 점수:", english_score)
del scores["math"]
print("del scores['math'] 후:", scores)

# update
new_scores = {"art": 88, "science": 95} # science 점수 덮어쓰기
scores.update(new_scores)
print("update 후:", scores)

# 길이와 멤버십 확인
print("항목 수:", len(scores))
print("'art' 과목이 있나요?:", "art" in scores) # True
print("'history' 과목이 있나요?:", "history" in scores) # False

# for 문과 items() 활용
print("\\n과목별 점수 출력:")
for subject, score in scores.items():
    print(f"{subject}: {score}")

예상 결과:

점수 딕셔너리: {'math': 90, 'english': 85, 'science': 92}
과목들 (keys): ['math', 'english', 'science']
점수들 (values): [90, 85, 92]
항목들 (items): [('math', 90), ('english', 85), ('science', 92)]
pop('english') 후: {'math': 90, 'science': 92} 삭제된 점수: 85
del scores['math'] 후: {'science': 92}
update 후: {'science': 95, 'art': 88}
항목 수: 2
'art' 과목이 있나요?: True
'history' 과목이 있나요?: False

과목별 점수 출력:
science: 95
art: 88

딕셔너리 컴프리헨션은 딕셔너리를 간결하게 생성하는 방법으로, 10장에서 자세히 다룹니다. 예: squares_dict = {x: x*x for x in range(5)}.

4.4. 세트 (Set - set)

세트는 수학의 집합과 유사한 개념으로, 순서가 없고 중복된 요소를 허용하지 않는 컬렉션입니다. 주로 멤버십 테스트(특정 요소의 존재 여부 확인)나 중복 제거, 집합 연산(합집합, 교집합 등)에 사용됩니다.

4.4.1. 세트의 특징

4.4.2. 세트 생성

세트는 중괄호({})를 사용하여 생성하거나 set() 생성자를 사용합니다. 중괄호 안에 쉼표로 구분된 요소들을 나열합니다. 주의: 빈 중괄호 {}는 빈 딕셔너리를 의미하므로, 빈 세트를 만들 때는 반드시 set()을 사용해야 합니다.

예시 코드: 세트 생성

# 빈 세트 생성 (set() 사용 필수!)
empty_set = set()
print("빈 세트:", empty_set, type(empty_set))
# empty_dict = {} # 이것은 빈 딕셔너리

# 중괄호로 세트 생성
my_set1 = {1, 2, 3, 2, 1} # 중복 요소는 자동으로 제거됨
print("세트1:", my_set1) # {1, 2, 3} (순서는 다를 수 있음)

# set() 생성자로 리스트나 튜플, 문자열로부터 세트 생성 (중복 제거)
list_data = [1, 2, "hello", "world", "hello"]
set_from_list = set(list_data)
print("리스트로부터 생성된 세트:", set_from_list)

string_data = "programming"
set_from_string = set(string_data)
print("문자열로부터 생성된 세트:", set_from_string) # 순서는 보장되지 않음

예상 결과 (세트 요소의 출력 순서는 다를 수 있음):

빈 세트: set() <class 'set'>
세트1: {1, 2, 3}
리스트로부터 생성된 세트: {1, 2, 'world', 'hello'}
문자열로부터 생성된 세트: {'r', 'o', 'a', 'm', 'i', 'n', 'p', 'g'}

4.4.3. 세트 주요 연산 (집합 연산)

세트는 다양한 집합 연산을 지원합니다.

연산연산자메서드설명
합집합s1 | s2s1.union(s2)두 세트의 모든 요소를 포함하는 새 세트
교집합s1 & s2s1.intersection(s2)두 세트에 공통으로 존재하는 요소만 포함하는 새 세트
차집합s1 - s2s1.difference(s2)s1에는 있지만 s2에는 없는 요소만 포함하는 새 세트
대칭 차집합s1 ^ s2s1.symmetric_difference(s2)두 세트 중 한쪽에만 속하는 요소들을 포함하는 새 세트 (합집합 - 교집합)
부분집합 확인s1 <= s2s1.issubset(s2)s1이 s2의 부분집합이면 True
진부분집합 확인s1 < s2s1이 s2의 진부분집합(같지 않은 부분집합)이면 True
상위집합 확인s1 >= s2s1.issuperset(s2)s1이 s2의 상위집합이면 True
진상위집합 확인s1 > s2s1이 s2의 진상위집합(같지 않은 상위집합)이면 True

4.4.4. 세트 주요 메서드

예시 코드: 세트 연산 및 메서드

set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
print("set_a:", set_a)
print("set_b:", set_b)

# 집합 연산
print("합집합 (union):", set_a | set_b) # {1, 2, 3, 4, 5, 6, 7, 8}
print("교집합 (intersection):", set_a & set_b) # {4, 5}
print("차집합 (difference a - b):", set_a - set_b) # {1, 2, 3}
print("대칭 차집합 (symmetric_difference):", set_a ^ set_b) # {1, 2, 3, 6, 7, 8}

# 요소 추가
set_c = {10, 20}
set_c.add(30)
print("add(30) 후 set_c:", set_c) # {10, 20, 30}
set_c.update([30, 40, 50]) # 중복 30은 무시, 40, 50 추가
print("update([30, 40, 50]) 후 set_c:", set_c) # {10, 20, 30, 40, 50}

# 요소 삭제
set_c.remove(20) # 20 삭제
print("remove(20) 후 set_c:", set_c) # {10, 30, 40, 50}
# set_c.remove(100) # KeyError 발생
set_c.discard(100) # 100이 없지만 에러 없음
print("discard(100) 후 set_c:", set_c)
popped_element = set_c.pop() # 임의의 요소 삭제 및 반환
print("pop() 후 set_c:", set_c, "삭제된 요소:", popped_element)

# 중복 제거 활용
my_list_with_duplicates = [1,1,2,2,3,4,5,5,5]
unique_elements_set = set(my_list_with_duplicates)
print("리스트 중복 제거 결과 (세트):", unique_elements_set)
unique_elements_list = list(unique_elements_set) # 다시 리스트로 변환 (순서 보장 안됨)
print("중복 제거 후 리스트:", unique_elements_list)

예상 결과 (세트 요소의 출력 순서는 다를 수 있음):

set_a: {1, 2, 3, 4, 5}
set_b: {4, 5, 6, 7, 8}
합집합 (union): {1, 2, 3, 4, 5, 6, 7, 8}
교집합 (intersection): {4, 5}
차집합 (difference a - b): {1, 2, 3}
대칭 차집합 (symmetric_difference): {1, 2, 3, 6, 7, 8}
add(30) 후 set_c: {10, 20, 30}
update([30, 40, 50]) 후 set_c: {40, 10, 50, 20, 30}
remove(20) 후 set_c: {40, 10, 50, 30}
discard(100) 후 set_c: {40, 10, 50, 30}
pop() 후 set_c: {10, 50, 30} 삭제된 요소: 40 (또는 다른 요소일 수 있음)
리스트 중복 제거 결과 (세트): {1, 2, 3, 4, 5}
중복 제거 후 리스트: [1, 2, 3, 4, 5] (순서는 다를 수 있음)

세트 컴프리헨션은 세트를 간결하게 생성하는 방법으로, 10장에서 자세히 다룹니다. 예: unique_squares = {x*x for x in range(-5, 6)}.

4.4.5. frozenset

frozenset은 변경 불가능한(immutable) 버전의 세트입니다. 생성된 후에는 요소를 추가하거나 삭제할 수 없습니다. 변경 불가능하기 때문에 딕셔너리의 키나 다른 세트의 요소가 될 수 있다는 장점이 있습니다.

예시 코드: frozenset

frozen = frozenset([1, 2, 3, 2]) # 생성 시 중복 제거
print("frozenset:", frozen)
# frozen.add(4) # AttributeError 발생

# 딕셔너리의 키로 사용 가능
key_as_frozenset = frozenset({'a', 'b'})
my_dict_with_set_key = {key_as_frozenset: "some value"}
print("딕셔너리 키로 사용된 frozenset:", my_dict_with_set_key)

예상 결과:

frozenset: frozenset({1, 2, 3})
딕셔너리 키로 사용된 frozenset: {frozenset({'a', 'b'}): 'some value'}

이번 장에서는 파이썬의 핵심적인 내장 자료 구조인 리스트, 튜플, 딕셔너리, 세트에 대해 알아보았습니다. 각 자료 구조는 고유한 특징과 용도를 가지므로, 해결하려는 문제의 성격에 맞춰 적절한 자료 구조를 선택하는 것이 중요합니다. 이러한 자료 구조들을 능숙하게 다루는 것은 효율적이고 가독성 높은 파이썬 코드를 작성하는 데 있어 필수적인 능력입니다. 다음 장에서는 코드의 재사용성을 높이고 프로그램을 더 모듈화할 수 있는 함수에 대해 배우겠습니다.

제 5 장: 함수

프로그램을 작성하다 보면 동일하거나 유사한 작업을 여러 곳에서 반복해야 하는 경우가 많습니다. 함수(Function)는 이렇게 반복적으로 사용되는 코드 묶음에 이름을 붙여, 필요할 때마다 호출하여 사용할 수 있도록 만든 코드의 단위입니다. 함수를 사용하면 코드의 중복을 줄여 재사용성을 높이고, 프로그램을 기능별로 모듈화하여 전체 구조를 더 명확하게 만들 수 있습니다. 또한, 복잡한 작업을 작은 단위의 함수로 나누어 구현함으로써 문제 해결 과정을 단순화하고 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.

파이썬에는 이미 다양한 기능을 제공하는 내장 함수(Built-in functions)들(예: print(), len(), type(), range(), int() 등)이 있으며, 필요에 따라 사용자가 직접 함수를 정의하여 사용할 수도 있습니다. 이번 장에서는 사용자 정의 함수를 만드는 방법과 함수의 다양한 활용법에 대해 자세히 알아보겠습니다.

5.1. 함수 정의하기 (Defining a Function)

파이썬에서 함수를 정의할 때는 def 키워드를 사용합니다. 기본적인 함수 정의 구조는 다음과 같습니다:

def 함수이름(매개변수1, 매개변수2, ...):
    """
    이 함수는 이런저런 기능을 수행합니다. (선택 사항: 독스트링)
    매개변수1: 첫 번째 매개변수에 대한 설명
    매개변수2: 두 번째 매개변수에 대한 설명
    반환 값: 함수의 반환 값에 대한 설명
    """
    # 함수가 수행할 코드 블록 (반드시 들여쓰기)
    명령문1
    명령문2
    ...
    return 반환_값 # 선택 사항

예시 코드: 간단한 함수 정의

# 매개변수와 반환 값이 없는 함수
def greet():
    """간단한 인사 메시지를 출력하는 함수입니다."""
    message = "안녕하세요, 파이썬 함수입니다!"
    print(message)

# 매개변수가 있고 반환 값이 있는 함수
def add(a, b):
    """두 숫자를 더한 결과를 반환하는 함수입니다.
    
    Args:
        a (int or float): 첫 번째 숫자.
        b (int or float): 두 번째 숫자.
        
    Returns:
        int or float: a와 b를 더한 값.
    """
    result = a + b
    return result

5.2. 함수 호출하기 (Calling a Function)

정의된 함수를 사용하려면 함수 이름 뒤에 괄호 ()를 붙여 호출합니다. 함수가 매개변수를 가지고 있다면, 호출 시 괄호 안에 해당 매개변수에 전달할 인자(Argument)를 제공해야 합니다.

예시 코드: 함수 호출

# greet 함수 호출
greet() # 출력: 안녕하세요, 파이썬 함수입니다!

# add 함수 호출 및 결과 사용
sum_result = add(10, 20)
print(f"10 + 20 = {sum_result}") # 출력: 10 + 20 = 30

another_sum = add(3.14, 2.71)
print(f"3.14 + 2.71 = {another_sum}")

# 독스트링 확인
help(add)
# 또는 print(add.__doc__)

예상 결과 (help(add)는 더 긴 설명 출력):

안녕하세요, 파이썬 함수입니다!
10 + 20 = 30
3.14 + 2.71 = 5.85
Help on function add in module __main__:

add(a, b)
    두 숫자를 더한 결과를 반환하는 함수입니다.
    
    Args:
        a (int or float): 첫 번째 숫자.
        b (int or float): 두 번째 숫자.
        
    Returns:
        int or float: a와 b를 더한 값.

5.3. 매개변수와 인자 (Parameters and Arguments)

함수를 정의할 때 사용하는 변수를 매개변수(parameter)라고 하며, 함수를 호출할 때 전달하는 실제 값을 인자(argument)라고 합니다. 파이썬은 다양한 방식으로 인자를 전달할 수 있습니다.

5.3.1. 위치 인자 (Positional Arguments)

가장 기본적인 방식으로, 함수에 정의된 매개변수 순서대로 인자를 전달합니다.

예시 코드: 위치 인자

def describe_pet(animal_type, pet_name):
    """애완동물의 종류와 이름을 출력합니다."""
    print(f"저의 애완동물은 {animal_type}이고, 이름은 {pet_name}입니다.")

describe_pet("강아지", "뽀삐") # animal_type="강아지", pet_name="뽀삐"
describe_pet("해피", "고양이") # 의도와 다르게 animal_type="해피", pet_name="고양이"

예상 결과:

저의 애완동물은 강아지이고, 이름은 뽀삐입니다.
저의 애완동물은 해피이고, 이름은 고양이입니다.

5.3.2. 키워드 인자 (Keyword Arguments)

매개변수이름=값 형태로 인자를 전달하며, 이 경우 인자의 순서는 중요하지 않습니다. 위치 인자 뒤에 와야 합니다.

예시 코드: 키워드 인자

def describe_pet(animal_type, pet_name):
    """애완동물의 종류와 이름을 출력합니다."""
    print(f"저의 애완동물은 {animal_type}이고, 이름은 {pet_name}입니다.")

describe_pet(animal_type="고양이", pet_name="나비")
describe_pet(pet_name="야옹이", animal_type="고양이") # 순서 변경 가능
# describe_pet(pet_name="점박이", "개") # SyntaxError: positional argument follows keyword argument

예상 결과:

저의 애완동물은 고양이이고, 이름은 나비입니다.
저의 애완동물은 고양이이고, 이름은 야옹이입니다.

5.3.3. 기본 매개변수 값 (Default Parameter Values)

함수를 정의할 때 매개변수에 기본값을 할당할 수 있습니다. 이렇게 하면 함수 호출 시 해당 인자를 생략할 수 있으며, 생략 시 기본값이 사용됩니다. 기본값이 있는 매개변수는 기본값이 없는 매개변수들 뒤에 위치해야 합니다.

주의: 기본값으로 리스트나 딕셔너리 같은 변경 가능한(mutable) 객체를 사용할 경우, 함수가 여러 번 호출될 때 동일한 객체가 계속 공유되므로 예기치 않은 동작을 유발할 수 있습니다. 이 경우 함수 내부에서 None을 기본값으로 하고 필요시 새 객체를 생성하는 것이 안전합니다.

예시 코드: 기본 매개변수 값

def greet_user(username, greeting="안녕하세요"): # greeting의 기본값은 "안녕하세요"
    """사용자에게 인사합니다."""
    print(f"{greeting}, {username}님!")

greet_user("홍길동")                 # greeting은 기본값 사용
greet_user("Alice", "Hello")       # greeting에 "Hello" 전달
# greet_user(greeting="반갑습니다") # TypeError: username 인자 누락

# 변경 가능한 객체를 기본값으로 사용할 때의 주의점 (나쁜 예)
def add_to_list_bad(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_to_list_bad(1)) # [1]
print(add_to_list_bad(2)) # [1, 2] (이전 호출의 my_list가 공유됨!)

# 변경 가능한 객체 기본값의 좋은 예
def add_to_list_good(item, my_list=None):
    if my_list is None:
        my_list = [] # 호출 시마다 새 리스트 생성
    my_list.append(item)
    return my_list

print(add_to_list_good(1)) # [1]
print(add_to_list_good(2)) # [2] (독립적인 리스트)
print(add_to_list_good(3, [10, 20])) # [10, 20, 3] (기존 리스트에 추가)

예상 결과:

안녕하세요, 홍길동님!
Hello, Alice님!
[1]
[1, 2]
[1]
[2]
[10, 20, 3]

5.3.4. 가변 인자 리스트 (Arbitrary Argument Lists)

함수가 임의의 개수의 인자를 받을 수 있도록 정의할 수 있습니다.

함수 정의 시 매개변수 순서는 일반 매개변수 -> *args -> **kwargs 순서여야 합니다.

예시 코드: *args**kwargs

def print_all_args(*args):
    """전달된 모든 위치 인자를 출력합니다."""
    print("전달된 위치 인자들 (*args):", args)
    for arg in args:
        print(arg)

print_all_args(1, "Python", True, 3.14)

def print_all_kwargs(**kwargs):
    """전달된 모든 키워드 인자를 출력합니다."""
    print("전달된 키워드 인자들 (**kwargs):", kwargs)
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_all_kwargs(name="Alice", age=30, city="New York")

# *args와 **kwargs 함께 사용
def process_data(id_num, *scores, **info):
    print(f"ID: {id_num}")
    print(f"Scores (*args): {scores}")
    avg_score = sum(scores) / len(scores) if scores else 0
    print(f"Average Score: {avg_score:.2f}")
    print(f"Info (**kwargs): {info}")
    if 'email' in info:
        print(f"Email: {info['email']}")

process_data(101, 90, 85, 92, name="Bob", subject="Math", email="bob@example.com")
process_data(102, status="pending") # scores가 비어있는 경우

예상 결과:

전달된 위치 인자들 (*args): (1, 'Python', True, 3.14)
1
Python
True
3.14
전달된 키워드 인자들 (**kwargs): {'name': 'Alice', 'age': 30, 'city': 'New York'}
name: Alice
age: 30
city: New York
ID: 101
Scores (*args): (90, 85, 92)
Average Score: 89.00
Info (**kwargs): {'name': 'Bob', 'subject': 'Math', 'email': 'bob@example.com'}
Email: bob@example.com
ID: 102
Scores (*args): ()
Average Score: 0.00
Info (**kwargs): {'status': 'pending'}

5.3.5. 인자 언패킹 (Argument Unpacking)

리스트나 튜플, 딕셔너리 등의 컬렉션에 저장된 값들을 풀어서(unpacking) 함수의 인자로 전달할 수 있습니다.

예시 코드: 인자 언패킹

def calculate_sum_three(a, b, c):
    return a + b + c

numbers_list = [10, 20, 30]
result1 = calculate_sum_three(*numbers_list) # 10, 20, 30이 a, b, c에 각각 전달
print(f"*numbers_list 결과: {result1}")

numbers_tuple = (5, 10, 15)
result2 = calculate_sum_three(*numbers_tuple)
print(f"*numbers_tuple 결과: {result2}")

def print_info(name, age, city="N/A"):
    print(f"이름: {name}, 나이: {age}, 도시: {city}")

info_dict = {"name": "David", "age": 25}
print_info(**info_dict) # name="David", age=25 전달

info_dict_with_city = {"name": "Eve", "age": 28, "city": "London"}
print_info(**info_dict_with_city)

예상 결과:

*numbers_list 결과: 60
*numbers_tuple 결과: 30
이름: David, 나이: 25, 도시: N/A
이름: Eve, 나이: 28, 도시: London

5.4. 반환 값 (Return Values)

함수는 return 문을 사용하여 실행 결과를 호출한 곳으로 되돌려줄 수 있습니다. return 문을 만나면 함수는 즉시 실행을 종료하고 지정된 값을 반환합니다.

예시 코드: 반환 값

def get_square(number):
    return number * number

square_of_5 = get_square(5)
print(f"5의 제곱: {square_of_5}")

def get_name_and_age():
    name = "Charles"
    age = 40
    return name, age # (name, age) 튜플 반환

user_info = get_name_and_age() # user_info는 ('Charles', 40) 튜플
print(f"사용자 정보 (튜플): {user_info}")
user_name, user_age = get_name_and_age() # 튜플 언패킹
print(f"이름: {user_name}, 나이: {user_age}")

def no_return_function():
    print("이 함수는 반환 값이 없습니다.")
    # return 이 없으므로 None 반환

result_none = no_return_function()
print(f"반환 값이 없는 함수의 결과: {result_none}") # None

예상 결과:

5의 제곱: 25
사용자 정보 (튜플): ('Charles', 40)
이름: Charles, 나이: 40
이 함수는 반환 값이 없습니다.
반환 값이 없는 함수의 결과: None

5.5. 변수의 유효 범위 (Scope of Variables)

스코프(Scope)는 변수가 유효한, 즉 접근 가능한 코드의 범위를 의미합니다. 파이썬에서 변수는 자신이 정의된 위치에 따라 유효 범위가 결정됩니다.

파이썬은 변수의 이름을 찾을 때 다음과 같은 순서(LEGB 규칙)로 검색합니다: Local (지역) -> Enclosing function locals (둘러싸는 함수의 지역) -> Global (전역) -> Built-in (내장).

global 키워드

함수 내부에서 전역 변수의 값을 직접 수정하려면, 해당 변수 이름 앞에 global 키워드를 사용하여 선언해야 합니다. 그렇지 않으면, 함수 내에서 같은 이름으로 변수에 값을 할당할 때 새로운 지역 변수가 생성되는 것으로 간주됩니다.

예시 코드: 변수 스코프와 global 키워드

global_var = 100 # 전역 변수

def scope_test_local():
    local_var = 10 # 지역 변수
    print(f"함수 내 지역 변수 local_var: {local_var}")
    print(f"함수 내에서 전역 변수 global_var 접근: {global_var}")
    # global_var = 200 # 여기서 이렇게 하면 새로운 지역 변수 global_var가 생길 수 있음 (권장하지 않음)

def scope_test_modify_global():
    global global_var # 전역 변수 global_var를 사용하겠다고 선언
    global_var = 200  # 이제 전역 변수의 값이 변경됨
    print(f"함수 내에서 수정한 global_var: {global_var}")

# 지역 변수 접근 시도 (오류 발생)
# print(local_var) # NameError: name 'local_var' is not defined

scope_test_local()
print(f"함수 호출 후 전역 변수 global_var (변경 전): {global_var}") # 아직 100

scope_test_modify_global()
print(f"전역 변수 수정 함수 호출 후 global_var: {global_var}") # 200으로 변경됨

# 전역 변수와 같은 이름의 지역 변수
x = "전역 x"
def my_func_scope():
    x = "지역 x" # 이 x는 전역 x와 다른 지역 변수
    print(f"함수 내 x: {x}")
my_func_scope()
print(f"함수 밖 x: {x}") # "전역 x"

예상 결과:

함수 내 지역 변수 local_var: 10
함수 내에서 전역 변수 global_var 접근: 100
함수 호출 후 전역 변수 global_var (변경 전): 100
함수 내에서 수정한 global_var: 200
전역 변수 수정 함수 호출 후 global_var: 200
함수 내 x: 지역 x
함수 밖 x: 전역 x

참고: 중첩 함수(함수 안에 정의된 함수) 환경에서는 nonlocal 키워드를 사용하여 바로 바깥쪽 함수의 지역 변수를 수정할 수 있습니다. 이는 더 고급 주제로, 여기서는 기본적인 지역/전역 스코프에 집중합니다.

5.6. 람다 함수 (Lambda Functions)

람다 함수(Lambda function)는 이름이 없는 한 줄짜리 간결한 함수로, '익명 함수(anonymous function)'라고도 불립니다. 주로 간단한 기능을 수행하거나, 다른 함수의 인자로 함수 객체를 전달해야 할 때 유용하게 사용됩니다.

람다 함수의 기본 구조는 다음과 같습니다:

lambda 매개변수들: 표현식

람다 함수는 표현식만 포함할 수 있으므로, 여러 줄의 명령문이나 복잡한 로직을 담을 수 없습니다.

예시 코드: 람다 함수

# 일반 함수로 제곱 계산
def square_normal(x):
    return x * x

# 람다 함수로 제곱 계산
square_lambda = lambda x: x * x

print(f"일반 함수로 5의 제곱: {square_normal(5)}")
print(f"람다 함수로 5의 제곱: {square_lambda(5)}")

# 두 수의 합을 계산하는 람다 함수
add_lambda = lambda a, b: a + b
print(f"람다 함수로 10 + 20 = {add_lambda(10, 20)}")

# 람다 함수를 다른 함수의 인자로 사용 (예: map, filter, sorted)
numbers = [1, 2, 3, 4, 5]

# map()과 람다: 리스트의 각 요소에 함수 적용
squared_numbers = list(map(lambda x: x * x, numbers))
print(f"map과 람다로 제곱된 리스트: {squared_numbers}")

# filter()와 람다: 리스트에서 조건에 맞는 요소만 필터링
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(f"filter와 람다로 필터링된 짝수 리스트: {even_numbers}")

# sorted()와 람다: 복잡한 객체 리스트를 특정 키 기준으로 정렬
points = [(1, 5), (3, 2), (5, 8), (2, 1)]
# y 좌표 기준으로 정렬
sorted_points_by_y = sorted(points, key=lambda point: point[1])
print(f"y좌표 기준 정렬된 점들: {sorted_points_by_y}")

예상 결과:

일반 함수로 5의 제곱: 25
람다 함수로 5의 제곱: 25
람다 함수로 10 + 20 = 30
map과 람다로 제곱된 리스트: [1, 4, 9, 16, 25]
filter와 람다로 필터링된 짝수 리스트: [2, 4]
y좌표 기준 정렬된 점들: [(2, 1), (3, 2), (1, 5), (5, 8)]

5.7. 함수 작성 시 좋은 습관

이번 장에서는 파이썬에서 함수를 정의하고 사용하는 다양한 방법을 배웠습니다. 함수는 프로그래밍의 핵심 구성 요소로, 코드를 효과적으로 구성하고 재사용하는 데 필수적입니다. 매개변수 전달 방식, 반환 값 처리, 변수의 유효 범위, 그리고 간결한 람다 함수의 개념을 이해함으로써 더욱 강력하고 유연한 프로그램을 작성할 수 있게 될 것입니다. 다음 장에서는 객체 지향 프로그래밍의 기초에 대해 알아보겠습니다.

제 6 장: 객체 지향 프로그래밍 (OOP) 기초

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 현대 소프트웨어 개발에서 널리 사용되는 프로그래밍 패러다임 중 하나입니다. OOP는 현실 세계의 사물이나 개념을 '객체(object)'로 모델링하고, 이러한 객체들의 상호작용을 통해 프로그램을 구성하는 방식입니다. 파이썬은 완벽한 객체 지향 언어로, OOP의 모든 핵심 기능을 지원합니다.

OOP를 사용하면 프로그램을 더 모듈화하고, 코드의 재사용성을 높이며, 대규모 프로젝트를 더 쉽게 관리하고 유지보수할 수 있다는 장점이 있습니다. 이번 장에서는 OOP의 기본이 되는 클래스(class)와 객체(object), 그리고 OOP의 주요 특징인 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism)에 대해 알아보겠습니다.

6.1. 클래스(Class)와 객체(Object)

OOP의 가장 기본적인 구성 요소는 클래스와 객체입니다.

6.1.1. 클래스 (Class)

클래스는 특정 종류의 객체를 만들기 위한 '설계도' 또는 '틀(template)'이라고 할 수 있습니다. 클래스는 객체가 가지게 될 데이터(속성, attribute)와 객체가 수행할 수 있는 동작(메서드, method)을 정의합니다.

예를 들어, '자동차'라는 클래스를 정의한다면, 이 클래스에는 '색상', '모델명', '최고 속도'와 같은 속성들과 '전진하다', '후진하다', '경적을 울리다'와 같은 메서드들이 포함될 수 있습니다.

파이썬에서 클래스는 class 키워드를 사용하여 정의하며, 클래스 이름은 관례적으로 각 단어의 첫 글자를 대문자로 쓰는 캐피털라이즈드 워드(CapitalizedWords, 또는 파스칼 케이스 PascalCase) 방식을 따릅니다. (예: Car, StudentProfile)

class 클래스이름:
    # 클래스 속성 (선택 사항)
    # 메서드 정의 (생성자 포함)
    pass # 아무 내용이 없을 경우 pass 사용

6.1.2. 객체 (Object) / 인스턴스 (Instance)

객체는 클래스라는 설계도를 바탕으로 실제로 메모리에 생성된 '실체'입니다. 클래스로부터 객체를 만드는 과정을 '인스턴스화(instantiation)'라고 하며, 이렇게 만들어진 객체를 해당 클래스의 '인스턴스(instance)'라고 부릅니다.

예를 들어, '자동차' 클래스로부터 '빨간색 소나타', '파란색 K5'와 같은 구체적인 자동차 객체들을 만들 수 있습니다. 이 객체들은 모두 '자동차' 클래스의 인스턴스이며, 각각 고유한 속성 값(예: 색상)을 가질 수 있지만, 클래스에 정의된 동일한 메서드(예: 전진하다)를 공유합니다.

객체는 클래스 이름을 함수처럼 호출하여 생성합니다:

객체변수 = 클래스이름()

6.2. 클래스 정의하고 사용하기

6.2.1. __init__ 메서드 (생성자 - Constructor)

__init__ (양쪽에 밑줄 두 개) 메서드는 클래스의 인스턴스가 생성될 때 자동으로 호출되는 특별한 메서드로, '생성자(constructor)'라고도 불립니다. 이 메서드의 주된 역할은 인스턴스가 처음 생성될 때 필요한 초기 설정을 하거나 인스턴스 속성(instance attribute)의 초기값을 할당하는 것입니다.

__init__ 메서드의 첫 번째 매개변수는 항상 self여야 하며, self는 생성되는 인스턴스 자기 자신을 가리킵니다. self 뒤에는 인스턴스 생성 시 전달받을 다른 매개변수들을 정의할 수 있습니다.

6.2.2. 인스턴스 속성 (Instance Attributes)

인스턴스 속성은 각 인스턴스가 개별적으로 가지는 데이터입니다. 보통 __init__ 메서드 내에서 self.속성이름 = 값 형태로 정의하고 초기화합니다.

6.2.3. 인스턴스 메서드 (Instance Methods)

인스턴스 메서드는 클래스 내에 정의된 함수로, 해당 클래스의 인스턴스를 통해서만 호출할 수 있습니다. 인스턴스 메서드의 첫 번째 매개변수도 항상 self이며, 이를 통해 인스턴스의 속성에 접근하거나 다른 메서드를 호출할 수 있습니다.

6.2.4. self 키워드

self는 클래스의 인스턴스 메서드에서 첫 번째 매개변수로 사용되며, 메서드가 호출된 인스턴스 자신을 가리킵니다. self를 사용하여 해당 인스턴스의 속성이나 다른 메서드에 접근할 수 있습니다. (이름이 반드시 self일 필요는 없지만, 파이썬 커뮤니티의 강력한 관례입니다.)

예시 코드: 간단한 Person 클래스 정의 및 사용

class Person:
    # 클래스 속성 (모든 인스턴스가 공유)
    species = "Homo sapiens"

    def __init__(self, name, age):
        """인스턴스 생성 시 호출되는 생성자 메서드."""
        # 인스턴스 속성 초기화
        self.name = name
        self.age = age
        print(f"{self.name} ({self.species}) 객체가 생성되었습니다.")

    def introduce(self):
        """자신을 소개하는 인스턴스 메서드."""
        print(f"안녕하세요, 저는 {self.name}이고, {self.age}살입니다.")

    def get_older(self, years=1):
        """나이를 먹는 메서드."""
        self.age += years
        print(f"{self.name}님이 {years}살 더 나이를 먹어 {self.age}살이 되었습니다.")

# Person 클래스의 인스턴스 생성
person1 = Person("홍길동", 30) # __init__ 메서드가 호출됨
person2 = Person("이영희", 25)

# 인스턴스 속성 접근
print(f"{person1.name}의 종: {person1.species}") # 클래스 속성도 인스턴스를 통해 접근 가능
print(f"{person2.name}의 나이: {person2.age}")

# 인스턴스 메서드 호출
person1.introduce()
person2.introduce()

person1.get_older()
person2.get_older(3)

예상 결과:

홍길동 (Homo sapiens) 객체가 생성되었습니다.
이영희 (Homo sapiens) 객체가 생성되었습니다.
홍길동의 종: Homo sapiens
이영희의 나이: 25
안녕하세요, 저는 홍길동이고, 30살입니다.
안녕하세요, 저는 이영희이고, 25살입니다.
홍길동님이 1살 더 나이를 먹어 31살이 되었습니다.
이영희님이 3살 더 나이를 먹어 28살이 되었습니다.

6.3. 객체 지향 프로그래밍의 주요 특징

OOP는 캡슐화, 상속, 다형성이라는 세 가지 주요 특징을 통해 강력한 기능을 제공합니다.

6.3.1. 캡슐화 (Encapsulation)

캡슐화는 관련된 데이터(속성)와 해당 데이터를 처리하는 함수(메서드)를 하나의 '캡슐' 즉, 클래스로 묶는 것을 의미합니다. 이를 통해 객체 내부의 복잡한 구현 세부 사항을 외부로부터 숨기고(정보 은닉, information hiding), 객체 외부에서는 잘 정의된 인터페이스(메서드)만을 통해 객체의 데이터에 접근하고 조작하도록 유도합니다.

파이썬에서는 엄격한 의미의 private 속성/메서드를 제공하지는 않지만, 다음과 같은 명명 규칙을 통해 캡슐화를 지원합니다:

캡슐화를 통해 객체의 데이터 무결성을 보호하고, 내부 구현 변경이 외부에 미치는 영향을 최소화하여 코드의 유지보수성을 높일 수 있습니다.

예시 코드: 캡슐화 (이름 장식 예시)

class BankAccount:
    def __init__(self, initial_balance):
        self.__balance = initial_balance # 더블 밑줄로 시작하는 "private" 속성 (이름 장식됨)

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"{amount}원이 입금되었습니다. 현재 잔액: {self.__balance}원")
        else:
            print("입금액은 0보다 커야 합니다.")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"{amount}원이 출금되었습니다. 현재 잔액: {self.__balance}원")
        else:
            print("출금액이 유효하지 않거나 잔액이 부족합니다.")

    def get_balance(self): # 외부에서 잔액을 확인할 수 있는 메서드 제공
        return self.__balance

account = BankAccount(10000)
account.deposit(5000)
account.withdraw(2000)
print(f"최종 잔액 조회: {account.get_balance()}원")

# print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance' (직접 접근 불가)
# 하지만 이름 장식된 이름으로는 접근 가능 (권장하지 않음)
# print(account._BankAccount__balance) 

예상 결과:

5000원이 입금되었습니다. 현재 잔액: 15000원
2000원이 출금되었습니다. 현재 잔액: 13000원
최종 잔액 조회: 13000원

6.3.2. 상속 (Inheritance)

상속은 기존 클래스(부모 클래스, Parent Class 또는 Base Class)의 속성과 메서드를 물려받아 새로운 클래스(자식 클래스, Child Class 또는 Derived Class)를 정의하는 기능입니다. 이를 통해 코드의 중복을 줄이고, 클래스 간의 계층적 관계("is-a" 관계, 예: '개는 동물이다')를 표현할 수 있습니다.

자식 클래스는 부모 클래스의 기능을 그대로 사용하거나, 필요에 따라 새로운 기능을 추가하거나 기존 기능을 재정의(오버라이딩, overriding)할 수 있습니다.

파이썬에서 상속은 클래스 정의 시 괄호 안에 부모 클래스 이름을 명시하여 사용합니다:

class 부모클래스:
    # 부모 클래스의 속성과 메서드
    pass

class 자식클래스(부모클래스):
    # 자식 클래스만의 속성과 메서드 추가
    # 또는 부모 클래스의 메서드 재정의
    pass
메서드 오버라이딩 (Method Overriding)

자식 클래스에서 부모 클래스와 동일한 이름의 메서드를 다시 정의하는 것을 메서드 오버라이딩이라고 합니다. 이렇게 하면 자식 클래스의 인스턴스가 해당 메서드를 호출할 때 자식 클래스에 정의된 버전이 실행됩니다.

super() 함수

자식 클래스에서 부모 클래스의 메서드(특히 __init__)를 호출하고 싶을 때 super() 함수를 사용합니다. super()는 부모 클래스의 인스턴스를 반환하는 것처럼 동작하여, 이를 통해 부모 클래스의 메서드에 접근할 수 있습니다.

예시 코드: 상속과 메서드 오버라이딩, super()

class Animal: # 부모 클래스
    def __init__(self, name):
        self.name = name
        print(f"{self.name} (Animal) 객체 생성")

    def speak(self):
        # raise NotImplementedError("자식 클래스에서 이 메서드를 반드시 구현해야 합니다.")
        print(f"{self.name}이(가) 소리를 냅니다. (일반적인 소리)")

class Dog(Animal): # 자식 클래스 (Animal을 상속)
    def __init__(self, name, breed):
        super().__init__(name) # 부모 클래스의 __init__ 호출하여 name 속성 초기화
        self.breed = breed    # Dog 클래스만의 속성 추가
        print(f"{self.name} ({self.breed}) Dog 객체 생성")

    def speak(self): # 부모 클래스의 speak 메서드 오버라이딩
        print(f"{self.name} ({self.breed})이(가) 멍멍! 하고 짖습니다.")

    def wag_tail(self):
        print(f"{self.name}이(가) 꼬리를 흔듭니다.")

class Cat(Animal): # 또 다른 자식 클래스
    def __init__(self, name, color):
        super().__init__(name)
        self.color = color
        print(f"{self.name} ({self.color}) Cat 객체 생성")
    
    def speak(self): # 메서드 오버라이딩
        print(f"{self.name} ({self.color})이(가) 야옹~ 하고 웁니다.")

my_dog = Dog("바둑이", "진돗개")
my_cat = Cat("나비", "삼색이")
generic_animal = Animal("동물친구")

my_dog.speak()      # Dog 클래스의 speak() 호출
my_cat.speak()      # Cat 클래스의 speak() 호출
generic_animal.speak() # Animal 클래스의 speak() 호출
my_dog.wag_tail()   # Dog 클래스만의 메서드
# generic_animal.wag_tail() # AttributeError (Animal 클래스에는 없음)

예상 결과:

바둑이 (Animal) 객체 생성
바둑이 (진돗개) Dog 객체 생성
나비 (Animal) 객체 생성
나비 (삼색이) Cat 객체 생성
동물친구 (Animal) 객체 생성
바둑이 (진돗개)이(가) 멍멍! 하고 짖습니다.
나비 (삼색이)이(가) 야옹~ 하고 웁니다.
동물친구이(가) 소리를 냅니다. (일반적인 소리)
바둑이이(가) 꼬리를 흔듭니다.

6.3.3. 다형성 (Polymorphism)

다형성은 '여러 가지 형태를 가질 수 있는 능력'을 의미합니다. OOP에서 다형성은 서로 다른 클래스의 객체들이 동일한 이름의 메서드를 호출했을 때, 각 객체의 타입에 따라 서로 다른 방식으로 동작하는 것을 말합니다.

파이썬은 동적 타이핑 언어이므로, 객체의 특정 타입보다는 객체가 특정 메서드나 속성을 가지고 있는지 여부(덕 타이핑, Duck Typing: "오리처럼 걷고, 오리처럼 꽥꽥거리면, 그것은 오리다")를 중시합니다. 이로 인해 다형성이 자연스럽게 지원됩니다.

메서드 오버라이딩은 다형성을 구현하는 대표적인 방법 중 하나입니다. 또한, 파이썬의 내장 함수 len()이 문자열, 리스트, 딕셔너리 등 다양한 타입의 객체에 대해 길이를 반환하는 것도 다형성의 한 예입니다 (각 타입이 내부적으로 __len__ 메서드를 다르게 구현하기 때문).

예시 코드: 다형성

# 위 Animal, Dog, Cat 클래스 재사용

def animal_sound_test(animals):
    """동물 객체 리스트를 받아 각 동물의 소리를 출력합니다."""
    for animal in animals:
        animal.speak() # animal 객체의 실제 타입에 따라 해당 클래스의 speak()가 호출됨

# Animal, Dog, Cat 클래스의 인스턴스들을 리스트에 담음
animal_list = [
    Dog("해피", "푸들"),
    Cat("야통이", "코리안숏헤어"),
    Animal("그냥동물")
]

animal_sound_test(animal_list)

print("\\n다형성 예시 (len 함수):")
print(len("hello"))      # 문자열 길이
print(len([1, 2, 3, 4])) # 리스트 길이
print(len({"a":1, "b":2})) # 딕셔너리 키 개수

예상 결과 (객체 생성 메시지는 생략):

해피 (푸들)이(가) 멍멍! 하고 짖습니다.
야통이 (코리안숏헤어)이(가) 야옹~ 하고 웁니다.
그냥동물이(가) 소리를 냅니다. (일반적인 소리)

다형성 예시 (len 함수):
5
4
2

이번 장에서는 객체 지향 프로그래밍의 기본적인 개념인 클래스와 객체, 그리고 OOP의 핵심 원리인 캡슐화, 상속, 다형성에 대해 알아보았습니다. OOP는 복잡한 시스템을 보다 체계적이고 효율적으로 설계하고 구현할 수 있게 해주는 강력한 도구입니다. 파이썬의 객체 지향 기능을 잘 활용하면 더욱 견고하고 유연한 프로그램을 만들 수 있습니다. 다음 장에서는 코드를 모듈화하고 다른 사람이 만든 유용한 기능을 가져와 사용하는 모듈과 패키지에 대해 배우겠습니다.

제 7 장: 모듈과 패키지

프로그램의 규모가 커지면 하나의 파일에 모든 코드를 작성하는 것이 비효율적이고 관리하기 어려워집니다. 파이썬에서는 관련된 코드(함수, 클래스, 변수 등)를 별도의 파일에 저장하여 재사용하고 체계적으로 관리할 수 있도록 하는 '모듈(Module)'과, 이러한 모듈들을 다시 디렉터리 구조로 묶어 관리하는 '패키지(Package)'라는 강력한 기능을 제공합니다.

모듈과 패키지를 사용하면 코드의 가독성을 높이고, 이름 충돌(naming conflict)을 방지하며, 다른 사람이 만든 유용한 기능을 쉽게 가져와(import) 활용할 수 있습니다. 이번 장에서는 모듈과 패키지를 만들고 사용하는 방법, 파이썬이 모듈을 찾는 과정, 그리고 파이썬의 강력한 기능 중 하나인 풍부한 표준 라이브러리의 몇 가지 예시를 살펴보겠습니다.

7.1. 모듈이란? (What is a Module?)

모듈은 파이썬 정의와 문장들(함수, 클래스, 변수 등)을 담고 있는 파일입니다. 파일 이름이 곧 모듈 이름이 되며, 확장자는 .py입니다. 예를 들어, my_functions.py라는 파일에 여러 함수를 정의해두면, 이 파일 자체가 my_functions라는 이름의 모듈이 됩니다.

모듈을 사용하는 주된 목적은 다음과 같습니다:

7.2. 모듈 만들기 및 사용하기

7.2.1. 모듈 만들기

모듈을 만드는 것은 매우 간단합니다. 그냥 파이썬 코드를 .py 확장자로 저장하면 됩니다.

예를 들어, my_math_module.py라는 이름의 파일을 만들고 다음과 같은 내용을 작성합니다:

예시 코드: my_math_module.py (모듈 파일 예시)

# my_math_module.py

PI = 3.141592

def add(a, b):
    """두 수를 더한 결과를 반환합니다."""
    return a + b

def subtract(a, b):
    """두 수의 차를 반환합니다."""
    return a - b

class Circle:
    """원의 넓이와 둘레를 계산하는 클래스입니다."""
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return PI * self.radius * self.radius

    def circumference(self):
        return 2 * PI * self.radius

my_math_module.py 파일은 my_math_module이라는 이름의 모듈이 됩니다.

7.2.2. 모듈 가져오기 (Importing Modules)

다른 파이썬 스크립트에서 모듈의 내용을 사용하려면 import 문을 사용해야 합니다. 모듈을 가져오는 방법은 여러 가지가 있습니다.

1. import 모듈이름

모듈 전체를 가져옵니다. 모듈 내의 함수, 클래스, 변수를 사용하려면 모듈이름.멤버이름 형식으로 접근해야 합니다.

예시 코드: import 모듈이름 (다른 .py 파일에서 사용)

# main_script.py (my_math_module.py와 같은 디렉터리에 있다고 가정)
import my_math_module

print(f"원주율: {my_math_module.PI}")

sum_result = my_math_module.add(10, 5)
print(f"10 + 5 = {sum_result}")

circle1 = my_math_module.Circle(5)
print(f"반지름 5인 원의 넓이: {circle1.area()}")

예상 결과:

원주율: 3.141592
10 + 5 = 15
반지름 5인 원의 넓이: 78.5398
2. from 모듈이름 import 멤버이름1, 멤버이름2, ...

모듈에서 특정 멤버(함수, 클래스, 변수)만 현재 네임스페이스로 직접 가져옵니다. 가져온 멤버는 모듈 이름을 붙이지 않고 바로 사용할 수 있습니다.

예시 코드: from 모듈이름 import 멤버이름

# main_script.py
from my_math_module import PI, add, Circle

print(f"원주율 (직접 접근): {PI}")

diff_result = add(100, 50) # add 함수 직접 사용
print(f"100 + 50 = {diff_result}")

circle2 = Circle(3) # Circle 클래스 직접 사용
print(f"반지름 3인 원의 둘레: {circle2.circumference()}")

예상 결과:

원주율 (직접 접근): 3.141592
100 + 50 = 150
반지름 3인 원의 둘레: 18.849552
3. from 모듈이름 import *

모듈의 모든 공개(public) 멤버를 현재 네임스페이스로 가져옵니다. (밑줄 _로 시작하는 이름은 보통 가져오지 않음). 이 방식은 어떤 이름이 현재 네임스페이스로 들어왔는지 명확하지 않아 이름 충돌의 위험이 있으므로, 일반적으로 권장되지 않습니다 (특히 큰 모듈의 경우).

4. import 모듈이름 as 별칭

모듈을 가져오면서 별칭(alias)을 부여합니다. 모듈 이름이 길거나 다른 모듈과 이름이 겹칠 때 유용합니다.

예시 코드: import 모듈이름 as 별칭

# main_script.py
import my_math_module as mmm # my_math_module을 mmm으로 사용

print(f"원주율 (별칭 사용): {mmm.PI}")
product = mmm.subtract(20, 7) # mmm.subtract 사용
print(f"20 - 7 = {product}")

예상 결과:

원주율 (별칭 사용): 3.141592
20 - 7 = 13
5. from 모듈이름 import 멤버이름 as 별칭

모듈의 특정 멤버를 가져오면서 별칭을 부여합니다.

예시 코드: from 모듈이름 import 멤버이름 as 별칭

# main_script.py
from my_math_module import add as 더하기함수, Circle as 원형클래스

total = 더하기함수(1, 2)
print(f"1 + 2 (별칭 사용) = {total}")

my_circle = 원형클래스(10)
print(f"반지름 10인 원의 넓이 (별칭 사용): {my_circle.area()}")

예상 결과:

1 + 2 (별칭 사용) = 3
반지름 10인 원의 넓이 (별칭 사용): 314.1592

7.2.3. 모듈 검색 경로 (Module Search Path)

파이썬 인터프리터는 import 문을 만나면 특정 순서로 모듈을 찾습니다. 이 검색 경로는 sys.path 리스트에 저장되어 있으며, 다음과 같은 순서로 탐색합니다:

  1. 현재 스크립트가 실행되는 디렉터리 (또는 현재 디렉터리)
  2. PYTHONPATH 환경 변수에 설정된 디렉터리들
  3. 파이썬 설치 시 기본으로 제공되는 표준 라이브러리 디렉터리들

sys.path는 파이썬 리스트이므로, 프로그램 내에서 동적으로 경로를 추가할 수도 있습니다 (예: import sys; sys.path.append('/my/custom/module/path')). 하지만 이는 주로 임시적인 해결책이며, 프로젝트 구조를 잘 설계하는 것이 더 좋습니다.

7.3. 패키지란? (What is a Package?)

패키지는 점(.)으로 구분된 모듈 이름(예: my_package.sub_package.module_x)을 사용하여 파이썬의 모듈 네임스페이스를 구조화하는 방법입니다. 간단히 말해, 패키지는 여러 관련 모듈들을 담고 있는 디렉터리 계층 구조입니다.

패키지를 사용하는 목적은 다음과 같습니다:

__init__.py 파일

파이썬 인터프리터가 어떤 디렉터리를 패키지로 인식하게 하려면, 해당 디렉터리 안에 __init__.py 라는 이름의 파일이 있어야 합니다. 이 파일은 비어 있어도 되지만, 다음과 같은 역할을 수행할 수 있습니다:

파이썬 3.3부터는 __init__.py 파일이 없어도 네임스페이스 패키지(namespace package)로 인식될 수 있지만, 일반적인 패키지에는 __init__.py를 포함하는 것이 관례입니다.

7.4. 패키지 만들기 및 사용하기

패키지를 만드는 것은 디렉터리 구조를 만드는 것과 유사합니다.

예를 들어, 다음과 같은 구조의 패키지를 만든다고 가정해봅시다:

my_app/  <-- 최상위 패키지 디렉터리
    __init__.py
    utils/       <-- 하위 패키지 (서브 패키지)
        __init__.py
        formatter.py
        logger.py
    models/      <-- 또 다른 하위 패키지
        __init__.py
        user.py
        product.py
    main.py      <-- 패키지 외부의 실행 스크립트 (예시)

여기서 my_app, my_app/utils, my_app/models는 각각 __init__.py 파일을 포함하므로 패키지로 인식됩니다. formatter.py, logger.py, user.py, product.py는 각 패키지 내의 모듈입니다.

my_app/utils/formatter.py 내용 예시:

예시 코드: my_app/utils/formatter.py

# formatter.py
def format_currency(amount, currency_symbol="$"):
    return f"{currency_symbol}{amount:,.2f}"

my_app/main.py에서 이 패키지의 모듈을 사용하는 방법은 다음과 같습니다 (main.pymy_app 디렉터리의 부모 디렉터리에 있거나, my_appPYTHONPATH에 포함되어야 함):

예시 코드: 패키지 사용 (main.py에서)

# main.py
# (my_app 디렉터리가 파이썬 검색 경로에 있다고 가정)

# 방법 1: import 패키지.하위패키지.모듈
import my_app.utils.formatter
price = 12345.67
formatted_price = my_app.utils.formatter.format_currency(price)
print(f"방법 1: {formatted_price}")

# 방법 2: from 패키지.하위패키지 import 모듈
from my_app.utils import formatter
formatted_price_2 = formatter.format_currency(price, currency_symbol="€")
print(f"방법 2: {formatted_price_2}")

# 방법 3: from 패키지.하위패키지.모듈 import 멤버
from my_app.utils.formatter import format_currency
formatted_price_3 = format_currency(price, currency_symbol="₩")
print(f"방법 3: {formatted_price_3}")

# __init__.py를 활용하여 import 경로 단축하기
# 만약 my_app/utils/__init__.py 안에 다음 코드가 있다면:
# from .formatter import format_currency
#
# 그러면 main.py에서 다음과 같이 사용 가능:
# from my_app.utils import format_currency
# formatted_price_4 = format_currency(price)
# print(f"방법 4 (__init__.py 활용): {formatted_price_4}")

예상 결과 (실제 실행 환경에 따라 경로 설정 필요):

방법 1: $12,345.67
방법 2: €12,345.67
방법 3: ₩12,345.67

상대적 임포트 (Relative Imports): 같은 패키지 내의 다른 모듈을 임포트할 때는 상대 경로를 사용할 수 있습니다. 예를 들어, my_app/utils/logger.py에서 같은 utils 패키지 내의 formatter.py를 임포트하려면 from .formatter import format_currency와 같이 사용합니다. 점(.)은 현재 패키지를, 점 두 개(..)는 부모 패키지를 의미합니다. 상대적 임포트는 패키지 내부에서만 사용해야 합니다.

7.5. 표준 라이브러리 (The Python Standard Library)

파이썬은 "배터리가 포함되어 있다(batteries included)"는 철학에 따라, 매우 강력하고 풍부한 표준 라이브러리를 기본으로 제공합니다. 표준 라이브러리는 별도의 설치 없이 파이썬을 설치하면 바로 사용할 수 있는 다양한 모듈과 패키지의 모음입니다. 다음은 자주 사용되는 몇 가지 표준 라이브러리 모듈입니다:

표준 라이브러리의 전체 목록과 각 모듈에 대한 자세한 설명은 파이썬 공식 문서에서 확인할 수 있습니다.

예시 코드: 표준 라이브러리 사용

import math
print(f"루트 16: {math.sqrt(16)}")
print(f"PI 값: {math.pi}")

import random
print(f"1~10 사이의 임의의 정수: {random.randint(1, 10)}")
my_list = ['a', 'b', 'c', 'd']
random.shuffle(my_list) # 원본 리스트를 섞음
print(f"섞인 리스트: {my_list}")
print(f"임의의 요소 선택: {random.choice(my_list)}")

import datetime
now = datetime.datetime.now()
print(f"현재 날짜 및 시간: {now}")
print(f"오늘 날짜: {now.date()}")
print(f"현재 시간: {now.time()}")
one_week_later = now + datetime.timedelta(weeks=1)
print(f"일주일 후: {one_week_later}")

import json
person_data = {"name": "Alice", "age": 30, "city": "Seoul"}
json_string = json.dumps(person_data, ensure_ascii=False, indent=4) # ensure_ascii=False 한글 유지, indent 예쁘게 출력
print("\\nJSON 문자열:")
print(json_string)
parsed_data = json.loads(json_string)
print(f"파싱된 데이터의 이름: {parsed_data['name']}")

예상 결과 (random, datetime 결과는 실행 시마다 다를 수 있음):

루트 16: 4.0
PI 값: 3.141592653589793
1~10 사이의 임의의 정수: (1에서 10 사이의 어떤 숫자)
섞인 리스트: (['a', 'b', 'c', 'd']가 섞인 순서)
임의의 요소 선택: (섞인 리스트 중 하나의 요소)
현재 날짜 및 시간: (실행 시점의 현재 날짜와 시간)
오늘 날짜: (실행 시점의 오늘 날짜)
현재 시간: (실행 시점의 현재 시간)
일주일 후: (실행 시점으로부터 일주일 후의 날짜와 시간)

JSON 문자열:
{
    "name": "Alice",
    "age": 30,
    "city": "Seoul"
}
파싱된 데이터의 이름: Alice

7.6. if __name__ == "__main__": 관용구

파이썬 파일을 직접 스크립트로 실행할 때와 다른 모듈에서 임포트하여 사용할 때를 구분하여 특정 코드가 실행되도록 하고 싶을 때 if __name__ == "__main__": 관용구를 사용합니다.

따라서 if __name__ == "__main__": 블록 안의 코드는 해당 파일이 직접 실행될 때만 실행됩니다. 이는 모듈 테스트용 코드나 스크립트의 주 실행 로직을 작성하는 데 유용합니다.

예시 코드: my_script_module.py (if __name__ == "__main__" 사용)

# my_script_module.py

def useful_function():
    print("useful_function이 호출되었습니다.")

def another_function():
    print("another_function이 호출되었습니다.")

print(f"my_script_module의 __name__: {__name__}") # 모듈이 임포트되거나 실행될 때 항상 실행됨

if __name__ == "__main__":
    # 이 블록은 my_script_module.py가 직접 실행될 때만 실행됩니다.
    # 다른 모듈에서 import my_script_module 할 때는 실행되지 않습니다.
    print("스크립트가 직접 실행되었습니다!")
    useful_function()
    another_function()
    # 여기에 테스트 코드나 주 실행 로직을 넣을 수 있습니다.
else:
    # 이 블록은 my_script_module.py가 다른 곳에서 임포트될 때 실행됩니다.
    print("my_script_module이 임포트되었습니다.")

# 이 파일을 다른 파일(예: main_caller.py)에서 import 해보세요.
# 그리고 직접 python my_script_module.py 로 실행해보세요.

예상 결과 1: python my_script_module.py로 직접 실행 시

my_script_module의 __name__: __main__
스크립트가 직접 실행되었습니다!
useful_function이 호출되었습니다.
another_function이 호출되었습니다.

예상 결과 2: 다른 파일에서 import my_script_module 할 때 (my_script_module.py의 출력)

my_script_module의 __name__: my_script_module
my_script_module이 임포트되었습니다.

7.7. 서드파티 라이브러리 및 PyPI

파이썬의 강력함은 방대한 표준 라이브러리뿐만 아니라, 전 세계 개발자들이 만들어 공유하는 수많은 서드파티(third-party) 라이브러리에서도 나옵니다. 이러한 라이브러리들은 PyPI(Python Package Index)라는 공식 저장소를 통해 제공되며, pip라는 패키지 관리 도구를 사용하여 쉽게 설치하고 관리할 수 있습니다.

터미널이나 커맨드 프롬프트에서 다음 명령으로 패키지를 설치할 수 있습니다:

pip install 패키지이름

예를 들어, 웹 요청을 보내는 데 유용한 requests 라이브러리, 수치 계산에 강력한 NumPy, 데이터 분석에 필수적인 Pandas, 시각화 도구인 Matplotlib, 웹 개발 프레임워크인 DjangoFlask 등 매우 다양한 분야의 라이브러리들이 존재합니다. (이러한 라이브러리들은 1장에서 간략히 소개되었습니다.)

이번 장에서는 파이썬 코드를 체계적으로 구성하고 재사용하기 위한 모듈과 패키지의 개념과 사용법, 그리고 파이썬의 풍부한 표준 라이브러리 및 서드파티 생태계에 대해 알아보았습니다. 이러한 기능들을 잘 활용하면 복잡한 프로그램도 효율적으로 개발하고 관리할 수 있습니다. 다음 장에서는 프로그램 외부의 파일로부터 데이터를 읽거나 파일로 저장하는 파일 입출력 방법에 대해 배우겠습니다.

제 8 장: 파일 입출력

대부분의 프로그램은 실행 중에 생성된 데이터를 영구적으로 저장하거나, 외부로부터 데이터를 읽어와 처리해야 합니다. 파일 입출력(File Input/Output, File I/O)은 프로그램이 디스크와 같은 저장 장치에 있는 파일로부터 데이터를 읽거나(입력) 파일에 데이터를 쓰는(출력) 작업을 의미합니다. 이를 통해 데이터의 영속성(persistence)을 확보하고, 프로그램 간 데이터 교환, 로그 기록 등 다양한 작업을 수행할 수 있습니다.

파이썬은 파일 처리를 위한 강력하고 사용하기 쉬운 내장 함수와 메서드를 제공합니다. 이번 장에서는 파일을 열고 닫는 기본적인 방법, 텍스트 파일과 이진 파일을 다루는 모드, 파일에서 데이터를 읽고 쓰는 다양한 방법, 그리고 파일 시스템과 관련된 몇 가지 유용한 기능에 대해 알아보겠습니다.

8.1. 파일 열고 닫기

8.1.1. open() 함수

파일 작업을 시작하려면 먼저 open() 함수를 사용하여 파일을 열어야 합니다. open() 함수는 파일 객체(file object)를 반환하며, 이 객체를 통해 파일 읽기/쓰기 작업을 수행합니다.

기본적인 open() 함수의 구문은 다음과 같습니다:

file_object = open(file_path, mode, encoding=None)

8.1.2. close() 메서드

파일 작업을 마친 후에는 반드시 file_object.close() 메서드를 호출하여 파일을 닫아야 합니다. 파일을 닫으면 다음과 같은 중요한 작업이 수행됩니다:

8.1.3. with 문 (컨텍스트 관리자) - 권장 방식

파일을 열고 닫는 작업을 더 안전하고 간결하게 처리하기 위해 with 문을 사용하는 것이 강력히 권장됩니다. with 문은 컨텍스트 관리 프로토콜을 따르는 객체(파일 객체도 해당)와 함께 사용되며, with 블록이 시작될 때 파일이 열리고, 블록을 벗어날 때 (정상적으로 종료되든, 예외가 발생하든) 자동으로 파일이 닫힙니다.

with open(file_path, mode, encoding='utf-8') as file_object:
    # file_object를 사용하여 파일 작업 수행
    # 이 블록을 벗어나면 file_object.close()가 자동으로 호출됨
    pass

with 문을 사용하면 close() 메서드를 명시적으로 호출할 필요가 없어 코드가 간결해지고, 파일을 닫는 것을 잊어버리는 실수를 방지할 수 있습니다.

예시 코드: 파일 열고 닫기 (with 문 사용)

# example.txt 파일을 쓰기 모드('w')로 열고 내용 작성
try:
    with open("example.txt", "w", encoding="utf-8") as f:
        f.write("안녕하세요, 파이썬 파일 입출력입니다.\\n")
        f.write("with 문을 사용하면 편리합니다.\\n")
    print("example.txt 파일에 쓰기를 완료했습니다.")

    # example.txt 파일을 읽기 모드('r')로 열고 내용 읽기
    with open("example.txt", "r", encoding="utf-8") as f:
        content = f.read()
        print("\\n--- example.txt 파일 내용 ---")
        print(content)
    print("example.txt 파일 읽기를 완료했습니다. 파일은 자동으로 닫혔습니다.")

except FileNotFoundError:
    print("파일을 찾을 수 없습니다.")
except Exception as e:
    print(f"오류 발생: {e}")

예상 결과:

example.txt 파일에 쓰기를 완료했습니다.

--- example.txt 파일 내용 ---
안녕하세요, 파이썬 파일 입출력입니다.
with 문을 사용하면 편리합니다.

example.txt 파일 읽기를 완료했습니다. 파일은 자동으로 닫혔습니다.

8.2. 파일 읽기 (Reading from Files)

파일을 읽기 모드('r', 'rt', 'rb' 등)로 연 후, 파일 객체의 여러 메서드를 사용하여 내용을 읽을 수 있습니다.

예시 코드: 다양한 파일 읽기 방법

# 테스트용 파일 생성
with open("readme.txt", "w", encoding="utf-8") as f:
    f.write("첫 번째 줄입니다.\\n")
    f.write("두 번째 줄입니다.\\n")
    f.write("세 번째 줄입니다.\\n")

print("--- read()로 전체 읽기 ---")
with open("readme.txt", "r", encoding="utf-8") as f:
    data_all = f.read()
    print(data_all)

print("--- readline()으로 한 줄씩 읽기 ---")
with open("readme.txt", "r", encoding="utf-8") as f:
    line1 = f.readline()
    print(f"첫 줄: {line1.strip()}") # strip()으로 양쪽 공백 및 개행 문자 제거
    line2 = f.readline()
    print(f"두 번째 줄: {line2.strip()}")

print("--- readlines()로 모든 줄 읽어 리스트로 받기 ---")
with open("readme.txt", "r", encoding="utf-8") as f:
    lines_list = f.readlines()
    for i, line_content in enumerate(lines_list):
        print(f"리스트 {i}번째 줄: {line_content.strip()}")

print("--- for 문으로 한 줄씩 읽기 (가장 효율적) ---")
with open("readme.txt", "r", encoding="utf-8") as f:
    for line_num, current_line in enumerate(f):
        print(f"for문 {line_num+1}번째 줄: {current_line.strip()}")

예상 결과:

--- read()로 전체 읽기 ---
첫 번째 줄입니다.
두 번째 줄입니다.
세 번째 줄입니다.

--- readline()으로 한 줄씩 읽기 ---
첫 줄: 첫 번째 줄입니다.
두 번째 줄: 두 번째 줄입니다.
--- readlines()로 모든 줄 읽어 리스트로 받기 ---
리스트 0번째 줄: 첫 번째 줄입니다.
리스트 1번째 줄: 두 번째 줄입니다.
리스트 2번째 줄: 세 번째 줄입니다.
--- for 문으로 한 줄씩 읽기 (가장 효율적) ---
for문 1번째 줄: 첫 번째 줄입니다.
for문 2번째 줄: 두 번째 줄입니다.
for문 3번째 줄: 세 번째 줄입니다.

8.3. 파일 쓰기 (Writing to Files)

파일을 쓰기 모드('w', 'a', 'wt', 'wb' 등)로 연 후, 파일 객체의 메서드를 사용하여 내용을 쓸 수 있습니다.

파일에 데이터를 쓸 때, 데이터는 즉시 디스크에 저장되지 않고 내부 버퍼에 모였다가 일정 조건(버퍼가 차거나, 파일이 닫히거나, flush() 메서드가 호출될 때)에 따라 디스크에 기록(flush)됩니다.

예시 코드: 파일 쓰기

# 새 파일 생성 및 쓰기 ('w' 모드: 기존 내용 덮어씀)
with open("output1.txt", "w", encoding="utf-8") as f:
    f.write("파이썬으로 파일 쓰기 예제입니다.\\n")
    f.write("write() 메서드는 줄바꿈을 자동으로 해주지 않습니다.\\n그래서 \\\\n을 직접 넣어야 합니다.\\n")
    num_written = f.write("세 번째 줄.")
    print(f"'세 번째 줄.' 쓰고 반환된 문자 수: {num_written}")

print("output1.txt 에 쓰기 완료.")

# 기존 파일에 내용 추가 ('a' 모드)
with open("output1.txt", "a", encoding="utf-8") as f:
    f.write("\\n--- 내용 추가 시작 ---\\n")
    f.write("이 내용은 기존 파일의 끝에 추가됩니다.\\n")
print("output1.txt 에 내용 추가 완료.")

# writelines() 사용
lines_to_write = [
    "여러 줄 쓰기 첫 번째 줄\\n",
    "두 번째 줄 내용\\n",
    "세 번째 줄도 써볼까요?\\n"
]
with open("output2.txt", "w", encoding="utf-8") as f:
    f.writelines(lines_to_write)
print("output2.txt 에 writelines()로 쓰기 완료.")

예상 결과 (output1.txt, output2.txt 파일이 생성/수정됨):

'세 번째 줄.' 쓰고 반환된 문자 수: 7
output1.txt 에 쓰기 완료.
output1.txt 에 내용 추가 완료.
output2.txt 에 writelines()로 쓰기 완료.

output1.txt 내용:

파이썬으로 파일 쓰기 예제입니다.
write() 메서드는 줄바꿈을 자동으로 해주지 않습니다.
그래서 \n을 직접 넣어야 합니다.
세 번째 줄.
--- 내용 추가 시작 ---
이 내용은 기존 파일의 끝에 추가됩니다.

output2.txt 내용:

여러 줄 쓰기 첫 번째 줄
두 번째 줄 내용
세 번째 줄도 써볼까요?

8.4. 파일 포인터 (File Pointer)

파일 객체는 파일 내에서 현재 읽거나 쓸 위치를 가리키는 '파일 포인터(file pointer)' 또는 '현재 파일 위치(current file position)'를 내부적으로 유지합니다. 파일에서 읽기 또는 쓰기 작업을 수행하면 이 포인터는 자동으로 다음 위치로 이동합니다.

주의: 텍스트 모드에서 파일을 열었을 때, seek()tell()은 주로 파일의 시작(seek(0))이나 tell()이 반환한 특정 위치로만 이동하는 것이 안전합니다. 텍스트 파일은 문자 인코딩 방식이나 운영체제의 줄 끝 문자 처리 방식에 따라 바이트 수와 문자 수가 일치하지 않을 수 있어, 임의의 offset으로 seek()를 사용하면 예기치 않은 결과가 발생할 수 있습니다. 이진 모드에서는 바이트 단위로 정확하게 위치를 이동할 수 있습니다.

예시 코드: 파일 포인터 제어 (이진 모드에서 더 명확)

# 테스트용 텍스트 파일 (UTF-8은 문자에 따라 바이트 수가 다를 수 있음)
file_content_for_seek = "가나다라마바사아자차카타파하" # 한글 15자
with open("seek_test.txt", "w", encoding="utf-8") as f:
    f.write(file_content_for_seek)

with open("seek_test.txt", "r", encoding="utf-8") as f:
    print(f"파일 시작 시 tell(): {f.tell()}") # 보통 0
    first_char = f.read(1) # 한 글자 읽기
    print(f"한 글자 읽음: '{first_char}', tell(): {f.tell()} (UTF-8에서 '가'는 3바이트일 수 있음)")
    
    f.seek(0) # 파일 시작으로 포인터 이동
    print(f"seek(0) 후 tell(): {f.tell()}")
    
    five_chars = f.read(5) # 5 글자 읽기
    print(f"5글자 읽음: '{five_chars}', tell(): {f.tell()}")

# 이진 모드 예시
with open("binary_seek_test.bin", "wb") as bf:
    bf.write(b"abcdefghij") # 10 바이트

with open("binary_seek_test.bin", "rb") as bf:
    print(f"\\n이진 파일 시작 시 tell(): {bf.tell()}") # 0
    data_3bytes = bf.read(3)
    print(f"3 바이트 읽음: {data_3bytes}, tell(): {bf.tell()}") # 3
    
    bf.seek(2, 1) # 현재 위치(3)에서 2바이트 앞으로 이동 -> 5
    print(f"seek(2, 1) 후 tell(): {bf.tell()}")
    data_at_5 = bf.read(1)
    print(f"위치 5에서 1바이트 읽음: {data_at_5}")
    
    bf.seek(-3, 2) # 파일 끝에서 3바이트 뒤로 (h 위치)
    print(f"seek(-3, 2) 후 tell(): {bf.tell()}") # 7
    data_last_3 = bf.read()
    print(f"파일 끝에서 3번째부터 끝까지: {data_last_3}")

예상 결과 (UTF-8 인코딩 및 시스템에 따라 바이트 수 다를 수 있음):

파일 시작 시 tell(): 0
한 글자 읽음: '가', tell(): 3 (또는 다른 바이트 수)
seek(0) 후 tell(): 0
5글자 읽음: '가나다라마', tell(): 15 (또는 다른 바이트 수)

이진 파일 시작 시 tell(): 0
3 바이트 읽음: b'abc', tell(): 3
seek(2, 1) 후 tell(): 5
위치 5에서 1바이트 읽음: b'f'
seek(-3, 2) 후 tell(): 7
파일 끝에서 3번째부터 끝까지: b'hij'

8.5. 파일 및 디렉터리 관리 (os 모듈 활용)

파일 입출력뿐만 아니라 파일 자체나 디렉터리를 관리하는 작업도 필요할 때가 많습니다. 파이썬의 os 모듈과 os.path 하위 모듈은 이러한 파일 시스템 관련 작업을 수행하는 다양한 함수를 제공합니다. (7장에서 간략히 소개됨)

참고: 비어 있지 않은 디렉터리와 그 내용을 모두 삭제하려면 shutil 모듈의 shutil.rmtree(path) 함수를 사용해야 합니다.

예시 코드: os 모듈 사용

import os
import shutil # 디렉터리 트리 삭제 시 필요

# 파일/디렉터리 존재 확인
file_to_check = "output1.txt"
if os.path.exists(file_to_check):
    print(f"'{file_to_check}' 파일이 존재합니다.")
    if os.path.isfile(file_to_check):
        print(f"'{file_to_check}'은(는) 파일입니다.")
        print(f"파일 크기: {os.path.getsize(file_to_check)} 바이트")
else:
    print(f"'{file_to_check}' 파일이 존재하지 않습니다.")

# 새 디렉터리 생성
new_dir = "my_temp_dir"
if not os.path.exists(new_dir):
    os.mkdir(new_dir)
    print(f"'{new_dir}' 디렉터리를 생성했습니다.")
    # 생성된 디렉터리 내에 파일 생성
    with open(os.path.join(new_dir, "temp_file.txt"), "w") as tf:
        tf.write("임시 파일 내용")
    print(f"'{new_dir}' 내에 temp_file.txt 생성 완료.")
else:
    print(f"'{new_dir}' 디렉터리가 이미 존재합니다.")

# 디렉터리 내 파일 목록 보기
if os.path.exists(new_dir) and os.path.isdir(new_dir):
    print(f"'{new_dir}' 디렉터리 내용: {os.listdir(new_dir)}")

# 파일 삭제 및 디렉터리 삭제 (주의해서 실행)
# temp_file_path = os.path.join(new_dir, "temp_file.txt")
# if os.path.exists(temp_file_path):
#     os.remove(temp_file_path)
#     print(f"'{temp_file_path}' 파일을 삭제했습니다.")
# if os.path.exists(new_dir):
#     # os.rmdir(new_dir) # 비어있지 않으면 오류 발생
#     shutil.rmtree(new_dir) # 내용물과 함께 디렉터리 삭제 (주의!)
#     print(f"'{new_dir}' 디렉터리(와 내용물)를 삭제했습니다.")

예상 결과 (실행 시점에 따라 파일/디렉터리 상태가 다를 수 있음):

'output1.txt' 파일이 존재합니다.
'output1.txt'은(는) 파일입니다.
파일 크기: (output1.txt의 실제 크기) 바이트
'my_temp_dir' 디렉터리를 생성했습니다.
'my_temp_dir' 내에 temp_file.txt 생성 완료.
'my_temp_dir' 디렉터리 내용: ['temp_file.txt']
(주석 처리된 삭제 코드 실행 시 해당 메시지 출력)

이번 장에서는 파이썬을 사용하여 파일로부터 데이터를 읽고 쓰는 기본적인 방법과 파일 시스템을 다루는 기초적인 내용을 학습했습니다. with open(...) 구문을 사용하여 파일을 안전하게 처리하는 것이 중요하며, 텍스트 파일을 다룰 때는 적절한 인코딩을 지정해야 합니다. 파일 입출력은 데이터 처리, 로그 분석, 설정 파일 관리 등 다양한 프로그래밍 작업의 기초가 됩니다. 다음 장에서는 프로그램 실행 중 발생할 수 있는 오류를 예측하고 대응하는 예외 처리에 대해 배우겠습니다.

제 9 장: 예외 처리

프로그램을 작성하고 실행하다 보면 다양한 종류의 오류를 마주하게 됩니다. 이러한 오류는 프로그램의 비정상적인 종료를 유발할 수 있으며, 사용자에게 좋지 않은 경험을 제공할 수 있습니다. 예외 처리(Exception Handling)는 프로그램 실행 중에 발생할 수 있는 예기치 않은 상황(예외)에 대비하고, 프로그램이 비정상적으로 중단되지 않고 적절하게 대응하도록 만드는 중요한 프로그래밍 기법입니다. 이번 장에서는 파이썬에서 오류와 예외가 무엇인지 알아보고, try, except, else, finally 키워드를 사용하여 예외를 처리하는 방법, 그리고 필요에 따라 예외를 직접 발생시키는 방법에 대해 학습합니다.

9.1. 오류와 예외란?

파이썬에서 오류는 크게 두 가지로 나눌 수 있습니다:

예외 처리는 바로 이 '예외(Exception)' 상황에 대처하기 위한 메커니즘입니다.

예시 코드: 구문 오류와 예외

# 구문 오류 예시 (실행 불가)
# print("Hello, Python!" # 괄호 누락
# if x > 0 y = x      # 콜론 누락 및 잘못된 문법

# 예외 발생 예시 (실행 중 오류)
# print(10 / 0)  # ZeroDivisionError
# my_list = [1, 2, 3]
# print(my_list[3]) # IndexError
# open("non_existent_file.txt", "r") # FileNotFoundError

9.2. 기본 예외 처리: try-except

가장 기본적인 예외 처리 구문은 tryexcept 블록을 사용하는 것입니다.

try:
    # 예외 발생 가능성이 있는 코드 블록
    # 이 코드를 실행하려고 시도합니다.
    명령문1
    명령문2
except 예외타입A:
    # try 블록에서 '예외타입A'가 발생했을 때 실행될 코드 블록
    예외_처리_명령문_A
except 예외타입B as 변수:
    # try 블록에서 '예외타입B'가 발생했을 때 실행될 코드 블록
    # 발생한 예외 객체는 '변수'에 할당되어 상세 정보 확인 가능
    예외_처리_명령문_B (변수 사용 가능)
except (예외타입C, 예외타입D):
    # '예외타입C' 또는 '예외타입D' 중 하나가 발생했을 때 실행
    예외_처리_명령문_CD
except: # 또는 except Exception:
    # 위에서 명시되지 않은 다른 모든 예외가 발생했을 때 실행 (일반적으로 권장되지 않음)
    모든_기타_예외_처리_명령문

try 블록에서 예외가 발생하면, 해당 예외를 처리할 수 있는 첫 번째 except 블록으로 점프하여 실행되고, 나머지 except 블록들은 건너뜁니다. 만약 발생한 예외를 처리할 수 있는 except 블록이 없다면, 예외는 처리되지 않고 프로그램은 비정상적으로 종료됩니다.

예시 코드: try-except 문 사용

def divide_numbers(x, y):
    try:
        result = x / y
        print(f"{x} / {y} = {result}")
    except ZeroDivisionError:
        print("오류: 0으로 나눌 수 없습니다.")
    except TypeError as e:
        print(f"오류: 잘못된 데이터 타입으로 연산을 시도했습니다. ({e})")
    except Exception as e: # 그 외 모든 예외 처리 (가장 마지막에 위치)
        print(f"알 수 없는 오류가 발생했습니다: {e}")

divide_numbers(10, 2)
divide_numbers(10, 0)
divide_numbers("백", 10) # TypeError 발생
divide_numbers(10, "이") # TypeError 발생

print("\\n파일 읽기 예외 처리:")
try:
    with open("non_existent_data.txt", "r", encoding="utf-8") as f:
        content = f.read()
        print(content)
except FileNotFoundError:
    print("오류: 파일을 찾을 수 없습니다.")
except IOError: # FileNotFoundError의 부모 클래스이므로 FileNotFoundError 뒤에 위치
    print("오류: 파일을 읽고 쓰는 중에 문제가 발생했습니다.")

예상 결과:

10 / 2 = 5.0
오류: 0으로 나눌 수 없습니다.
오류: 잘못된 데이터 타입으로 연산을 시도했습니다. (unsupported operand type(s) for /: 'str' and 'int')
오류: 잘못된 데이터 타입으로 연산을 시도했습니다. (unsupported operand type(s) for /: 'int' and 'str')

파일 읽기 예외 처리:
오류: 파일을 찾을 수 없습니다.

9.3. else

try-except 문에는 선택적으로 else 절을 추가할 수 있습니다. else 절은 try 블록에서 예외가 전혀 발생하지 않았을 경우에만 실행됩니다. else 절은 except 블록들 뒤에 위치해야 합니다.

try:
    # 예외 발생 가능성이 있는 코드
    명령문
except 예외타입:
    # 예외 발생 시 실행될 코드
    예외_처리_명령문
else:
    # 예외가 발생하지 않았을 때만 실행될 코드
    정상_실행_명령문

else 절을 사용하면, try 블록에는 예외 발생 가능성이 있는 최소한의 코드만 남기고, 예외가 발생하지 않아야만 실행되어야 하는 후속 처리 코드를 else 절로 분리하여 코드의 가독성을 높일 수 있습니다.

예시 코드: else 절 사용

try:
    num_str = input("숫자를 입력하세요: ")
    value = int(num_str)
except ValueError:
    print("잘못된 입력입니다. 정수를 입력해주세요.")
else:
    # try 블록에서 예외가 발생하지 않았을 때 (즉, int() 변환 성공 시) 실행
    print(f"입력한 숫자의 두 배는 {value * 2} 입니다.")

print("프로그램의 다음 부분입니다.")

예상 결과 1 (정상 입력 시):

숫자를 입력하세요: 10
입력한 숫자의 두 배는 20 입니다.
프로그램의 다음 부분입니다.

예상 결과 2 (잘못된 입력 시):

숫자를 입력하세요: abc
잘못된 입력입니다. 정수를 입력해주세요.
프로그램의 다음 부분입니다.

9.4. finally

try-except-else 문에는 선택적으로 finally 절을 추가할 수 있습니다. finally 절은 try 블록에서의 예외 발생 여부와 관계없이 (즉, try가 성공하든, 특정 except가 실행되든, 심지어 tryexcept 블록 내에서 return, break, continue 등으로 빠져나가더라도) 항상 마지막에 실행되는 코드 블록입니다.

try:
    # 예외 발생 가능성이 있는 코드
    명령문_A
except 예외타입:
    # 예외 발생 시 실행될 코드
    예외_처리_명령문
else:
    # 예외가 발생하지 않았을 때만 실행될 코드
    정상_실행_명령문
finally:
    # 예외 발생 여부와 관계없이 항상 실행될 코드
    마무리_명령문

finally 절은 주로 파일 닫기, 네트워크 연결 해제, 데이터베이스 커밋/롤백 등 프로그램이 어떤 상황에 처하든 반드시 실행되어야 하는 리소스 정리(clean-up) 작업에 사용됩니다. (단, 파일 닫기의 경우 with 문을 사용하는 것이 더 간결하고 안전합니다.)

예시 코드: finally 절 사용

def process_file(filepath):
    f = None # 파일 객체 변수 초기화
    try:
        print(f"파일 '{filepath}' 열기를 시도합니다.")
        f = open(filepath, "r", encoding="utf-8")
        content = f.read(50) # 처음 50자만 읽기
        print("--- 파일 내용 (일부) ---")
        print(content)
        # 여기서 오류를 발생시켜 봅시다 (예: result = 10 / 0)
        # result = 10 / 0 
    except FileNotFoundError:
        print(f"오류: '{filepath}' 파일을 찾을 수 없습니다.")
    except Exception as e:
        print(f"처리 중 오류 발생: {e}")
    else:
        print("파일 처리가 성공적으로 완료되었습니다.")
    finally:
        print("finally 블록: 파일 닫기를 시도합니다.")
        if f: # 파일 객체가 성공적으로 생성되었다면
            f.close()
            print(f"파일 '{filepath}'를 닫았습니다.")
        else:
            print("파일이 열리지 않아 닫을 수 없습니다.")

process_file("readme.txt") # 8장에서 만든 readme.txt 파일이 있다고 가정
print("-" * 30)
process_file("non_existent.txt")

예상 결과 (readme.txt 내용에 따라 일부 다를 수 있음):

파일 'readme.txt' 열기를 시도합니다.
--- 파일 내용 (일부) ---
첫 번째 줄입니다.
두 번째 줄입니다.
세 번째 줄입니다.

파일 처리가 성공적으로 완료되었습니다.
finally 블록: 파일 닫기를 시도합니다.
파일 'readme.txt'를 닫았습니다.
------------------------------
파일 'non_existent.txt' 열기를 시도합니다.
오류: 'non_existent.txt' 파일을 찾을 수 없습니다.
finally 블록: 파일 닫기를 시도합니다.
파일이 열리지 않아 닫을 수 없습니다.

9.5. 예외 발생시키기: raise

때로는 프로그램 로직상 특정 조건에서 의도적으로 예외를 발생시켜야 할 필요가 있습니다. 이럴 때 raise 문을 사용합니다.

raise 예외타입("선택적인 오류 메시지")
raise 예외객체
raise # except 블록 내에서 현재 처리 중인 예외를 다시 발생시킴 (re-raise)

예시 코드: raise 문 사용

def get_age_category(age):
    if not isinstance(age, int):
        raise TypeError("나이는 정수여야 합니다.")
    if age < 0:
        raise ValueError("나이는 음수일 수 없습니다.")
    elif age < 13:
        return "어린이"
    elif age < 19:
        return "청소년"
    else:
        return "성인"

try:
    # category = get_age_category(-5)
    # category = get_age_category("스무살")
    category = get_age_category(25)
    print(f"연령대: {category}")
except TypeError as e:
    print(f"타입 오류: {e}")
except ValueError as e:
    print(f"값 오류: {e}")

# 예외 다시 발생시키기 (re-raise)
def process_data_with_reraise(data):
    try:
        # 데이터를 처리하는 복잡한 로직 가정
        if data is None:
            raise ValueError("입력 데이터가 None입니다.")
        # ... 처리 ...
        print("데이터 처리 성공")
    except ValueError as ve:
        print(f"process_data_with_reraise에서 ValueError 발생: {ve} - 로깅 후 다시 발생")
        # 여기서 로깅 등의 추가 작업 수행 가능
        raise # 동일한 예외를 상위로 다시 전달

try:
    process_data_with_reraise(None)
except ValueError as final_ve:
    print(f"최종적으로 처리된 ValueError: {final_ve}")

예상 결과:

연령대: 성인
process_data_with_reraise에서 ValueError 발생: 입력 데이터가 None입니다. - 로깅 후 다시 발생
최종적으로 처리된 ValueError: 입력 데이터가 None입니다.

9.6. 주요 내장 예외 (Common Built-in Exceptions)

파이썬에는 다양한 상황에 맞는 여러 내장 예외 클래스들이 정의되어 있습니다. 모든 예외는 BaseException 클래스를 (직접 또는 간접적으로) 상속하며, 일반적인 프로그램 오류는 대부분 Exception 클래스의 하위 클래스입니다. 자주 접하게 되는 주요 내장 예외들은 다음과 같습니다:

전체 내장 예외 계층 구조는 파이썬 공식 문서에서 확인할 수 있습니다.

9.7. 사용자 정의 예외 (User-Defined Exceptions) - 간략 소개

때로는 내장 예외만으로는 프로그램의 특정 오류 상황을 명확하게 표현하기 어려울 수 있습니다. 이 경우, 개발자가 직접 새로운 예외 클래스를 정의하여 사용할 수 있습니다. 사용자 정의 예외는 일반적으로 Exception 클래스나 특정 내장 예외 클래스를 상속받아 만듭니다.

class MyCustomError(Exception):
    """이 애플리케이션만의 특정 오류 상황을 나타내는 예외입니다."""
    def __init__(self, message, error_code=None):
        super().__init__(message) # 부모 클래스의 생성자 호출
        self.error_code = error_code

# 사용 예시
# raise MyCustomError("특별한 문제가 발생했습니다!", 5001)

사용자 정의 예외를 사용하면 프로그램의 오류 상황을 더 구체적이고 의미론적으로 관리할 수 있습니다.

9.8. 예외 처리 시 좋은 습관

이번 장에서는 파이썬의 예외 처리 메커니즘에 대해 깊이 있게 학습했습니다. 안정적이고 견고한 프로그램을 만들기 위해서는 예외 상황을 예측하고 이에 적절히 대응하는 능력이 매우 중요합니다. try, except, else, finally 구문을 효과적으로 활용하고, 필요한 경우 예외를 직접 발생시켜 프로그램의 흐름을 제어할 수 있게 되었습니다. 다음 장에서는 리스트, 딕셔너리, 세트 등을 더욱 간결하고 효율적으로 생성할 수 있는 컴프리헨션에 대해 알아보겠습니다.

제 10 장: 컴프리헨션 (Comprehensions)

파이썬의 컴프리헨션(Comprehension)은 기존의 반복 가능한(iterable) 객체로부터 새로운 리스트(list), 딕셔너리(dictionary), 또는 세트(set)를 매우 간결하고 효율적으로 생성할 수 있게 해주는 강력한 기능입니다. 컴프리헨션을 사용하면 여러 줄에 걸쳐 작성해야 하는 for 반복문과 조건문 조합을 한 줄로 표현할 수 있어 코드의 가독성을 높이고 작성 시간도 단축할 수 있습니다 (단, 너무 복잡한 컴프리헨션은 오히려 가독성을 해칠 수 있으므로 주의해야 합니다). 수학의 집합 표현식(set-builder notation)과 유사한 형태를 가지고 있습니다.

이번 장에서는 리스트 컴프리헨션, 딕셔너리 컴프리헨션, 세트 컴프리헨션의 사용법을 다양한 예제와 함께 살펴보고, 제너레이터 표현식에 대해서도 간략히 소개하겠습니다.

10.1. 리스트 컴프리헨션 (List Comprehensions)

리스트 컴프리헨션은 기존 리스트나 다른 반복 가능한 객체를 기반으로 새로운 리스트를 만드는 가장 일반적인 유형의 컴프리헨션입니다.

10.1.1. 기본 구문

기본적인 리스트 컴프리헨션의 구문은 다음과 같습니다:

new_list = [표현식 for 항목 in 반복_가능한_객체]

예시 코드: 기본 리스트 컴프리헨션

# 0부터 9까지 숫자의 제곱으로 이루어진 리스트 생성
# 일반적인 for 루프 사용
squares_loop = []
for x in range(10):
    squares_loop.append(x**2)
print("For 루프 사용:", squares_loop)

# 리스트 컴프리헨션 사용
squares_comp = [x**2 for x in range(10)]
print("리스트 컴프리헨션 사용:", squares_comp)

# 문자열 리스트에서 각 문자열의 길이를 담은 리스트 생성
words = ["apple", "banana", "cherry", "date"]
lengths = [len(word) for word in words]
print("단어 길이 리스트:", lengths)

# 문자열의 각 글자를 대문자로 바꾼 리스트 생성
greeting = "hello"
upper_chars = [char.upper() for char in greeting]
print("대문자 글자 리스트:", upper_chars)

예상 결과:

For 루프 사용: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
리스트 컴프리헨션 사용: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
단어 길이 리스트: [5, 6, 6, 4]
대문자 글자 리스트: ['H', 'E', 'L', 'L', 'O']

10.1.2. 조건문(if) 추가하기

리스트 컴프리헨션에는 if 조건을 추가하여 특정 조건을 만족하는 항목에 대해서만 표현식을 적용하고 새 리스트에 포함시킬 수 있습니다.

new_list = [표현식 for 항목 in 반복_가능한_객체 if 조건문]

예시 코드: if 조건이 있는 리스트 컴프리헨션

# 0부터 9까지의 숫자 중 짝수만 제곱하여 리스트 생성
even_squares = [x**2 for x in range(10) if x % 2 == 0]
print("짝수 제곱 리스트:", even_squares)

# 문자열 리스트에서 길이가 5 이상인 단어만 선택
long_words = [word for word in ["apple", "banana", "kiwi", "strawberry", "fig"] if len(word) >= 5]
print("길이가 5 이상인 단어:", long_words)

# 1부터 30까지의 숫자 중 3의 배수이면서 5의 배수인 숫자 리스트
multiples_3_and_5 = [num for num in range(1, 31) if num % 3 == 0 if num % 5 == 0] # 여러 if 가능
# 또는 multiples_3_and_5 = [num for num in range(1, 31) if num % 3 == 0 and num % 5 == 0]
print("3과 5의 공배수 (1~30):", multiples_3_and_5)

예상 결과:

짝수 제곱 리스트: [0, 4, 16, 36, 64]
길이가 5 이상인 단어: ['apple', 'banana', 'strawberry']
3과 5의 공배수 (1~30): [15, 30]

10.1.3. 중첩 for 문 사용하기

리스트 컴프리헨션 안에는 여러 개의 for 문을 중첩하여 사용할 수 있습니다. for 문은 왼쪽에서 오른쪽 순서로 중첩됩니다.

new_list = [표현식 for 항목1 in 반복_객체1 for 항목2 in 반복_객체2 ...]

예시 코드: 중첩 for문이 있는 리스트 컴프리헨션

# 두 리스트의 모든 요소 조합 (쌍) 만들기
list1 = ['a', 'b']
list2 = [1, 2, 3]
combinations = [(x, y) for x in list1 for y in list2]
print("모든 조합:", combinations) # [('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3)]

# 중첩된 리스트를 하나의 리스트로 펼치기 (flatten)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_list = [num for row in matrix for num in row]
print("펼쳐진 리스트:", flattened_list) # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 구구단 결과 리스트 (2단, 3단)
multiplication_table = [f"{i}x{j}={i*j}" for i in range(2, 4) for j in range(1, 10)]
# print("구구단 결과:", multiplication_table) # 너무 길어서 일부만 생각
print("구구단 2단 일부:", [f"2x{j}={2*j}" for j in range(1,4)])

예상 결과:

모든 조합: [('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3)]
펼쳐진 리스트: [1, 2, 3, 4, 5, 6, 7, 8, 9]
구구단 2단 일부: ['2x1=2', '2x2=4', '2x3=6']

중첩 for 문과 if 조건을 함께 사용할 수도 있습니다. if 조건은 관련된 for 문 뒤에 위치할 수 있습니다.

10.1.4. if-else 조건 표현식 사용하기

리스트 컴프리헨션의 표현식 부분에서 if-else 조건 표현식(삼항 연산자)을 사용하여 조건에 따라 다른 값을 리스트에 포함시킬 수 있습니다.

new_list = [참일_때_표현식 if 조건문 else 거짓일_때_표현식 for 항목 in 반복_가능한_객체]

주의:if-else는 필터링을 위한 if 조건문과는 위치와 역할이 다릅니다. 필터링 iffor ... in ... 뒤에 오지만, 표현식 내의 if-elsefor 앞에 위치하며, 모든 항목에 대해 값을 생성하되 조건에 따라 다른 값을 생성합니다.

예시 코드: if-else 조건 표현식을 사용한 리스트 컴프리헨션

numbers = [1, 2, 3, 4, 5, 6]
# 숫자가 짝수이면 "짝수", 홀수이면 "홀수" 문자열을 리스트에 담기
odd_even_labels = ["짝수" if num % 2 == 0 else "홀수" for num in numbers]
print("홀짝 레이블:", odd_even_labels)

# 0보다 크면 그대로, 아니면 0으로 치환하는 리스트
values = [-1, 0, 5, -10, 7]
non_negative_values = [x if x > 0 else 0 for x in values]
print("음수 아닌 값들:", non_negative_values)

예상 결과:

홀짝 레이블: ['홀수', '짝수', '홀수', '짝수', '홀수', '짝수']
음수 아닌 값들: [0, 0, 5, 0, 7]

10.2. 딕셔너리 컴프리헨션 (Dictionary Comprehensions)

딕셔너리 컴프리헨션은 리스트 컴프리헨션과 유사하게, 기존 반복 가능한 객체로부터 새로운 딕셔너리를 생성합니다.

10.2.1. 기본 구문

new_dict = {키_표현식: 값_표현식 for 항목 in 반복_가능한_객체}

리스트 컴프리헨션과 마찬가지로 if 조건을 추가하거나 중첩 for 문을 사용할 수 있습니다.

예시 코드: 딕셔너리 컴프리헨션

# 숫자와 그 제곱을 키-값으로 하는 딕셔너리 생성
squared_dict = {x: x**2 for x in range(5)}
print("제곱 딕셔너리:", squared_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 두 리스트를 사용하여 딕셔너리 생성
keys = ["name", "age", "city"]
values = ["Alice", 30, "New York"]
person_dict = {keys[i]: values[i] for i in range(len(keys))}
# 더 파이썬스러운 방법: person_dict = {k: v for k, v in zip(keys, values)}
print("zip 사용 딕셔너리:", {k: v for k, v in zip(keys, values)})

# 문자열에서 각 글자의 빈도수 계산 (간단한 예, collections.Counter가 더 적합)
text = "hello python"
char_counts = {char: text.count(char) for char in set(text)} # set(text)로 중복 문자 제거 후 계산
print("문자 빈도수:", char_counts)

# 특정 조건을 만족하는 항목만으로 딕셔너리 생성
original_scores = {"math": 90, "english": 75, "science": 88, "history": 60}
passed_scores = {subject: score for subject, score in original_scores.items() if score >= 80}
print("80점 이상 과목:", passed_scores)

예상 결과 (딕셔너리 순서는 Python 3.7+에서 삽입 순서 유지):

제곱 딕셔너리: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
zip 사용 딕셔너리: {'name': 'Alice', 'age': 30, 'city': 'New York'}
문자 빈도수: {'h': 2, 'l': 2, 'n': 1, 'p': 1, 't': 1, ' ': 1, 'o': 2, 'e': 1, 'y': 1} (순서 다를 수 있음)
80점 이상 과목: {'math': 90, 'science': 88}

10.3. 세트 컴프리헨션 (Set Comprehensions)

세트 컴프리헨션은 중괄호({})를 사용한다는 점에서 딕셔너리 컴프리헨션과 유사하지만, 키-값 쌍 대신 단일 표현식만 사용하여 세트의 고유한 요소들을 생성합니다.

10.3.1. 기본 구문

new_set = {표현식 for 항목 in 반복_가능한_객체}

마찬가지로 if 조건이나 중첩 for 문을 사용할 수 있습니다.

예시 코드: 세트 컴프리헨션

# 리스트에서 중복을 제거하고 각 요소의 제곱으로 세트 생성
numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5]
unique_squares_set = {x**2 for x in numbers}
print("고유 제곱 세트:", unique_squares_set) # {1, 4, 9, 16, 25} (순서 다를 수 있음)

# 문자열에서 모음만 추출하여 세트 생성 (중복 없이)
vowels_in_string = {char for char in "programming is fun" if char in "aeiou"}
print("문자열 내 모음:", vowels_in_string) # {'o', 'i', 'u', 'a'} (순서 다를 수 있음)

# 0부터 9까지의 짝수만 포함하는 세트
even_numbers_set = {x for x in range(10) if x % 2 == 0}
print("짝수 세트:", even_numbers_set)

예상 결과 (세트 요소의 출력 순서는 다를 수 있음):

고유 제곱 세트: {1, 4, 9, 16, 25}
문자열 내 모음: {'u', 'i', 'a', 'o'}
짝수 세트: {0, 2, 4, 6, 8}

10.4. 제너레이터 표현식 (Generator Expressions) - 간략 소개

제너레이터 표현식은 리스트 컴프리헨션과 매우 유사한 구문을 가지지만, 대괄호([]) 대신 소괄호(())를 사용합니다. 가장 큰 차이점은 제너레이터 표현식이 즉시 리스트나 세트 같은 컬렉션을 메모리에 생성하는 것이 아니라, 필요할 때마다 항목을 하나씩 생성하는 '제너레이터 객체(generator object)'를 반환한다는 것입니다.

generator_obj = (표현식 for 항목 in 반복_가능한_객체 if 조건문)

제너레이터 표현식은 다음과 같은 특징과 장점이 있습니다:

제너레이터 표현식은 sum(), min(), max(), list(), tuple(), set() 등 이터레이터를 인자로 받는 함수들과 함께 자주 사용됩니다. 제너레이터에 대한 자세한 내용은 다음 장에서 다룹니다.

예시 코드: 제너레이터 표현식

# 0부터 999까지의 제곱수를 생성하는 제너레이터
large_squares_gen = (x**2 for x in range(1000)) 
print("제너레이터 객체:", large_squares_gen) # 리스트가 아닌 제너레이터 객체 출력
# print("첫 5개 제곱수:", list(large_squares_gen)[:5]) # 이렇게 하면 제너레이터가 소모됨

# 제너레이터를 사용하여 합계 계산 (메모리에 모든 제곱수를 저장하지 않음)
sum_of_squares = sum(x**2 for x in range(1, 101)) # 1부터 100까지 제곱의 합
print("1~100 제곱의 합:", sum_of_squares)

# 제너레이터 객체는 한 번만 순회 가능
gen_expr = (i for i in range(3))
print("첫 번째 순회:", list(gen_expr)) # [0, 1, 2]
print("두 번째 순회:", list(gen_expr)) # [] (이미 소모됨)

예상 결과:

제너레이터 객체: <generator object <genexpr> at 0x...> (메모리 주소는 다를 수 있음)
1~100 제곱의 합: 338350
첫 번째 순회: [0, 1, 2]
두 번째 순회: []

10.5. 컴프리헨션 사용 시 장단점 및 고려 사항

"파이썬에서는 한 가지, 그리고 가능하면 명확한 한 가지 방법만이 존재해야 한다 (There should be one-- and preferably only one --obvious way to do it.)" - 파이썬의 선(Zen of Python) 중. 컴프리헨션은 강력하지만, 항상 최선의 선택은 아닐 수 있습니다. 코드의 명확성과 유지보수성을 최우선으로 고려하여 적절한 도구를 선택하는 것이 중요합니다.

이번 장에서는 파이썬의 강력한 기능인 컴프리헨션을 사용하여 리스트, 딕셔너리, 세트를 간결하고 효율적으로 생성하는 방법을 배웠습니다. 컴프리헨션은 파이썬다운(Pythonic) 코드를 작성하는 데 중요한 부분이며, 적절히 사용하면 코드의 질을 크게 향상시킬 수 있습니다. 다음 장에서는 데이터의 순차적 처리를 위한 이터레이터와 메모리 효율적인 데이터 생성을 위한 제너레이터에 대해 더 자세히 알아보겠습니다.

제 11 장: 이터레이터와 제너레이터

파이썬에서 반복(iteration)은 데이터를 순차적으로 하나씩 처리하는 매우 일반적인 작업입니다. for 루프는 이러한 반복 작업을 간편하게 수행할 수 있도록 해주는데, 그 내부 동작 원리에는 '이터레이션 프로토콜(iteration protocol)'이라는 중요한 개념이 숨어 있습니다. 이터레이션 프로토콜을 이해하면 파이썬의 반복 메커니즘을 더 깊이 있게 활용할 수 있으며, 직접 사용자 정의 이터레이터를 만들 수도 있습니다.

또한, 대량의 데이터를 다루거나 무한한 시퀀스를 표현해야 할 때 메모리 효율성이 중요한 문제가 됩니다. 제너레이터(Generator)는 이러한 상황에서 매우 유용한 기능으로, 필요할 때마다 값을 하나씩 생성하여 메모리를 효율적으로 사용하면서도 반복 가능한 객체를 만들 수 있게 해줍니다. 이번 장에서는 이터레이션 프로토콜의 핵심인 이터러블(iterable)과 이터레이터(iterator), 그리고 이터레이터를 쉽게 만드는 방법인 제너레이터 함수와 제너레이터 표현식에 대해 자세히 알아보겠습니다.

11.1. 이터레이션 프로토콜 (Iteration Protocol)

파이썬에서 for 루프가 동작하는 방식은 이터레이션 프로토콜을 기반으로 합니다. 이 프로토콜은 두 가지 주요 개념으로 구성됩니다: '이터러블(iterable)'과 '이터레이터(iterator)'.

11.1.1. 반복 가능한 객체 (Iterable)

이터러블은 자신의 멤버들을 한 번에 하나씩 반환할 수 있는 객체를 의미합니다. 즉, for 루프의 in 뒤에 올 수 있는 객체들이 바로 이터러블입니다. 리스트, 튜플, 문자열, 딕셔너리, 세트, 파일 객체, range() 객체 등이 대표적인 이터러블입니다.

어떤 객체가 이터러블이 되기 위해서는 다음 두 가지 중 하나를 만족해야 합니다:

주로 __iter__() 메서드를 구현하는 것이 이터러블의 표준적인 방법입니다. 이 __iter__() 메서드는 '이터레이터' 객체를 반환해야 합니다.

11.1.2. 이터레이터 (Iterator)

이터레이터는 값들의 스트림(흐름)을 나타내는 객체입니다. 이터레이터는 한 번에 하나의 데이터 요소만을 반환하며, 현재 스트림 내에서의 위치를 기억합니다. 이터레이터의 핵심은 __next__() 메서드입니다.

이터레이터 객체는 다음 두 가지 메서드를 반드시 구현해야 합니다:

11.1.3. iter()next() 내장 함수

예시 코드: 이터러블과 이터레이터, iter(), next()

my_list = [10, 20, 30] # my_list는 이터러블 객체

# 1. iter() 함수로 이터레이터 객체 얻기
my_iterator = iter(my_list) # my_list.__iter__() 호출과 동일
print("이터레이터 타입:", type(my_iterator))

# 2. next() 함수로 이터레이터에서 값 꺼내기
print("첫 번째 값:", next(my_iterator)) # my_iterator.__next__() 호출
print("두 번째 값:", next(my_iterator))
print("세 번째 값:", next(my_iterator))
# print("네 번째 값:", next(my_iterator)) # StopIteration 예외 발생

# for 루프의 내부 동작 방식 (개념적으로)
print("\\nfor 루프의 내부 동작 (개념):")
numbers = [1, 2]
iterator_for_loop = iter(numbers) # 1. 이터레이터 얻기
while True:
    try:
        item = next(iterator_for_loop) # 2. 다음 항목 가져오기
        print(item)                    # 3. 항목 처리
    except StopIteration:              # 4. StopIteration 발생 시 루프 종료
        break

예상 결과:

이터레이터 타입: <class 'list_iterator'>
첫 번째 값: 10
두 번째 값: 20
세 번째 값: 30
(네 번째 next() 호출 시 StopIteration 발생 주석 처리됨)

for 루프의 내부 동작 (개념):
1
2

11.2. 사용자 정의 이터레이터 만들기

클래스를 작성하여 __iter__()__next__() 메서드를 직접 구현함으로써 사용자 정의 이터레이터를 만들 수 있습니다. 이는 특정 규칙에 따라 연속적인 값을 생성하거나, 복잡한 데이터 구조를 순회하는 로직을 캡슐화할 때 유용합니다.

예시 코드: 간단한 카운터 이터레이터 클래스

class Counter:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        # 이터레이터 객체는 자기 자신을 반환
        return self

    def __next__(self):
        if self.current <= self.end:
            value = self.current
            self.current += 1
            return value
        else:
            # 더 이상 반환할 값이 없으면 StopIteration 예외 발생
            raise StopIteration

# Counter 이터레이터 사용
my_counter = Counter(1, 3)

# my_counter는 이터러블이면서 동시에 이터레이터입니다.
print("Counter 사용 (next 호출):")
print(next(my_counter)) # 1
print(next(my_counter)) # 2
print(next(my_counter)) # 3
# print(next(my_counter)) # StopIteration

# for 루프에서도 사용 가능
print("\\nCounter 사용 (for 루프):")
for num in Counter(5, 7):
    print(num)

예상 결과:

Counter 사용 (next 호출):
1
2
3
(네 번째 next() 호출 시 StopIteration 발생 주석 처리됨)

Counter 사용 (for 루프):
5
6
7

11.3. 제너레이터란? (What are Generators?)

제너레이터는 이터레이터를 생성하는 간단하고 강력한 방법입니다. 모든 제너레이터는 이터레이터이지만, 모든 이터레이터가 제너레이터인 것은 아닙니다. 제너레이터는 사용자 정의 이터레이터 클래스를 직접 작성하는 것보다 훨씬 간결한 코드로 이터레이터를 구현할 수 있게 해줍니다.

제너레이터는 두 가지 형태로 만들 수 있습니다:

제너레이터의 핵심은 값을 한 번에 하나씩 '생성(generate)'하여 반환하고, 다음 값 요청이 있을 때까지 함수의 현재 상태(지역 변수, 실행 위치 등)를 기억하고 유지한다는 점입니다. 이를 통해 메모리를 매우 효율적으로 사용할 수 있습니다.

11.4. 제너레이터 함수 (Generator Functions) - yield 키워드

함수 본문 어딘가에 yield 문을 포함하면, 그 함수는 일반 함수가 아닌 제너레이터 함수가 됩니다. 제너레이터 함수를 호출하면 함수 코드가 바로 실행되는 것이 아니라, 제너레이터 객체(이터레이터의 일종)가 반환됩니다.

yield 키워드의 동작 방식:

예시 코드: 제너레이터 함수

def simple_generator(n):
    print("제너레이터 시작")
    for i in range(n):
        print(f"{i} 값을 yield 하기 직전")
        yield i # 값을 반환하고 여기서 실행 일시 중지
        print(f"{i} 값을 yield 한 직후")
    print("제너레이터 종료")

# 제너레이터 함수 호출 -> 제너레이터 객체 반환
gen_obj = simple_generator(3)
print("제너레이터 객체:", gen_obj)

print("\\nnext()로 값 가져오기:")
try:
    print("첫 번째 next():", next(gen_obj))
    print("두 번째 next():", next(gen_obj))
    print("세 번째 next():", next(gen_obj))
    # print("네 번째 next():", next(gen_obj)) # StopIteration
except StopIteration:
    print("StopIteration 발생")

# 새로운 제너레이터 객체로 for 루프 사용
print("\\nfor 루프로 값 가져오기:")
for value in simple_generator(2): # 호출 시마다 새로운 제너레이터 객체 생성
    print(f"for 루프에서 받은 값: {value}")

# 무한 시퀀스를 생성하는 제너레이터 (예: 피보나치 수열)
def fibonacci_generator():
    a, b = 0, 1
    while True: # 무한 루프
        yield a
        a, b = b, a + b

print("\\n피보나치 수열 제너레이터:")
fib_gen = fibonacci_generator()
for _ in range(10): # 처음 10개 피보나치 수 출력
    print(next(fib_gen), end=" ")
print()

예상 결과:

제너레이터 객체: <generator object simple_generator at 0x...>

next()로 값 가져오기:
제너레이터 시작
0 값을 yield 하기 직전
첫 번째 next(): 0
0 값을 yield 한 직후
1 값을 yield 하기 직전
두 번째 next(): 1
1 값을 yield 한 직후
2 값을 yield 하기 직전
세 번째 next(): 2
2 값을 yield 한 직후
제너레이터 종료
StopIteration 발생 (또는 네 번째 next() 주석 해제 시 여기서 발생)

for 루프로 값 가져오기:
제너레이터 시작
0 값을 yield 하기 직전
for 루프에서 받은 값: 0
0 값을 yield 한 직후
1 값을 yield 하기 직전
for 루프에서 받은 값: 1
1 값을 yield 한 직후
제너레이터 종료

피보나치 수열 제너레이터:
0 1 1 2 3 5 8 13 21 34 

11.5. 제너레이터 표현식 (Generator Expressions)

제너레이터 표현식은 리스트 컴프리헨션과 문법이 매우 유사하지만, 대괄호 [] 대신 소괄호 ()를 사용합니다. 제너레이터 함수처럼, 제너레이터 표현식도 호출 즉시 모든 값을 계산하여 메모리에 저장하는 것이 아니라, 필요할 때마다 값을 하나씩 생성하는 제너레이터 객체를 반환합니다.

(표현식 for 항목 in 반복_가능한_객체 if 조건문)

제너레이터 표현식은 주로 함수의 인자로 전달될 때 소괄호를 생략하고 사용되기도 합니다. (예: sum(x*x for x in range(10)))

예시 코드: 제너레이터 표현식

# 리스트 컴프리헨션 (모든 값을 메모리에 저장)
list_comp = [x**2 for x in range(5)] 
print("리스트 컴프리헨션 결과:", list_comp)

# 제너레이터 표현식 (제너레이터 객체 반환)
gen_expr = (x**2 for x in range(5))
print("제너레이터 표현식 객체:", gen_expr)

print("제너레이터 표현식 결과 (list()로 변환):", list(gen_expr)) # 이때 실제 값들이 생성됨
# gen_expr은 이미 소모되었으므로 다시 list(gen_expr)하면 빈 리스트 반환

# 파일에서 특정 단어가 포함된 줄만 처리하는 예 (메모리 효율적)
# with open("large_file.txt", "r") as f:
#     lines_with_keyword = (line for line in f if "keyword" in line)
#     for L in lines_with_keyword:
#         process(L) # 각 줄을 처리하는 함수

# 함수의 인자로 사용될 때 괄호 생략 가능
sum_of_cubes = sum(x**3 for x in range(1, 6)) # 1^3 + 2^3 + ... + 5^3
print("1~5 세제곱의 합:", sum_of_cubes)

예상 결과:

리스트 컴프리헨션 결과: [0, 1, 4, 9, 16]
제너레이터 표현식 객체: <generator object <genexpr> at 0x...>
제너레이터 표현식 결과 (list()로 변환): [0, 1, 4, 9, 16]
1~5 세제곱의 합: 225

11.6. 제너레이터의 장점 및 사용 사례

yield from 표현식 (간략 소개)

파이썬 3.3부터 도입된 yield from 표현식은 하나의 제너레이터가 다른 제너레이터(또는 이터러블)에게 반복 작업을 위임할 수 있게 해줍니다. 즉, yield from other_iterableother_iterable의 모든 값을 하나씩 yield하는 것과 같습니다. 이는 제너레이터를 연결하거나 중첩된 루프를 단순화하는 데 유용합니다.

예시 코드: yield from

def sub_generator():
    yield 10
    yield 20

def main_generator():
    yield "시작"
    yield from sub_generator() # sub_generator의 모든 yield 값을 전달
    yield from [30, 40]        # 리스트의 모든 값도 전달 가능
    yield "끝"

for item in main_generator():
    print(item)

예상 결과:

시작
10
20
30
40
끝

이번 장에서는 파이썬의 이터레이션 프로토콜과 이를 쉽게 구현할 수 있는 제너레이터에 대해 깊이 있게 알아보았습니다. 이터레이터와 제너레이터는 데이터를 순차적으로 처리하고, 특히 대량의 데이터를 메모리 효율적으로 다루는 데 있어 매우 강력하고 필수적인 도구입니다. 이러한 개념을 잘 이해하고 활용하면 더욱 정교하고 성능 좋은 파이썬 프로그램을 작성할 수 있습니다. 다음 장에서는 기존 함수의 코드를 수정하지 않고 기능을 추가하거나 변경할 수 있게 해주는 데코레이터에 대해 간략히 소개하겠습니다.

제 12 장: 데코레이터 (간략 소개)

데코레이터(Decorator)는 파이썬의 고급 기능 중 하나로, 기존 함수의 코드를 직접 수정하지 않으면서 해당 함수에 새로운 기능을 추가하거나, 함수의 행동을 변경하고자 할 때 사용되는 디자인 패턴입니다. 데코레이터는 함수를 인자로 받아서, 추가적인 기능을 덧씌운 새로운 함수를 반환하는 함수(또는 다른 호출 가능한 객체)입니다. 이는 코드의 재사용성을 높이고, 관심사의 분리(separation of concerns)를 통해 프로그램을 더 깔끔하게 구성하는 데 도움을 줍니다.

파이썬에서 함수는 일급 객체(first-class object)이기 때문에 함수를 다른 함수의 인자로 전달하거나, 함수 내에서 새로운 함수를 정의하고 반환하는 것이 가능합니다. 이러한 특징이 데코레이터를 구현하는 핵심 원리가 됩니다. 이 장에서는 데코레이터의 기본 개념과 간단한 사용 예를 간략히 소개합니다.

12.1. 데코레이터란 무엇인가?

데코레이터는 이름 그대로 기존 함수를 '장식(decorate)'하는 역할을 합니다. 마치 선물 포장지에 리본을 추가하여 선물을 더 특별하게 만드는 것처럼, 데코레이터는 원래 함수의 핵심 기능은 그대로 유지하면서 부가적인 기능을 '덧씌웁니다'.

주로 다음과 같은 상황에서 유용하게 사용됩니다:

데코레이터는 @ 기호를 사용하여 간결하게 적용할 수 있으며, 이를 '문법 설탕(syntactic sugar)'이라고도 부릅니다.

12.2. 데코레이터의 기본 구조 및 만들기

가장 기본적인 데코레이터는 다음과 같은 구조를 가집니다:

  1. 데코레이터 함수는 꾸며줄 함수(원래 함수)를 인자로 받습니다.
  2. 데코레이터 함수 내부에 '래퍼(wrapper)' 함수를 정의합니다. 이 래퍼 함수가 실제로 원래 함수를 감싸는 역할을 합니다.
  3. 래퍼 함수는 원래 함수가 받을 수 있는 모든 인자를 처리할 수 있도록 *args**kwargs를 사용하는 것이 일반적입니다.
  4. 래퍼 함수 내부에서는 원래 함수 호출 전후에 필요한 부가 기능을 수행합니다.
  5. 래퍼 함수는 원래 함수의 실행 결과를 반환해야 합니다 (원래 함수가 반환 값이 있다면).
  6. 데코레이터 함수는 이 래퍼 함수를 반환합니다.

예시 코드: 간단한 데코레이터 만들기 및 사용

# 1. 데코레이터 함수 정의
def simple_decorator(func_to_decorate):
    """간단한 데코레이터: 함수 실행 전후에 메시지를 출력합니다."""
    
    # 2. 래퍼 함수 정의
    def wrapper_function(*args, **kwargs): # 3. 모든 인자 처리
        print(f"'{func_to_decorate.__name__}' 함수 실행 전입니다.") # 4. 부가 기능 (전)
        
        result = func_to_decorate(*args, **kwargs) # 원래 함수 호출
        
        print(f"'{func_to_decorate.__name__}' 함수 실행 후입니다.") # 4. 부가 기능 (후)
        return result # 5. 원래 함수의 결과 반환
    
    return wrapper_function # 6. 래퍼 함수 반환

# 데코레이터 적용 방법 1: @ 구문 사용 (권장)
@simple_decorator
def say_hello(name):
    """주어진 이름에게 인사하는 함수입니다."""
    print(f"안녕하세요, {name}님!")
    return f"인사 완료: {name}"

@simple_decorator
def add_numbers(a, b):
    """두 숫자를 더하는 함수입니다."""
    print(f"{a} + {b} 계산 중...")
    return a + b

# 데코레이터가 적용된 함수 호출
hello_result = say_hello("홍길동")
print(f"say_hello 반환 값: {hello_result}")

print("-" * 30)

sum_result = add_numbers(10, 20)
print(f"add_numbers 반환 값: {sum_result}")

# 데코레이터 적용 방법 2: 수동 적용 (내부 동작 이해용)
# def greet_manually():
#     print("수동으로 인사합니다!")
#
# decorated_greet = simple_decorator(greet_manually) # 데코레이터 함수에 원래 함수 전달
# decorated_greet() # 데코레이터가 반환한 래퍼 함수 호출

예상 결과:

'say_hello' 함수 실행 전입니다.
안녕하세요, 홍길동님!
'say_hello' 함수 실행 후입니다.
say_hello 반환 값: 인사 완료: 홍길동
------------------------------
'add_numbers' 함수 실행 전입니다.
10 + 20 계산 중...
'add_numbers' 함수 실행 후입니다.
add_numbers 반환 값: 30

위 예제에서 @simple_decoratorsay_hello = simple_decorator(say_hello)와 동일하게 동작합니다. 즉, say_hello라는 이름은 이제 simple_decorator가 반환한 wrapper_function을 가리키게 됩니다.

12.3. functools.wraps 사용하기

데코레이터를 사용하면 유용하지만, 한 가지 문제점이 있습니다. 데코레이터가 적용된 함수의 메타데이터(예: 함수의 이름 __name__, 독스트링 __doc__ 등)가 원래 함수의 것이 아니라 래퍼 함수의 것으로 변경된다는 점입니다. 이는 디버깅이나 문서화 도구 사용 시 혼란을 줄 수 있습니다.

이러한 문제를 해결하기 위해 파이썬 표준 라이브러리인 functools 모듈의 wraps 데코레이터를 사용할 수 있습니다. @functools.wraps(func_to_decorate)를 래퍼 함수 정의 바로 위에 적용하면, 래퍼 함수가 원래 함수의 메타데이터를 유지하도록 도와줍니다.

예시 코드: functools.wraps 사용

import functools # functools 모듈 임포트

def proper_decorator(func_to_decorate):
    """functools.wraps를 사용하여 메타데이터를 보존하는 데코레이터입니다."""
    
    @functools.wraps(func_to_decorate) # 여기에 적용!
    def wrapper_function(*args, **kwargs):
        print(f"'{func_to_decorate.__name__}' (wrapper 실행 전) - 메타데이터 보존됨!")
        result = func_to_decorate(*args, **kwargs)
        print(f"'{func_to_decorate.__name__}' (wrapper 실행 후) - 메타데이터 보존됨!")
        return result
    return wrapper_function

@proper_decorator
def greet_with_metadata(name):
    """이것은 greet_with_metadata 함수의 독스트링입니다."""
    print(f"반갑습니다, {name}님!")

# 데코레이터 적용 후 함수 메타데이터 확인
print(f"함수 이름: {greet_with_metadata.__name__}")   # 'greet_with_metadata' (wraps 사용 시)
                                                    # 만약 wraps를 사용하지 않으면 'wrapper_function'이 됨
print(f"함수 독스트링: {greet_with_metadata.__doc__}") # 원래 함수의 독스트링 (wraps 사용 시)

print("-" * 30)
greet_with_metadata("이순신")

예상 결과:

함수 이름: greet_with_metadata
함수 독스트링: 이것은 greet_with_metadata 함수의 독스트링입니다.
------------------------------
'greet_with_metadata' (wrapper 실행 전) - 메타데이터 보존됨!
반갑습니다, 이순신님!
'greet_with_metadata' (wrapper 실행 후) - 메타데이터 보존됨!

항상 데코레이터를 작성할 때는 @functools.wraps를 사용하여 원래 함수의 정보를 보존하는 것이 좋은 습관입니다.

12.4. 데코레이터의 추가적인 측면 (간략 언급)

데코레이터는 이 장에서 소개한 내용보다 훨씬 더 다양한 형태로 활용될 수 있습니다. 예를 들어:

이러한 고급 주제들은 파이썬을 더 깊이 있게 학습하면서 접하게 될 것입니다. 이 장의 목표는 데코레이터의 기본적인 개념과 "왜 사용하는지", 그리고 "간단한 것은 어떻게 만드는지"에 대한 이해를 돕는 것입니다.

이번 장에서는 기존 코드 변경 없이 함수의 기능을 확장하거나 수정할 수 있는 파이썬의 강력한 기능인 데코레이터에 대해 간략히 알아보았습니다. 데코레이터는 코드의 중복을 줄이고, 가독성을 높이며, 다양한 부가 기능을 모듈화하는 데 매우 유용합니다. 다음 장에서는 프로젝트별로 독립적인 파이썬 실행 환경을 구성하여 패키지 의존성 문제를 해결하는 가상 환경에 대해 알아보겠습니다.

제 13 장: 가상 환경 (Virtual Environments)

파이썬으로 여러 프로젝트를 진행하다 보면, 각 프로젝트마다 필요로 하는 라이브러리(패키지)의 종류나 버전이 다른 경우가 많습니다. 예를 들어, 어떤 프로젝트는 특정 라이브러리의 1.0 버전을 사용해야 하고, 다른 프로젝트는 동일 라이브러리의 2.0 버전을 필요로 할 수 있습니다. 만약 모든 라이브러리를 시스템 전역에 설치한다면 이러한 버전 충돌 문제를 해결하기 어렵고, 프로젝트 간 의존성이 꼬일 수 있습니다.

가상 환경(Virtual Environment)은 이러한 문제를 해결하기 위해 프로젝트별로 독립적인 파이썬 실행 환경을 만들어주는 도구입니다. 각 가상 환경은 자신만의 파이썬 인터프리터(또는 그 복사본/심볼릭 링크)와 설치된 라이브러리들을 가지므로, 프로젝트 간섭 없이 각 프로젝트에 맞는 환경을 구성할 수 있습니다. 이번 장에서는 가상 환경의 필요성과 파이썬에 내장된 `venv` 모듈을 사용하여 가상 환경을 만들고 관리하는 기본적인 방법을 간략히 알아보겠습니다.

13.1. 가상 환경이란 무엇인가?

가상 환경은 특정 프로젝트를 위해 독립적으로 격리된 파이썬 환경입니다. 이 환경 안에는 해당 프로젝트에서 사용할 파이썬 인터프리터와 특정 버전의 라이브러리들이 설치됩니다. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:

즉, 가상 환경은 "이 프로젝트는 이 버전의 파이썬과 이 버전의 라이브러리들로만 돌아간다"는 것을 보장해주는 작업 공간과 같습니다.

13.2. `venv` 모듈 소개

파이썬 3.3 버전부터 표준 라이브러리에 venv라는 모듈이 포함되어 가상 환경 생성을 지원합니다. 이전에는 virtualenv라는 외부 패키지를 주로 사용했지만, 기본적인 가상 환경 관리에는 venv만으로도 충분합니다.

venv는 지정된 디렉터리에 가상 환경을 만들며, 이 디렉터리 안에는 현재 사용 중인 파이썬 인터프리터의 복사본(또는 심볼릭 링크)과 표준 라이브러리의 일부, 그리고 가상 환경에 설치될 패키지들을 위한 공간(site-packages 디렉터리)이 생성됩니다.

13.3. `venv`를 사용한 가상 환경 생성, 활성화, 비활성화

venv를 사용하는 기본적인 단계는 다음과 같습니다.

13.3.1. 가상 환경 생성하기

터미널(Windows에서는 명령 프롬프트 또는 PowerShell, macOS/Linux에서는 터미널)에서 다음 명령을 사용하여 가상 환경을 생성합니다:

python -m venv 가상환경_이름

여기서 python은 시스템에 설치된 파이썬 3 실행 명령어입니다 (시스템에 따라 python3일 수도 있습니다). 가상환경_이름은 생성될 가상 환경 디렉터리의 이름입니다. 일반적으로 프로젝트 디렉터리 내에 .venv 또는 venv와 같은 이름으로 생성하고, 이 디렉터리는 버전 관리 시스템(예: Git)의 .gitignore 파일에 추가하여 저장소에 포함되지 않도록 합니다.

예시 코드: 가상 환경 생성 (터미널 명령어)

# my_project 라는 디렉터리를 만들고 그 안으로 이동했다고 가정
# cd my_project

# .venv 라는 이름으로 가상 환경 생성
python -m venv .venv 
# 또는 python3 -m venv .venv (시스템에 따라)

위 명령을 실행하면 현재 디렉터리(my_project) 안에 .venv라는 하위 디렉터리가 생성되고, 그 안에 가상 환경 관련 파일들이 설치됩니다.

13.3.2. 가상 환경 활성화하기

생성된 가상 환경을 사용하려면 먼저 '활성화(activate)'해야 합니다. 활성화하면 현재 터미널 세션의 환경 변수(주로 PATH)가 변경되어, python이나 pip 같은 명령어를 실행할 때 시스템 전역의 것이 아닌 가상 환경 내의 것을 사용하게 됩니다. 일반적으로 활성화되면 터미널 프롬프트 앞에 가상 환경 이름이 표시됩니다.

예시 코드: 가상 환경 활성화 (터미널 명령어)

Windows (PowerShell 또는 CMD):

.\.venv\Scripts\activate

macOS / Linux (bash, zsh 등):

source .venv/bin/activate

활성화 후 터미널 프롬프트가 (.venv) C:\Users\YourUser\my_project> 또는 (.venv) youruser@hostname:~/my_project$ 와 같이 변경된 것을 볼 수 있습니다.

13.3.3. 가상 환경 비활성화하기

가상 환경에서의 작업을 마치고 원래 시스템 환경으로 돌아가려면 '비활성화(deactivate)'합니다. 활성화된 가상 환경의 터미널에서 다음 명령을 입력합니다:

deactivate

이 명령은 모든 플랫폼에서 동일하게 작동하며, 실행 후 터미널 프롬프트가 원래대로 돌아옵니다.

13.4. 가상 환경 내에서 패키지 관리

가상 환경이 활성화된 상태에서 pip를 사용하여 패키지를 설치하면, 해당 패키지는 현재 활성화된 가상 환경 내의 site-packages 디렉터리에 설치됩니다. 이는 시스템 전역의 파이썬 환경에는 영향을 주지 않습니다.

requirements.txt 파일로 의존성 관리

프로젝트에 필요한 패키지들과 그 버전들을 기록해두면 다른 환경에서도 동일한 개발 환경을 쉽게 구축할 수 있습니다. 이를 위해 requirements.txt라는 파일을 사용하는 것이 일반적입니다.

requirements.txt 파일은 프로젝트의 루트 디렉터리에 두고 버전 관리 시스템(Git 등)에 포함시켜 다른 팀원들과 공유하거나 배포 시 활용합니다.

예시 코드: 가상 환경 내 패키지 관리 (터미널 명령어)

# (가상 환경이 활성화된 상태라고 가정: 예: (.venv) 프롬프트)

# requests 패키지 설치
pip install requests

# 설치된 패키지 목록 확인
pip list 

# 현재 환경의 패키지 목록을 requirements.txt 파일로 저장
pip freeze > requirements.txt

# (다른 환경이나, 나중에 이 프로젝트를 다시 설정할 때)
# requirements.txt 파일로부터 모든 패키지 설치
# pip install -r requirements.txt

13.5. 가상 환경 사용의 일반적인 작업 흐름

  1. 새로운 프로젝트를 시작할 때 프로젝트 디렉터리를 만듭니다. (예: mkdir my_new_project && cd my_new_project)
  2. 프로젝트 디렉터리 내에 가상 환경을 생성합니다. (예: python -m venv .venv)
  3. 생성한 가상 환경을 활성화합니다. (예: source .venv/bin/activate 또는 .\.venv\Scripts\activate)
  4. pip를 사용하여 필요한 패키지들을 설치합니다.
  5. 프로젝트 개발을 진행합니다.
  6. 주기적으로 또는 프로젝트 완료 시점에 pip freeze > requirements.txt 명령으로 의존성 목록을 업데이트합니다.
  7. 작업이 끝나면 deactivate 명령으로 가상 환경을 비활성화합니다.
  8. 가상 환경 디렉터리(예: .venv/)는 .gitignore 파일에 추가하여 Git 저장소에 포함되지 않도록 합니다.

이번 장에서는 프로젝트별 독립적인 개발 환경을 구성하는 데 필수적인 가상 환경의 개념과 venv 모듈을 사용한 기본적인 관리 방법을 간략히 알아보았습니다. 가상 환경을 사용하면 패키지 의존성 문제를 효과적으로 해결하고, 협업 및 배포 시 일관된 환경을 유지하는 데 큰 도움이 됩니다. 다음 장에서는 파이썬 기초를 다진 후 나아갈 수 있는 다양한 분야와 추가 학습 자료들을 소개하겠습니다.

제 14 장: 다음 단계 및 추가 학습

지금까지 파이썬 프로그래밍의 기초부터 핵심 개념까지 성공적으로 학습하신 것을 축하드립니다! 이 가이드를 통해 변수, 자료 구조, 제어 흐름, 함수, 객체 지향 프로그래밍, 모듈과 패키지, 파일 처리, 예외 처리, 컴프리헨션, 이터레이터와 제너레이터, 데코레이터, 그리고 가상 환경까지 파이썬의 중요한 요소들을 두루 살펴보았습니다. 이 정도면 파이썬으로 간단한 프로그램을 작성하고, 더 복잡한 문제에 도전할 준비가 되었다고 할 수 있습니다.

하지만 프로그래밍의 세계는 넓고, 파이썬은 매우 다재다능한 언어이기에 여기서 멈추지 않고 끊임없이 배우고 탐험하는 자세가 중요합니다. 이 장에서는 여러분이 파이썬 실력을 한 단계 더 끌어올리고, 특정 전문 분야로 나아가는 데 도움이 될 만한 다양한 활용 분야, 관련 라이브러리, 그리고 효과적인 학습 전략들을 소개하고자 합니다.

14.1. 파이썬 활용 분야 심층 탐색

파이썬은 그 간결함과 강력한 라이브러리 생태계 덕분에 다양한 분야에서 핵심적인 프로그래밍 언어로 사용되고 있습니다. 여러분의 관심사와 목표에 맞는 분야를 선택하여 더 깊이 있는 학습을 진행해 보세요.

14.1.1. 웹 개발 (Web Development)

파이썬은 동적인 웹사이트나 웹 애플리케이션, API 서버를 구축하는 데 널리 사용됩니다. 주로 백엔드(서버 측 로직) 개발에 활용됩니다.

14.1.2. 데이터 과학 및 분석 (Data Science and Analysis)

파이썬은 데이터 수집, 정제, 처리, 분석, 시각화, 모델링 등 데이터 과학의 전 과정에서 가장 인기 있는 언어 중 하나입니다.

14.1.3. 머신러닝 및 인공지능 (Machine Learning and AI)

데이터 과학의 한 분야로, 데이터를 학습하여 예측 모델을 만들거나 패턴을 인식하는 기술입니다. 파이썬은 이 분야에서도 풍부한 라이브러리를 제공합니다.

14.1.4. 자동화 및 스크립팅 (Automation and Scripting)

파이썬은 반복적인 작업이나 시스템 관리, 데이터 처리 등을 자동화하는 스크립트 작성에 매우 효과적입니다.

14.1.5. GUI 데스크톱 애플리케이션 개발

파이썬으로 그래픽 사용자 인터페이스(GUI)를 갖춘 데스크톱 애플리케이션을 개발할 수도 있습니다.

14.1.6. 게임 개발 (Game Development)

파이썬으로 간단한 게임부터 좀 더 복잡한 게임까지 개발할 수 있습니다.

14.1.7. 기타 유망 분야

14.2. 실력 향상을 위한 추가 학습 전략

파이썬 실력을 꾸준히 향상시키기 위한 몇 가지 효과적인 학습 전략은 다음과 같습니다.

14.3. 맺음말: 끊임없는 배움의 여정

파이썬은 배우기 시작하기는 쉽지만, 그 깊이와 활용 범위는 매우 넓습니다. 이 가이드는 여러분이 파이썬이라는 강력한 도구를 사용하는 데 필요한 기본적인 지식과 방향을 제시해 드리고자 했습니다. 진정한 파이썬 전문가가 되기 위한 여정은 이제부터 시작입니다.

가장 중요한 것은 호기심을 잃지 않고 꾸준히 코딩하며, 실제 문제를 해결하는 데 파이썬을 적용해보는 것입니다. 작은 성공들을 쌓아가며 프로그래밍의 즐거움을 느끼고, 때로는 어려움에 부딪히더라도 포기하지 않고 해결책을 찾아나가는 과정 자체가 훌륭한 학습이 될 것입니다.

여러분의 파이썬 여정에 이 가이드가 작은 디딤돌이 되었기를 바랍니다. 끊임없는 배움과 탐구를 통해 훌륭한 파이썬 개발자로 성장하시기를 응원합니다!

- ~ : )
(with Gemini)

제 15 장: Python 연습 문제

지금까지 학습한 파이썬의 다양한 기능들을 종합적으로 활용해보는 연습 문제입니다. 각 문제의 요구사항을 잘 읽고 스스로 해결해본 후, '해답 보기'를 클릭하여 정답 코드와 비교해보세요. 직접 코드를 작성하고 실행해보는 과정에서 실력이 크게 향상될 것입니다. 💡

문제 1: 변수와 기본 연산

a) 두 정수를 사용자로부터 입력받아, 두 수의 합, 차, 곱, 나눗셈(소수점 결과), 몫, 나머지를 출력하는 프로그램을 작성하세요.
b) 밑변과 높이를 입력받아 삼각형의 넓이를 계산하여 출력하세요.

# a) 두 정수 연산
num1_str = input("첫 번째 정수를 입력하세요: ")
num2_str = input("두 번째 정수를 입력하세요: ")

# 입력받은 문자열을 정수로 변환
try:
    num1 = int(num1_str)
    num2 = int(num2_str)

    print(f"{num1} + {num2} = {num1 + num2}")
    print(f"{num1} - {num2} = {num1 - num2}")
    print(f"{num1} * {num2} = {num1 * num2}")
    if num2 != 0:
        print(f"{num1} / {num2} = {num1 / num2}")
        print(f"{num1} // {num2} (몫) = {num1 // num2}")
        print(f"{num1} % {num2} (나머지) = {num1 % num2}")
    else:
        print("0으로 나눌 수 없습니다.")

except ValueError:
    print("잘못된 입력입니다. 정수를 입력해주세요.")

print("-" * 20)

# b) 삼각형 넓이
base_str = input("삼각형의 밑변을 입력하세요: ")
height_str = input("삼각형의 높이를 입력하세요: ")

try:
    base = float(base_str)
    height = float(height_str)
    area = 0.5 * base * height
    print(f"밑변 {base}, 높이 {height}인 삼각형의 넓이는 {area} 입니다.")
except ValueError:
    print("잘못된 입력입니다. 숫자를 입력해주세요.")

문제 2: 조건문과 반복문 활용

a) 1부터 100까지의 정수 중 3의 배수이면서 5의 배수가 아닌 수들의 합을 구하여 출력하세요.
b) 사용자로부터 양의 정수 N을 입력받아, N의 약수를 모두 출력하는 프로그램을 작성하세요.

# a) 3의 배수이면서 5의 배수가 아닌 수들의 합
total_sum = 0
for i in range(1, 101):
    if i % 3 == 0 and i % 5 != 0:
        total_sum += i
print("1~100 중 3의 배수이면서 5의 배수가 아닌 수들의 합:", total_sum)

print("-" * 20)

# b) 약수 구하기
n_str = input("양의 정수 N을 입력하세요: ")
try:
    n = int(n_str)
    if n <= 0:
        print("양의 정수를 입력해주세요.")
    else:
        divisors = []
        for i in range(1, n + 1):
            if n % i == 0:
                divisors.append(i)
        print(f"{n}의 약수: {divisors}")
except ValueError:
    print("잘못된 입력입니다. 정수를 입력해주세요.")

문제 3: 리스트와 딕셔너리 조작

a) 5명의 학생 이름과 점수를 딕셔너리 형태로 저장하세요 (이름이 key, 점수가 value). 그 후, 점수가 80점 이상인 학생들의 이름만 리스트로 만들어 출력하세요.
b) 주어진 숫자 리스트에서 중복된 요소를 제거하고, 유니크한 요소들만으로 이루어진 새 리스트를 오름차순으로 정렬하여 출력하세요.

# a) 학생 점수 처리
scores = {'Alice': 92, 'Bob': 78, 'Charlie': 88, 'David': 95, 'Eve': 70}
high_scorers = []
for name, score in scores.items():
    if score >= 80:
        high_scorers.append(name)
print("80점 이상 학생들:", high_scorers)

# 또는 리스트 컴프리헨션 사용
# high_scorers_comp = [name for name, score in scores.items() if score >= 80]
# print("80점 이상 학생들 (컴프리헨션):", high_scorers_comp)

print("-" * 20)

# b) 중복 제거 및 정렬
numbers = [1, 5, 2, 3, 5, 1, 4, 2, 2, 6, 3, 7, 7]
# 세트를 사용하여 중복 제거 후 리스트로 변환하고 정렬
unique_sorted_numbers = sorted(list(set(numbers)))
print("원본 리스트:", numbers)
print("중복 제거 및 정렬된 리스트:", unique_sorted_numbers)

문제 4: 함수 작성 및 활용

숫자들로 이루어진 리스트를 인자로 받아, 해당 리스트의 합계, 평균, 최댓값, 최솟값을 계산하여 튜플 형태로 반환하는 함수 analyze_list(numbers)를 작성하세요. 빈 리스트가 입력될 경우, 합계 0, 평균 0, 최댓값 None, 최솟값 None을 반환하도록 처리하세요.

def analyze_list(numbers):
    """숫자 리스트를 받아 합계, 평균, 최댓값, 최솟값을 튜플로 반환합니다.
    빈 리스트의 경우 (0, 0, None, None)을 반환합니다.
    """
    if not numbers: # 리스트가 비어있는 경우
        return (0, 0, None, None)
    
    list_sum = sum(numbers)
    list_avg = list_sum / len(numbers)
    list_max = max(numbers)
    list_min = min(numbers)
    
    return (list_sum, list_avg, list_max, list_min)

# 테스트
data1 = [10, 20, 30, 40, 50]
result1 = analyze_list(data1)
print(f"{data1} 분석 결과: 합계={result1[0]}, 평균={result1[1]:.2f}, 최댓값={result1[2]}, 최솟값={result1[3]}")

data2 = []
result2 = analyze_list(data2)
print(f"{data2} 분석 결과: 합계={result2[0]}, 평균={result2[1]:.2f}, 최댓값={result2[2]}, 최솟값={result2[3]}")

data3 = [15, 5, 25, -5, 10]
result3 = analyze_list(data3)
print(f"{data3} 분석 결과: 합계={result3[0]}, 평균={result3[1]:.2f}, 최댓값={result3[2]}, 최솟값={result3[3]}")

문제 5: 객체 지향 프로그래밍 기초

Book이라는 이름의 클래스를 정의하세요. 이 클래스는 다음과 같은 특징을 가집니다:

  • 생성자(__init__)는 책의 제목(title), 저자(author), 페이지 수(pages)를 인자로 받습니다.
  • get_info()라는 인스턴스 메서드를 가지며, 이 메서드는 "제목 by 저자, 페이지수 pages" 형식의 문자열을 반환합니다. (예: "파이썬 프로그래밍 by 귀도 반 로섬, 300 pages")

Book 클래스의 인스턴스를 두 개 이상 생성하고, 각 인스턴스의 get_info() 메서드를 호출하여 결과를 출력하세요.

class Book:
    def __init__(self, title, author, pages):
        """Book 객체의 생성자입니다."""
        self.title = title
        self.author = author
        self.pages = pages

    def get_info(self):
        """책의 정보를 문자열로 반환합니다."""
        return f"{self.title} by {self.author}, {self.pages} pages"

# Book 클래스 인스턴스 생성
book1 = Book("파이썬 심층 가이드", "AI Assistant (Gemini)", 500)
book2 = Book("데이터 과학 실전", "김파이", 450)
book3 = Book("알고리즘 첫걸음", "이코딩", 380)

# get_info() 메서드 호출 및 출력
print(book1.get_info())
print(book2.get_info())
print(book3.get_info())

문제 6: 파일 입출력 및 문자열 처리 (단어 빈도수 계산)

다음 내용으로 sample_words.txt 파일을 생성하세요 (또는 직접 만드세요):

Python is fun.
Python is powerful and Python is easy to learn.
Fun, fun, fun!
Learning Python is a good start.

sample_words.txt 파일을 읽어, 각 단어가 몇 번 나타나는지 빈도수를 계산하는 프로그램을 작성하세요. 단어는 대소문자를 구분하지 않으며, 문장 부호(마침표, 쉼표, 느낌표)는 제거한 후 계산합니다. 가장 많이 나타난 단어 3개와 그 빈도수를 출력하세요.

import string # 문장 부호 제거에 사용

def count_word_frequency(filepath="sample_words.txt"):
    """파일을 읽어 단어 빈도수를 계산하고 상위 3개를 출력합니다."""
    word_counts = {}
    
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            for line in f:
                # 문장 부호 제거 및 소문자 변환
                # string.punctuation는 모든 구두점 문자열을 담고 있음
                translator = str.maketrans('', '', string.punctuation)
                cleaned_line = line.translate(translator).lower()
                
                words = cleaned_line.split() # 공백 기준으로 단어 분리
                
                for word in words:
                    if word: # 빈 문자열이 아닌 경우
                        word_counts[word] = word_counts.get(word, 0) + 1
                        
    except FileNotFoundError:
        print(f"오류: '{filepath}' 파일을 찾을 수 없습니다.")
        return

    if not word_counts:
        print("파일 내용이 비어있거나 단어를 찾을 수 없습니다.")
        return

    # 빈도수를 기준으로 내림차순 정렬
    # sorted_word_counts는 (단어, 빈도수) 튜플의 리스트가 됨
    sorted_word_counts = sorted(word_counts.items(), key=lambda item: item[1], reverse=True)
    
    print(f"--- '{filepath}' 단어 빈도수 분석 ---")
    print("상위 3개 단어:")
    for i, (word, count) in enumerate(sorted_word_counts[:3]): # 상위 3개만 슬라이싱
        print(f"{i+1}. '{word}': {count}회")

# sample_words.txt 파일 생성 (실제 실행 시 파일이 있어야 함)
sample_content = """Python is fun.
Python is powerful and Python is easy to learn.
Fun, fun, fun!
Learning Python is a good start."""
with open("sample_words.txt", "w", encoding="utf-8") as f:
    f.write(sample_content)

# 함수 호출
count_word_frequency()

문제 7: 예외 처리 활용

리스트(my_list)와 인덱스(index), 그리고 기본값(default, 기본값은 None)을 인자로 받는 함수 safe_list_get(my_list, index, default=None)를 작성하세요. 이 함수는 my_list[index]를 시도하여 값을 반환하되, 만약 IndexError가 발생하면 default 값을 반환하도록 하세요. 다른 종류의 예외(예: TypeError)는 처리하지 않아도 됩니다.

def safe_list_get(my_list, index, default=None):
    """리스트와 인덱스를 받아 안전하게 요소를 반환합니다.
    IndexError 발생 시 default 값을 반환합니다.
    """
    try:
        return my_list[index]
    except IndexError:
        return default
    # 다른 예외는 여기서 처리하지 않고 호출한 쪽으로 전달됨

# 테스트
my_data = [10, 20, 30, 40]

print(f"my_data[1]: {safe_list_get(my_data, 1)}")         # 20
print(f"my_data[5]: {safe_list_get(my_data, 5)}")         # None (기본 default 값)
print(f"my_data[5] (default='없음'): {safe_list_get(my_data, 5, default='없음')}") # 없음
print(f"my_data[-1]: {safe_list_get(my_data, -1)}")       # 40

# TypeError 예외는 처리하지 않으므로 발생 가능
# print(safe_list_get(my_data, "a")) # TypeError: list indices must be integers or slices, not str

문제 8: 컴프리헨션 활용 (회문 찾기)

주어진 문자열 리스트에서 회문(palindrome, 앞에서 읽으나 뒤에서 읽으나 동일한 단어 또는 구절)인 문자열만으로 이루어진 새 리스트를 리스트 컴프리헨션을 사용하여 만드세요. 회문 검사 시 대소문자는 구분하지 않습니다.

예시 입력: words = ["madam", "Python", "level", "racecar", "hello", "Noon"]

예상 출력: ['madam', 'level', 'racecar', 'Noon']

def find_palindromes_comprehension(word_list):
    """리스트 컴프리헨션을 사용하여 회문인 문자열 리스트를 반환합니다.
    대소문자는 구분하지 않습니다.
    """
    palindromes = [
        word for word in word_list 
        if word.lower() == word.lower()[::-1] # 소문자로 변환 후, 원본과 뒤집은 것이 같은지 비교
    ]
    return palindromes

# 테스트
words = ["madam", "Python", "level", "racecar", "hello", "Noon", "Kayak"]
palindrome_list = find_palindromes_comprehension(words)
print(f"원본 리스트: {words}")
print(f"회문 리스트: {palindrome_list}")

words2 = ["Able was I ere I saw Elba", "A man, a plan, a canal: Panama"] # 문장 회문은 추가 처리 필요
# 위 함수는 단어 단위 회문에 적합. 문장 회문은 공백, 문장부호 제거 후 비교해야 함.
# 여기서는 단어 회문에 집중합니다.
simple_phrases = ["Was it a car or a cat I saw?", "Step on no pets"] # 간단한 문구
# (참고) 문장 회문을 위한 전처리 예시:
# cleaned_phrase = ''.join(filter(str.isalnum, phrase)).lower()
# is_palindrome = cleaned_phrase == cleaned_phrase[::-1]

print(f"간단한 문구 테스트: {find_palindromes_comprehension(['rotor', 'kayak', 'stats'])}")

이것으로 파이썬 심층 가이드의 모든 장과 연습 문제를 마칩니다! 🥳 꾸준한 연습과 실제 프로젝트 적용을 통해 파이썬 실력을 더욱 발전시켜 나가시길 바랍니다. 이 가이드가 여러분의 파이썬 학습 여정에 도움이 되었기를 바랍니다. 💪

이 문서는 파이썬 프로그래밍의 핵심 기능들을 소개했습니다. 파이썬은 매우 방대하고 강력한 언어이므로, 더 자세한 정보와 고급 기능들은 파이썬 공식 문서 및 다양한 학습 자료를 참고하시는 것이 좋습니다.

문서 업데이트 시간: 2025년 5월 18일