이터레이터, 제네레이터, 데코레이터, *args, **kwargs 개념 정리

2star_·2024년 10월 9일
0

Python

목록 보기
12/13

1. 이터레이터 기본 개념 및 사용법

파이썬의 이터레이터(iterator) 개념을 코드 예시를 통해 하나씩 살펴본다. 이터레이터는 데이터를 순차적으로 하나씩 반환하는 객체로, 데이터의 반복 처리에 중요한 역할을 한다.

1.1. 이터레이터 예시

class MyIterator:
    def __init__(self, data):
        self.data = data  # 이터레이터가 순차적으로 다룰 데이터를 저장
        self.index = 0  # 현재 데이터를 반환할 위치를 나타내며, 처음에는 0으로 초기화

    def __iter__(self):
        return self  # 이터러블 객체임을 나타내기 위해 self를 반환

    def __next__(self):
        if self.index < len(self.data):  # 현재 인덱스가 리스트의 길이보다 작은지 확인
            result = self.data[self.index]  # 현재 인덱스에 해당하는 데이터를 result에 저장
            self.index += 1  # 다음 호출 시 다음 데이터를 가져오기 위해 인덱스를 1 증가시킴
            return result  # 저장된 데이터를 반환
        else:
            raise StopIteration  # 더 이상 반환할 값이 없으면 StopIteration 예외를 발생시킴

# 이터레이터 사용
my_iter = MyIterator([1, 2, 3])
for item in my_iter:
    print(item)

1.2. 코드 설명

  1. 클래스 정의: class MyIterator:

    • MyIterator라는 이름의 클래스를 정의한다. 이 클래스는 이터레이터 역할을 하면서 데이터를 하나씩 반환할 수 있게 한다.
  2. 생성자 메서드: def __init__(self, data):

    • __init__은 클래스가 생성될 때 호출되는 생성자 메서드다.
    • data 매개변수는 이터레이터가 순차적으로 다룰 데이터를 저장하는 역할을 한다.
  3. 데이터 저장: self.data = data

    • 전달받은 데이터를 self.data에 저장한다. 이 데이터는 이터레이터가 접근할 리스트나 시퀀스다.
  4. 인덱스 초기화: self.index = 0

    • index는 현재 데이터를 반환할 위치를 나타낸다. 처음에는 0으로 시작해서 데이터를 첫 번째 항목부터 접근한다.
  5. 이터러블 반환: def __iter__(self):

    • __iter__ 메서드는 파이썬에서 이 객체가 이터러블임을 나타내며, 이 메서드가 호출되면 self를 반환한다. 이렇게 해서 이 객체가 이터레이터로 사용될 수 있게 된다.
  6. 다음 값 반환: def __next__(self):

    • __next__ 메서드는 이터레이터가 next() 함수를 호출할 때 실행된다. 이 메서드는 이터레이터가 다음 값을 반환할 때 호출되는 핵심 메서드다.
  7. 조건문: if self.index < len(self.data):

    • 현재 인덱스가 리스트의 길이보다 작은지 확인해서 반환할 데이터가 남아 있는지 확인한다.
  8. 현재 데이터 저장: result = self.data[self.index]

    • 현재 인덱스에 해당하는 데이터를 result에 저장한다. 예를 들어, 처음에는 self.data[0]의 값을 가져온다.
    • 여기서 result 변수는 현재 반환할 값을 임시로 저장하는 역할을 한다. 이 변수에 저장된 값을 통해 인덱스를 사용해 직접 self.data에 접근하지 않고도 데이터를 반환할 수 있다. 이렇게 하면 코드의 가독성을 높이고, 데이터를 반환하기 전에 추가적인 처리를 할 수 있는 여지를 남긴다.
  9. 인덱스 증가: self.index += 1

    • 인덱스를 1씩 증가시켜서 다음 번에 __next__()가 호출될 때 다음 데이터를 가져올 수 있게 한다.
  10. 값 반환: return result

    • result 변수에 저장된 현재 인덱스의 데이터를 반환한다. 이 반환된 값이 for 루프에서 사용된다.
    • 반환된 값은 이터레이터를 사용하는 곳에서 변수에 저장하거나, 바로 출력하거나, 다른 연산에 사용될 수 있다. 예를 들어, for 루프에서 item 변수에 각각의 값을 저장해서 반복적으로 처리할 수 있다.
  11. 반복 종료: else: raise StopIteration

    • 만약 indexdata 리스트의 길이와 같거나 커지면 더 이상 반환할 값이 없으므로 StopIteration 예외를 발생시켜서 이터레이션이 끝났음을 알린다. 이 예외는 for 루프가 자동으로 처리해서 루프를 종료시킨다.

1.3. 코드 실행 결과

my_iter = MyIterator([1, 2, 3])
for item in my_iter:
    print(item)

이 코드는 다음과 같이 출력된다:

1
2
3

이터레이터를 사용하면 데이터의 각 항목을 순차적으로 처리할 수 있어서 반복 작업을 간단하게 수행할 수 있다.


2. 제너레이터 & yield

제너레이터와 함께 자주 사용되는 키워드 중 하나가 yield다. yield는 함수에서 값을 생성하고 반환하는 데 사용되는 특별한 키워드로, 함수가 일시 중단된 상태를 기억하고 이후에 이어서 실행할 수 있도록 한다.

2.1. 제너레이터와 yield의 기본 개념

제너레이터 함수는 일반적인 함수와 다르게 return 대신 yield를 사용하여 값을 반환한다. yield가 호출되면 함수의 실행이 중단되고, 현재 값이 호출자에게 반환된다. 함수는 이후에 호출되면 중단된 지점부터 실행을 재개할 수 있다.

def my_generator():
    yield 1
    yield 2
    yield 3

# 제너레이터 사용
for value in my_generator():
    print(value)

위의 코드는 제너레이터 함수의 예시로, yield 키워드를 통해 순차적으로 1, 2, 3을 반환한다. 실행 결과는 다음과 같다:

1
2
3

2.2. yield와 return의 차이점

  • return: 함수의 실행을 종료하고 값을 반환한다. 함수가 호출될 때마다 새로운 실행이 시작된다.
  • yield: 함수의 상태를 유지하면서 값을 반환하고, 함수가 재개될 수 있도록 한다. 이는 함수가 일시 중단된 상태를 기억하고, 이후에 이어서 실행할 수 있도록 해준다.

yield를 사용하면 제너레이터 객체를 생성하게 되고, 이 객체는 이터레이터처럼 동작하여 값을 하나씩 반환한다. 제너레이터는 메모리 효율성이 높으며, 많은 데이터를 다룰 때 매우 유용하다.

2.3. 제너레이터 기본 구조

제너레이터 표현식은 제너레이터 함수를 간결하게 작성할 수 있는 방법으로, 리스트 컴프리헨션과 유사한 문법을 사용하지만, 메모리 효율성을 위해 데이터를 한 번에 하나씩 생성하는 방식이다.

gen_exp = (x * x for x in range(5))

이 코드는 제너레이터 표현식이다.

  • x * x for x in range(5) 부분은 range(5)에서 값을 하나씩 가져와서 그 값을 제곱(x * x)한 값을 순차적으로 생성한다.
  • 이때 제너레이터 표현식은 ()로 감싸져 있다. 이 덕분에 값들이 메모리에 미리 저장되지 않고, 필요할 때마다 하나씩 생성된다.
  • 이터레이터처럼 next() 함수를 호출하거나, for 루프를 통해 값을 하나씩 순차적으로 얻을 수 있다.

2.4. 리스트 컴프리헨션과의 차이점

제너레이터 표현식은 리스트 컴프리헨션과 매우 비슷해 보이지만, 큰 차이점이 있다.

리스트 컴프리헨션

list_comp = [x * x for x in range(5)]
  • 리스트 컴프리헨션은 range(5)에서 값을 가져와 모든 결과를 메모리에 저장한다. 즉 리스트가 완성된 후에 메모리에 저장된 값들이 사용된다.
  • 작은 데이터셋에는 빠르고 편리하지만 메모리 사용량이 커지게 된다. 큰 데이터셋을 다룰 때는 효율적이지 않다.

제너레이터

gen_exp = (x * x for x in range(5))
  • 제너레이터 표현식은 값들을 미리 계산해 메모리에 저장하지 않고, 필요할 때 하나씩 값을 생성하여 반환한다.
  • 이는 메모리 효율성이 매우 좋고 큰 데이터셋을 처리할 때 특히 유리하다.

2.5. 제너레이터 동작 방식

제너레이터 표현식은 값을 한 번에 하나씩 반환하기 때문에 for 루프에서 자동으로 처리된다. 제너레이터는 지연 평가(lazy evaluation) 방식을 사용하므로, next()가 호출되거나 for 루프가 실행될 때마다 필요한 값만 계산하여 반환한다.

gen_exp = (x * x for x in range(5))

# 첫 번째 호출
print(next(gen_exp))  # 출력: 0 (0 * 0)

# 두 번째 호출
print(next(gen_exp))  # 출력: 1 (1 * 1)

# 계속해서 호출하면 4까지 출력

for 루프 사용

for num in gen_exp:
    print(num)

제너레이터 표현식은 이터레이터처럼 동작하므로, for 루프에서 자동으로 __next__() 메서드가 호출되어 값을 하나씩 가져온다. 값이 더 이상 없을 때 StopIteration 예외가 발생하고, for 루프는 종료된다.

2.6. 제너레이터를 사용하는 이유

제너레이터 표현식을 사용하는 주요 이유는 메모리 효율성과 지연 평가 방식이다.

메모리 효율성

  • 제너레이터 표현식은 값을 필요할 때만 생성하므로, 대용량 데이터나 무한한 데이터를 처리할 때 메모리를 절약할 수 있다.
  • 예를 들어, 1억 개의 값을 저장할 필요가 있을 때 리스트 컴프리헨션을 사용하면 메모리가 꽉 차게 되지만, 제너레이터 표현식은 필요할 때만 값을 하나씩 반환하므로 훨씬 효율적이다.

지연 평가 (Lazy Evaluation)

  • 제너레이터는 모든 데이터를 미리 계산하지 않고, 필요할 때마다 하나씩 계산하여 반환한다. 이렇게 하면 계산 비용을 분산시킬 수 있어 효율적이다.
  • 예를 들어, 무한대 숫자 생성기처럼 끝이 없는 데이터를 생성하는 상황에서 유용하다. 필요한 만큼만 계산해서 사용하면 되기 때문에 전체 데이터를 미리 생성하지 않아도 된다.

2.7. 리스트 컴프리헨션 vs 제너레이터 비교 예시

리스트 컴프리헨션

list_comp = [x * x for x in range(1000000)]  # 1,000,000개의 제곱 값을 메모리에 저장
  • 모든 제곱 값이 한꺼번에 메모리에 저장되므로, 메모리 사용량이 많다.

제너레이터 표현식

gen_exp = (x * x for x in range(1000000))  # 1,000,000개의 제곱 값을 하나씩 생성
  • 이 경우 메모리에 값이 저장되지 않고, 필요할 때만 계산하여 반환되므로 메모리 사용량이 적다.

2.8. 예시: 큰 파일의 각 줄을 처리하는 경우

예를 들어, 큰 텍스트 파일의 각 줄을 처리해야 한다고 가정해 보자. 리스트 컴프리헨션을 사용하면 모든 줄을 메모리에 한꺼번에 저장해야 하지만, 제너레이터 표현식을 사용하면 한 줄씩 읽고 처리할 수 있다.

# 리스트 컴프리헨션을 사용할 경우
lines = [line.strip() for line in open('large_file.txt')]

# 제너레이터 표현식을 사용할 경우
lines_gen = (line.strip() for line in open('large_file.txt'))

# 제너레이터를 사용하면 파일을 한 줄씩 처리
for line in lines_gen:
    print(line)

제너레이터 표현식은 파일에서 한 줄씩 읽어 메모리 사용을 최소화하며 처리한다. 큰 파일을 처리할 때 특히 유용하다.


3. 데코레이터(Decorator)

이번 글에서는 파이썬의 매우 강력한 기능 중 하나인 데코레이터(Decorator)에 대해 알아보자. 데코레이터는 함수를 꾸며주는 역할을 하며, 기존 함수에 새로운 기능을 추가할 수 있다. 복잡하게 들릴 수 있지만, 천천히 예제를 통해 하나씩 살펴보면 쉽게 이해할 수 있을 것이다.

3.1. 데코레이터의 기본 개념

데코레이터는 함수를 다른 함수로 감싸는 역할을 한다. 어떤 함수를 꾸미고 싶을 때, 그 함수의 동작을 바꾸지 않고 추가적인 기능을 넣을 수 있다.

예제:

def hello():
    # 'Hello, World!'를 출력하는 간단한 함수
    print("Hello, World!")

위 함수는 "Hello, World!"를 출력하는 간단한 함수다. 이제 이 함수에 데코레이터를 적용해, 함수가 실행되기 전과 후에 메시지를 추가로 출력해보자.

3.2. 데코레이터 없는 함수

데코레이터 없이 함수의 동작을 변경하는 방식을 보자. 새로운 함수를 만들고, 그 안에서 hello() 함수를 호출해본다.

def decorator_function(original_function):
    # 원래 함수를 감싸는 데코레이터 함수 정의
    def wrapper_function():
        print("함수 실행 전입니다.")  # 추가 기능: 함수 실행 전 메시지 출력
        original_function()  # 원래 함수 실행
        print("함수 실행 후입니다.")  # 추가 기능: 함수 실행 후 메시지 출력
    return wrapper_function

# 'hello' 함수를 데코레이터로 감싸기
decorated_hello = decorator_function(hello)
decorated_hello()  # 데코레이터 적용된 함수 실행

설명:

  • decorator_function은 함수를 인자로 받아 그 함수를 꾸며주는 역할을 한다.
  • wrapper_function은 원래 함수를 호출하기 전후에 추가로 메시지를 출력한다.
  • decorated_hellohello 함수를 데코레이터 함수로 꾸며서 실행한 것이다.

실행 결과:

함수 실행 전입니다.
Hello, World!
함수 실행 후입니다.

이렇게 함수를 감싸서 새로운 동작을 추가할 수 있다.

3.3. 데코레이터를 사용

위 예제는 데코레이터를 직접 호출해서 사용했다. 하지만 파이썬에서는 @ 문법을 사용하여 쉽게 데코레이터를 적용할 수 있다.

@decorator_function
# 'hello' 함수에 데코레이터 적용
def hello():
    print("Hello, World!")

hello()  # 데코레이터가 적용된 함수 호출

설명:

  • @decorator_functionhello() 함수 위에 작성하면, hello 함수는 자동으로 decorator_function으로 꾸며진다.
  • 이제 hello()를 호출하면 자동으로 데코레이터가 적용된 함수가 실행된다.

실행 결과:

함수 실행 전입니다.
Hello, World!
함수 실행 후입니다.

3.4. 데코레이터 간단 요약

데코레이터는 "어떤 함수의 실행을 가로채서 전후에 다른 작업을 수행하는 함수"라고 이해하면 된다. 이 방법을 통해 코드의 반복을 줄이면서 공통적인 동작을 쉽게 추가할 수 있다.

예를 들어, 함수를 실행하기 전후로 로깅하거나 실행 시간을 측정하는 등의 동작을 추가할 수 있다.

3.5. 예제 : 함수 실행 시간을 측정하는 데코레이터

이제 데코레이터의 예제를 보자. 아래 코드는 함수가 실행되는 데 걸리는 시간을 측정하는 데코레이터다.

import time

def time_decorator(func):
    # 함수 실행 시간을 측정하는 데코레이터 정의
    def wrapper():
        start_time = time.time()  # 시작 시간 기록
        func()  # 원래 함수 실행
        end_time = time.time()  # 끝난 시간 기록
        print(f"함수 실행 시간: {end_time - start_time}초")
    return wrapper

@time_decorator
# 함수 실행 시간 측정을 위해 데코레이터 적용
def test_function():
    print("이 함수는 2초간 멈춥니다.")
    time.sleep(2)  # 2초간 대기

test_function()  # 함수 실행

설명:

  • time_decorator는 원래 함수를 감싸는 데코레이터다.
  • wrapper는 함수가 시작될 때 시간을 기록하고, 함수 실행 후에 끝난 시간을 기록하여 실행 시간을 출력한다.
  • test_function@time_decorator로 감싸져, 실행할 때마다 실행 시간이 자동으로 측정된다.

실행 결과:

이 함수는 2초간 멈춥니다.
함수 실행 시간: 2.002초

3.6. 매개변수가 있는 함수에 데코레이터 적용하기

위 예제들은 매개변수가 없는 함수에 적용했지만, 매개변수가 있는 함수에도 데코레이터를 적용할 수 있다. 이를 위해 *args**kwargs를 사용한다.

def decorator_function(original_function):
    # 매개변수가 있는 함수를 감싸는 데코레이터 함수 정의
    def wrapper_function(*args, **kwargs):
        print("함수 실행 전입니다.")  # 추가 기능: 함수 실행 전 메시지 출력
        result = original_function(*args, **kwargs)  # 원래 함수 호출 후 반환값을 'result'에 저장  # 매개변수 전달 후 원래 함수 실행
        print("함수 실행 후입니다.")  # 추가 기능: 함수 실행 후 메시지 출력
        return result  # 원래 함수의 실행 결과를 반환하여 데코레이터가 원래 함수의 반환값을 그대로 유지하도록 함
    return wrapper_function

@decorator_function
# 매개변수가 있는 함수에 데코레이터 적용
def greet(name, age):
    print(f"안녕하세요, 저는 {name}이고, 나이는 {age}입니다.")

greet("홍길동", 30)  # 데코레이터 적용된 함수 호출

설명:

  • wrapper_function에서 *args**kwargs를 사용하여 함수가 어떤 매개변수를 받든 데코레이터를 적용할 수 있다.
  • greet() 함수는 이름과 나이를 매개변수로 받지만, 데코레이터가 적용되어 함수 실행 전후로 메시지가 출력된다.

실행 결과:

함수 실행 전입니다.
안녕하세요, 저는 홍길동이고, 나이는 30입니다.
함수 실행 후입니다.

4. *args와 **kwargs의 개념

*args**kwargs는 파이썬 함수에서 임의의 개수의 인자를 처리할 수 있게 해주는 기능이다. 이 기능을 사용하면 함수가 몇 개의 인자를 받을지 미리 알 수 없거나, 다양한 개수의 인자를 유연하게 받아 처리할 수 있다.

4.1. *args: 임의의 개수의 위치 인자

*args는 함수를 호출할 때 몇 개의 인자를 넣을지 미리 모를 때 사용한다. 여러 개의 인자를 하나의 튜플로 묶어서 함수 안으로 전달한다.

예시:

def print_args(*args):
    for a in args:
        print(a)

print_args(1, 2, 3)

설명:

  • *args는 여러 개의 인자를 받는다. 이때 args는 튜플로 처리된다.
  • print_args(1, 2, 3)을 호출하면, 1, 2, 3이 모두 args에 들어간다.
  • 함수 내부에서는 반복문을 통해 각각의 인자를 출력할 수 있다.

실행 결과:

1
2
3

즉, *args는 위치 인자들을 개수에 상관없이 함수에 넘길 수 있도록 해준다.

4.2. **kwargs: 임의의 개수의 키워드 인자

**kwargs는 함수에 이름이 지정된 인자(키워드 인자)들을 넘길 때 사용한다. 여러 개의 키워드 인자를 딕셔너리 형태로 함수에 전달한다.

예시:

def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_kwargs(name="홍길동", age=30, city="서울")

설명:

  • **kwargs는 키워드 인자들을 받는다. 이때 kwargs는 딕셔너리로 처리된다.
  • 함수 호출 시 name="홍길동", age=30 등의 형식으로 인자를 넘긴다.
  • 함수 내부에서는 키워드와 값이 key: value 형식으로 출력된다.

실행 결과:

name: 홍길동
age: 30
city: 서울

즉, **kwargs는 이름이 있는 인자(키워드 인자)들을 함수에 넘길 때 사용된다.

4.3. *args와 **kwargs를 함께 사용하는 경우

함수에서 *args**kwargs를 동시에 사용할 수 있다. 이때는 위치 인자가 먼저 오고, 그 뒤에 키워드 인자가 와야 한다.

예시:

def print_all(*args, **kwargs):
    print("위치 인자:", args)
    print("키워드 인자:", kwargs)

print_all(1, 2, 3, name="홍길동", age=30)

설명:

  • *args는 위치 인자(1, 2, 3)를 처리하고, **kwargs는 키워드 인자(name="홍길동", age=30)를 처리한다.
  • 함수 내부에서 각각의 인자가 어떻게 전달되었는지 확인할 수 있다.

실행 결과:

위치 인자: (1, 2, 3)
키워드 인자: {'name': '홍길동', 'age': 30}

위치 인자는 튜플로, 키워드 인자는 딕셔너리로 처리된다.


4.4. 정리

  • *args: 임의의 개수의 위치 인자를 처리하며, 함수 내부에서는 튜플로 처리된다.
  • **kwargs: 임의의 개수의 키워드 인자를 처리하며, 함수 내부에서는 딕셔너리로 처리된다.
  • 두 가지를 함께 사용하면 매우 유연한 함수를 만들 수 있으며, 특히 데코레이터에서 다양한 인자를 가진 함수들을 꾸밀 때 매우 유용하다.

return (추가)

return result는 데코레이터 안에서 원래 함수의 실행 결과를 반환하는 중요한 역할을 한다. 데코레이터가 함수의 동작을 감싸면서도 원래 함수의 결과를 그대로 유지하려면 반드시 return result로 원래 함수의 결과를 반환해야 한다. 그렇지 않으면, 데코레이터로 감싼 함수가 호출된 후 결과를 잃어버리게 된다.

  • result = original_function(*args, **kwargs): 데코레이터 안에서 원래 함수를 호출하고 그 결과를 result에 저장한다.
  • return result: 원래 함수가 반환하는 값을 그대로 반환한다. 이를 통해 원래 함수의 결과가 데코레이터를 통해 변하지 않고 그대로 유지된다.

반환값이 있는 함수에서도 데코레이터가 정상적으로 동작하기 위해, 반드시 result를 반환해줘야 한다.


회고 : 휴일에 시간적 여유가 생겨서 파이썬을 공부하다가 놓치거나, 부족한 부분을 공부하고 글을 작성했다. 심적 여유가 있으니 애매하던 것도 이해가 잘 됐고 다른 매체들을 충분히 활용하여 공부할 수 있었다. 잘 정리해두고 두고두고 볼 생각으로 정리했으니 자주 봤으면 하는 바람이 있는데 자주 볼런진 아직 모르겠다.ㅎ

profile
안녕하세요.

0개의 댓글

관련 채용 정보