파이썬의 이터레이터(iterator) 개념을 코드 예시를 통해 하나씩 살펴본다. 이터레이터는 데이터를 순차적으로 하나씩 반환하는 객체로, 데이터의 반복 처리에 중요한 역할을 한다.
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)
클래스 정의: class MyIterator:
MyIterator
라는 이름의 클래스를 정의한다. 이 클래스는 이터레이터 역할을 하면서 데이터를 하나씩 반환할 수 있게 한다.생성자 메서드: def __init__(self, data):
__init__
은 클래스가 생성될 때 호출되는 생성자 메서드다.data
매개변수는 이터레이터가 순차적으로 다룰 데이터를 저장하는 역할을 한다.데이터 저장: self.data = data
self.data
에 저장한다. 이 데이터는 이터레이터가 접근할 리스트나 시퀀스다.인덱스 초기화: self.index = 0
index
는 현재 데이터를 반환할 위치를 나타낸다. 처음에는 0으로 시작해서 데이터를 첫 번째 항목부터 접근한다.이터러블 반환: def __iter__(self):
__iter__
메서드는 파이썬에서 이 객체가 이터러블임을 나타내며, 이 메서드가 호출되면 self
를 반환한다. 이렇게 해서 이 객체가 이터레이터로 사용될 수 있게 된다.다음 값 반환: def __next__(self):
__next__
메서드는 이터레이터가 next()
함수를 호출할 때 실행된다. 이 메서드는 이터레이터가 다음 값을 반환할 때 호출되는 핵심 메서드다.조건문: if self.index < len(self.data):
현재 데이터 저장: result = self.data[self.index]
result
에 저장한다. 예를 들어, 처음에는 self.data[0]
의 값을 가져온다.result
변수는 현재 반환할 값을 임시로 저장하는 역할을 한다. 이 변수에 저장된 값을 통해 인덱스를 사용해 직접 self.data
에 접근하지 않고도 데이터를 반환할 수 있다. 이렇게 하면 코드의 가독성을 높이고, 데이터를 반환하기 전에 추가적인 처리를 할 수 있는 여지를 남긴다.인덱스 증가: self.index += 1
__next__()
가 호출될 때 다음 데이터를 가져올 수 있게 한다.값 반환: return result
result
변수에 저장된 현재 인덱스의 데이터를 반환한다. 이 반환된 값이 for
루프에서 사용된다. for
루프에서 item
변수에 각각의 값을 저장해서 반복적으로 처리할 수 있다.반복 종료: else: raise StopIteration
index
가 data
리스트의 길이와 같거나 커지면 더 이상 반환할 값이 없으므로 StopIteration
예외를 발생시켜서 이터레이션이 끝났음을 알린다. 이 예외는 for
루프가 자동으로 처리해서 루프를 종료시킨다.my_iter = MyIterator([1, 2, 3])
for item in my_iter:
print(item)
이 코드는 다음과 같이 출력된다:
1
2
3
이터레이터를 사용하면 데이터의 각 항목을 순차적으로 처리할 수 있어서 반복 작업을 간단하게 수행할 수 있다.
제너레이터와 함께 자주 사용되는 키워드 중 하나가 yield
다. 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
return
: 함수의 실행을 종료하고 값을 반환한다. 함수가 호출될 때마다 새로운 실행이 시작된다.yield
: 함수의 상태를 유지하면서 값을 반환하고, 함수가 재개될 수 있도록 한다. 이는 함수가 일시 중단된 상태를 기억하고, 이후에 이어서 실행할 수 있도록 해준다.yield
를 사용하면 제너레이터 객체를 생성하게 되고, 이 객체는 이터레이터처럼 동작하여 값을 하나씩 반환한다. 제너레이터는 메모리 효율성이 높으며, 많은 데이터를 다룰 때 매우 유용하다.
제너레이터 표현식은 제너레이터 함수를 간결하게 작성할 수 있는 방법으로, 리스트 컴프리헨션과 유사한 문법을 사용하지만, 메모리 효율성을 위해 데이터를 한 번에 하나씩 생성하는 방식이다.
gen_exp = (x * x for x in range(5))
이 코드는 제너레이터 표현식이다.
x * x for x in range(5)
부분은 range(5)
에서 값을 하나씩 가져와서 그 값을 제곱(x * x
)한 값을 순차적으로 생성한다.()
로 감싸져 있다. 이 덕분에 값들이 메모리에 미리 저장되지 않고, 필요할 때마다 하나씩 생성된다.next()
함수를 호출하거나, for
루프를 통해 값을 하나씩 순차적으로 얻을 수 있다.제너레이터 표현식은 리스트 컴프리헨션과 매우 비슷해 보이지만, 큰 차이점이 있다.
list_comp = [x * x for x in range(5)]
range(5)
에서 값을 가져와 모든 결과를 메모리에 저장한다. 즉 리스트가 완성된 후에 메모리에 저장된 값들이 사용된다.gen_exp = (x * x for x in range(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
루프는 종료된다.
제너레이터 표현식을 사용하는 주요 이유는 메모리 효율성과 지연 평가 방식이다.
list_comp = [x * x for x in range(1000000)] # 1,000,000개의 제곱 값을 메모리에 저장
gen_exp = (x * x for x in range(1000000)) # 1,000,000개의 제곱 값을 하나씩 생성
예를 들어, 큰 텍스트 파일의 각 줄을 처리해야 한다고 가정해 보자. 리스트 컴프리헨션을 사용하면 모든 줄을 메모리에 한꺼번에 저장해야 하지만, 제너레이터 표현식을 사용하면 한 줄씩 읽고 처리할 수 있다.
# 리스트 컴프리헨션을 사용할 경우
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)
제너레이터 표현식은 파일에서 한 줄씩 읽어 메모리 사용을 최소화하며 처리한다. 큰 파일을 처리할 때 특히 유용하다.
이번 글에서는 파이썬의 매우 강력한 기능 중 하나인 데코레이터(Decorator)에 대해 알아보자. 데코레이터는 함수를 꾸며주는 역할을 하며, 기존 함수에 새로운 기능을 추가할 수 있다. 복잡하게 들릴 수 있지만, 천천히 예제를 통해 하나씩 살펴보면 쉽게 이해할 수 있을 것이다.
데코레이터는 함수를 다른 함수로 감싸는 역할을 한다. 어떤 함수를 꾸미고 싶을 때, 그 함수의 동작을 바꾸지 않고 추가적인 기능을 넣을 수 있다.
예제:
def hello():
# 'Hello, World!'를 출력하는 간단한 함수
print("Hello, World!")
위 함수는 "Hello, World!"를 출력하는 간단한 함수다. 이제 이 함수에 데코레이터를 적용해, 함수가 실행되기 전과 후에 메시지를 추가로 출력해보자.
데코레이터 없이 함수의 동작을 변경하는 방식을 보자. 새로운 함수를 만들고, 그 안에서 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_hello
에 hello
함수를 데코레이터 함수로 꾸며서 실행한 것이다.실행 결과:
함수 실행 전입니다.
Hello, World!
함수 실행 후입니다.
이렇게 함수를 감싸서 새로운 동작을 추가할 수 있다.
위 예제는 데코레이터를 직접 호출해서 사용했다. 하지만 파이썬에서는 @
문법을 사용하여 쉽게 데코레이터를 적용할 수 있다.
@decorator_function
# 'hello' 함수에 데코레이터 적용
def hello():
print("Hello, World!")
hello() # 데코레이터가 적용된 함수 호출
설명:
@decorator_function
을 hello()
함수 위에 작성하면, hello
함수는 자동으로 decorator_function
으로 꾸며진다.hello()
를 호출하면 자동으로 데코레이터가 적용된 함수가 실행된다.실행 결과:
함수 실행 전입니다.
Hello, World!
함수 실행 후입니다.
데코레이터는 "어떤 함수의 실행을 가로채서 전후에 다른 작업을 수행하는 함수"라고 이해하면 된다. 이 방법을 통해 코드의 반복을 줄이면서 공통적인 동작을 쉽게 추가할 수 있다.
예를 들어, 함수를 실행하기 전후로 로깅하거나 실행 시간을 측정하는 등의 동작을 추가할 수 있다.
이제 데코레이터의 예제를 보자. 아래 코드는 함수가 실행되는 데 걸리는 시간을 측정하는 데코레이터다.
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초
위 예제들은 매개변수가 없는 함수에 적용했지만, 매개변수가 있는 함수에도 데코레이터를 적용할 수 있다. 이를 위해 *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입니다.
함수 실행 후입니다.
*args
와 **kwargs
는 파이썬 함수에서 임의의 개수의 인자를 처리할 수 있게 해주는 기능이다. 이 기능을 사용하면 함수가 몇 개의 인자를 받을지 미리 알 수 없거나, 다양한 개수의 인자를 유연하게 받아 처리할 수 있다.
*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
는 위치 인자들을 개수에 상관없이 함수에 넘길 수 있도록 해준다.
**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
는 이름이 있는 인자(키워드 인자)들을 함수에 넘길 때 사용된다.
함수에서 *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}
위치 인자는 튜플로, 키워드 인자는 딕셔너리로 처리된다.
*args
: 임의의 개수의 위치 인자를 처리하며, 함수 내부에서는 튜플로 처리된다.**kwargs
: 임의의 개수의 키워드 인자를 처리하며, 함수 내부에서는 딕셔너리로 처리된다.return result
는 데코레이터 안에서 원래 함수의 실행 결과를 반환하는 중요한 역할을 한다. 데코레이터가 함수의 동작을 감싸면서도 원래 함수의 결과를 그대로 유지하려면 반드시 return result
로 원래 함수의 결과를 반환해야 한다. 그렇지 않으면, 데코레이터로 감싼 함수가 호출된 후 결과를 잃어버리게 된다.
result = original_function(*args, **kwargs)
: 데코레이터 안에서 원래 함수를 호출하고 그 결과를 result
에 저장한다.return result
: 원래 함수가 반환하는 값을 그대로 반환한다. 이를 통해 원래 함수의 결과가 데코레이터를 통해 변하지 않고 그대로 유지된다.반환값이 있는 함수에서도 데코레이터가 정상적으로 동작하기 위해, 반드시 result
를 반환해줘야 한다.
회고 : 휴일에 시간적 여유가 생겨서 파이썬을 공부하다가 놓치거나, 부족한 부분을 공부하고 글을 작성했다. 심적 여유가 있으니 애매하던 것도 이해가 잘 됐고 다른 매체들을 충분히 활용하여 공부할 수 있었다. 잘 정리해두고 두고두고 볼 생각으로 정리했으니 자주 봤으면 하는 바람이 있는데 자주 볼런진 아직 모르겠다.ㅎ