컨테이너(Container)란?

파이썬에서 컨테이너(Container)는 다른 객체들을 담을 수 있는 자료구조입니다.

# 여러 가지 컨테이너들
my_list = [1, 2, 3, 4, 5]           # 리스트
my_tuple = (1, 2, 3)                # 튜플
my_set = {1, 2, 3}                  # 세트
my_dict = {"name": "Python"}        # 딕셔너리
my_string = "hello"                 # 문자열도 컨테이너!

이들은 모두 여러 개의 데이터를 담고 있다는 공통점이 있습니다.

컨테이너의 특징

컨테이너는 두 가지 중요한 특징을 가지고 있습니다:

1. 멤버십 테스트 (Membership Test)

numbers = [1, 2, 3, 4, 5]

# "이 값이 컨테이너 안에 있나?" 확인
print(3 in numbers)      # True
print(10 in numbers)     # False
print(10 not in numbers) # True

in 연산자를 사용할 수 있는 것이 컨테이너의 첫 번째 특징입니다.

2. 반복 가능 (Iterable)

# for 루프로 하나씩 꺼낼 수 있음
for num in numbers:
    print(num)

컨테이너의 원소들을 하나씩 순회할 수 있습니다.

파이썬의 주요 컨테이너들

컨테이너순서 유지중복 허용변경 가능용도
listOOO순서 있는 데이터 모음
tupleOOX변경 불가능한 데이터
setXXO중복 없는 데이터
dictO (3.7+)X (키)O키-값 쌍
strOOX문자 시퀀스

이 중에서 가장 많이 사용하고 중요한 것이 바로 리스트(List)입니다!

리스트 - 컨테이너의 대표주자

리스트는 파이썬에서 가장 많이 사용되는 컨테이너입니다.

numbers = [1, 2, 3, 4, 5]
fruits = ["apple", "banana", "orange"]
mixed = [1, "hello", 3.14, True]  # 다양한 타입 가능!

리스트의 내부 구조

메모리에서 리스트는 어떻게 생겼을까?

리스트를 만들면 파이썬은 메모리에 어떻게 저장할까요?

my_list = [10, 20, 30]

리스트는 실제로 배열(Array)로 구현됩니다. 하지만 일반적인 배열과는 조금 다릅니다.

메모리 구조:

리스트 객체:
┌─────────────┐
│ size: 3     │  ← 현재 원소 개수
│ capacity: 4 │  ← 할당된 공간 크기
│ items: ───┐ │  ← 실제 원소들을 가리키는 포인터
└───────────┼─┘
            │
            ▼
원소 배열:
┌────┬────┬────┬────┐
│ •  │ •  │ •  │    │
└─┼──┴─┼──┴─┼──┴────┘
  │    │    │
  ▼    ▼    ▼
  10   20   30

리스트는 두 가지 정보를 가지고 있습니다:
- size: 실제로 들어있는 원소 개수
- capacity: 메모리에 확보된 공간 크기

왜 capacity가 size보다 클까?

리스트에 원소를 추가할 때마다 메모리를 다시 할당하면 너무 느립니다.
그래서 파이썬은 미리 여유 공간을 확보해둡니다.

my_list = []  # capacity: 0
my_list.append(1)  # capacity: 4 (한 번에 4칸 확보)
my_list.append(2)  # capacity: 4 (아직 여유 있음)
my_list.append(3)  # capacity: 4
my_list.append(4)  # capacity: 4
my_list.append(5)  # capacity: 8 (공간 부족, 2배로 증가)

이런 방식을 동적 배열(Dynamic Array)이라고 합니다.

리스트 연산의 시간 복잡도

리스트의 각 연산이 얼마나 빠른지 알아봅시다.

빠른 연산 (O(1) - 상수 시간)

# 인덱스로 접근
value = my_list[2]  # 매우 빠름

# 끝에 추가
my_list.append(10)  # 평균적으로 빠름

# 끝에서 제거
my_list.pop()  # 빠름

느린 연산 (O(n) - 리스트 크기에 비례)

# 중간에 삽입
my_list.insert(0, 5)  # 느림 (모든 원소를 한 칸씩 이동)

# 중간에서 제거
my_list.pop(0)  # 느림 (모든 원소를 한 칸씩 당김)

# 원소 검색
if 10 in my_list:  # 느림 (처음부터 끝까지 검색)
    pass

리스트 복사의 비밀

리스트를 복사할 때 주의해야 할 점이 있습니다.

# 얕은 복사 (Shallow Copy)
original = [1, 2, 3]
copy1 = original  # 같은 리스트를 가리킴!

copy1.append(4)
print(original)  # [1, 2, 3, 4] - 원본도 변경됨!

# 진짜 복사
copy2 = original.copy()  # 또는 original[:]
copy2.append(5)
print(original)  # [1, 2, 3, 4] - 원본은 그대로

중첩 리스트는 더 조심해야 합니다.

# 2차원 리스트
matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()

shallow[0].append(99)
print(matrix)  # [[1, 2, 99], [3, 4]] - 내부 리스트는 공유됨!

# 깊은 복사 (Deep Copy)
import copy
deep = copy.deepcopy(matrix)
deep[0].append(100)
print(matrix)  # [[1, 2, 99], [3, 4]] - 원본은 안전

Iterator와 Iterable

컨테이너를 순회하려면 IteratorIterable을 이해해야 합니다.

Iterable이란?

Iterable은 "반복 가능한 객체"입니다. 쉽게 말하면 for 루프에 넣을 수 있는 모든 것입니다.

# 이것들은 모두 Iterable입니다
for x in [1, 2, 3]:        # 리스트
    print(x)

for x in (1, 2, 3):        # 튜플
    print(x)

for x in "hello":          # 문자열
    print(x)

for x in {1, 2, 3}:        # 세트
    print(x)

Iterator란?

Iterator는 실제로 값을 하나씩 꺼내주는 객체입니다.

numbers = [1, 2, 3]

# iter() 함수로 Iterator 만들기
iterator = iter(numbers)

# next() 함수로 값을 하나씩 꺼내기
print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3
print(next(iterator))  # StopIteration 에러!

for 루프의 비밀

우리가 쓰는 for 루프는 실제로 이렇게 작동합니다.

# 우리가 쓰는 코드
for num in [1, 2, 3]:
    print(num)

# 실제로 파이썬이 하는 일
iterator = iter([1, 2, 3])  # Iterable을 Iterator로 변환
while True:
    try:
        num = next(iterator)  # 다음 값 꺼내기
        print(num)
    except StopIteration:     # 더 이상 값이 없으면 종료
        break

Iterable vs Iterator

┌──────────────┐
│   Iterable   │  "반복 가능한 객체" (list, tuple, str 등)
└──────┬───────┘
       │ iter()
       ▼
┌──────────────┐
│   Iterator   │  "값을 하나씩 꺼내주는 객체"
└──────┬───────┘
       │ next()
       ▼
     값 반환

핵심:
- Iterable: iter() 함수를 호출하면 Iterator를 반환
- Iterator: next() 함수를 호출하면 다음 값을 반환

직접 만들어보기

class CountUp:
    """1부터 n까지 세는 Iterable"""
    def __init__(self, max):
        self.max = max

    def __iter__(self):
        """Iterator를 반환"""
        return CountUpIterator(self.max)

class CountUpIterator:
    """실제로 값을 반환하는 Iterator"""
    def __init__(self, max):
        self.max = max
        self.current = 0

    def __next__(self):
        """다음 값 반환"""
        if self.current >= self.max:
            raise StopIteration
        self.current += 1
        return self.current

# 사용
counter = CountUp(5)
for num in counter:
    print(num)
# 1, 2, 3, 4, 5

복잡해 보이죠? 더 간단한 방법이 있습니다. 바로 제너레이터입니다!

제너레이터 - 똑똑한 Iterator

문제 상황: 메모리 낭비

100만 개의 숫자를 처리하고 싶습니다.

# 리스트 (Container)로 만들기
numbers = [i for i in range(1000000)]  # 100만 개를 모두 메모리에!

for num in numbers:
    print(num)

문제점:
- 100만 개의 숫자를 모두 메모리에 저장
- 실제로는 한 번에 하나씩만 필요한데 비효율적

제너레이터의 등장

제너레이터(Generator)는 값을 필요할 때마다 하나씩 만들어내는 똑똑한 Iterator입니다.

# 제너레이터 버전
def number_generator():
    for i in range(1000000):
        yield i  # 값을 하나씩 반환

for num in number_generator():
    print(num)

차이점:

리스트 (Container)제너레이터 (Iterator)
메모리모든 값을 저장 (100만 개)하나씩만 생성 (1개)
속도초기 생성 느림즉시 시작
재사용여러 번 순회 가능한 번만 순회 가능

제너레이터는 Iterator를 쉽게 만드는 방법입니다!

yield 키워드

yieldreturn과 비슷하지만, 함수의 상태를 기억합니다.

def simple_generator():
    print("첫 번째 값 생성")
    yield 1
    print("두 번째 값 생성")
    yield 2
    print("세 번째 값 생성")
    yield 3

gen = simple_generator()
print(next(gen))  # "첫 번째 값 생성" → 1
print(next(gen))  # "두 번째 값 생성" → 2
print(next(gen))  # "세 번째 값 생성" → 3

yield를 만나면:
1. 값을 반환
2. 함수를 일시정지
3. 다음 호출 때 이어서 실행

제너레이터의 장점

# 무한 수열도 만들 수 있습니다!
def infinite_numbers():
    num = 0
    while True:
        yield num
        num += 1

gen = infinite_numbers()
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2
# 계속 가능...

리스트로는 불가능한 일입니다. 무한한 메모리가 필요하니까요!

for 루프와 제너레이터

for 루프의 원리

for 루프는 실제로 제너레이터를 사용합니다.

# 우리가 쓰는 코드
for num in [1, 2, 3]:
    print(num)

# 실제로 일어나는 일
iterator = iter([1, 2, 3])
while True:
    try:
        num = next(iterator)
        print(num)
    except StopIteration:
        break

for 루프로 제너레이터 만들기

def count_up_to(max):
    count = 1
    while count <= max:
        yield count
        count += 1

for num in count_up_to(5):
    print(num)
# 1
# 2
# 3
# 4
# 5

while 루프 안에서 yield를 사용하면 값을 계속 생성할 수 있습니다.

실전 예제 1: 파일 읽기

큰 파일을 읽을 때 제너레이터가 유용합니다.

def read_large_file(file_path):
    """파일을 한 줄씩 읽는 제너레이터"""
    with open(file_path) as file:
        for line in file:
            yield line.strip()

# 100GB 파일도 메모리 부담 없이 처리
for line in read_large_file("huge_file.txt"):
    process(line)

실전 예제 2: 피보나치 수열

def fibonacci():
    """무한 피보나치 수열 생성기"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 처음 10개만 가져오기
fib = fibonacci()
for _ in range(10):
    print(next(fib))
# 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

실전 예제 3: 데이터 필터링

def even_numbers(numbers):
    """짝수만 반환하는 제너레이터"""
    for num in numbers:
        if num % 2 == 0:
            yield num

# 사용
for num in even_numbers(range(10)):
    print(num)
# 0, 2, 4, 6, 8

제너레이터 표현식

리스트 컴프리헨션과 비슷하지만 [] 대신 ()를 사용합니다.

# 리스트 컴프리헨션 (메모리 많이 사용)
squares_list = [x**2 for x in range(1000000)]

# 제너레이터 표현식 (메모리 절약)
squares_gen = (x**2 for x in range(1000000))

# 필요할 때만 계산
print(next(squares_gen))  # 0
print(next(squares_gen))  # 1
print(next(squares_gen))  # 4

while 루프와 제너레이터

while 루프로 더 복잡한 제너레이터를 만들 수 있습니다.

조건부 생성

def numbers_until_condition(limit):
    """합이 limit를 넘을 때까지 숫자 생성"""
    total = 0
    num = 1
    while total < limit:
        yield num
        total += num
        num += 1

for n in numbers_until_condition(20):
    print(n)
# 1, 2, 3, 4, 5 (1+2+3+4+5=15, 6을 더하면 21이므로 중단)

상태를 가진 제너레이터

def countdown(start):
    """카운트다운 제너레이터"""
    current = start
    while current > 0:
        yield current
        current -= 1
    yield "발사!"

for count in countdown(5):
    print(count)
# 5, 4, 3, 2, 1, 발사!

제너레이터 체이닝

def first_n(generator, n):
    """제너레이터에서 처음 n개만 가져오기"""
    count = 0
    while count < n:
        yield next(generator)
        count += 1

# 무한 제너레이터 + 제한
def all_numbers():
    num = 0
    while True:
        yield num
        num += 1

limited = first_n(all_numbers(), 5)
print(list(limited))  # [0, 1, 2, 3, 4]

컨테이너 vs 제너레이터

언제 무엇을 사용해야 할까요?

컨테이너(리스트)를 사용할 때

# 1. 여러 번 접근이 필요할 때
data = [1, 2, 3, 4, 5]
print(data[2])  # 인덱스 접근
print(len(data))  # 길이 확인
print(data[:3])  # 슬라이싱

# 2. 데이터 크기가 작을 때
small_list = [x for x in range(100)]  # OK

# 3. 전체 데이터가 필요할 때
sorted_data = sorted([3, 1, 2])  # 정렬은 전체 데이터 필요

# 4. 여러 번 순회가 필요할 때
for x in data:
    print(x)
for x in data:  # 다시 순회 가능!
    print(x * 2)

제너레이터(Iterator)를 사용할 때

# 1. 데이터가 클 때
huge_data = (x for x in range(10000000))  # 메모리 절약

# 2. 한 번만 순회할 때
for item in huge_data:
    process(item)

# 3. 무한 수열일 때
def infinite():
    while True:
        yield get_next_value()

# 4. 파이프라인 처리 (체인)
data = (x for x in range(100))
filtered = (x for x in data if x % 2 == 0)
squared = (x**2 for x in filtered)

# 5. 지연 평가(Lazy Evaluation)가 필요할 때
# 필요한 순간까지 계산을 미룸

정리

배운 개념의 계층 구조

graph TD
    A["Container
가장 일반적인 개념
데이터를 담는 자료구조
list, tuple, set, dict, str"] B["Iterable
반복 가능한 객체
for 루프에 넣을 수 있음
iter()를 호출하면 Iterator 반환"] C["Iterator
값을 하나씩 꺼냄
next()로 다음 값 반환
한 번만 순회 가능"] D["Generator
Iterator의 간편 버전
yield로 값을 생성
메모리 효율적, 무한 수열 가능"] A -->|"모든 Container는..."| B B -->|"iter()"| C C -->|"쉽게 만드는 방법"| D style A fill:#e3f2fd style B fill:#fff3e0 style C fill:#f3e5f5 style D fill:#e8f5e9

컨테이너 (Container)

  1. 정의: 여러 객체를 담을 수 있는 자료구조
  2. 특징:
  • 멤버십 테스트 (in 연산자)
  • 반복 가능 (Iterable)
  1. 종류: list, tuple, set, dict, str

리스트 (List)

  1. 내부 구조: 동적 배열로 구현
  2. capacity: 미리 공간을 확보해서 효율성 향상
  3. 시간 복잡도:
  • 빠른 것: 인덱스 접근, 끝에 추가/제거
  • 느린 것: 중간 삽입/제거, 검색
  1. 복사 주의: 얕은 복사 vs 깊은 복사

Iterator와 Iterable

  1. Iterable: iter()를 호출하면 Iterator 반환
  2. Iterator: next()를 호출하면 다음 값 반환
  3. for 루프: 내부적으로 iter()next() 사용
  4. 차이점: Iterable은 여러 번 순회 가능, Iterator는 한 번만

제너레이터 (Generator)

  1. 정의: Iterator를 쉽게 만드는 방법
  2. yield: 값을 하나씩 반환하고 상태를 유지
  3. 장점: 메모리 효율적, 무한 수열 가능
  4. 사용법:
  • for 루프와 yield
  • while 루프와 yield (조건부 생성)
  • 제너레이터 표현식 (x for x in ...)

언제 무엇을 사용할까?

상황사용할 것이유
작은 데이터Container (List)빠르고 편리
여러 번 순회Container (List)재사용 가능
인덱스 접근Container (List)랜덤 액세스
큰 데이터Generator메모리 절약
한 번만 순회Generator효율적
무한 수열Generator필수
파이프라인Generator체인 구성