본문 바로가기

파이썬

클로저와 데코레이터, 이터레이터와 제너레이터, 파이썬 타입 어노테이션

1. 클로저(closure)

클로저란 외부 함수 안에 내부 함수를 구현하고 이 내부 함수를 반환하는 함수를 말한다.

■ 예를 들어 클래스를 이용하여 덧셈 기능을 수행할 수 있는 클래스를 만든다면, 다음과 같이 클래스로부터 객체를 생성하고 생성한 객체에 클래스의 메소드를 호출해 덧셈을 계산할 수 있다.

class Add:
    def __init__(self, first):
        self.first = first
        
    def add(self, second):
        return self.first + second
        
a1 = Add(1)
a1.add(2)
```#결과#```
3
````````````

혹은 다음과 같이 if __name__ = "__main__":을 이용하여 덧셈을 계산할 수도 있다.

if __name__ == "__main__":
    a3, a4 = Add(3), Add(4)
    print(a3.add(3))
    print(a4.add(4))
    
```#결과#```
6
8
````````````

혹은 Add 클래스의 add 메소드를 __call__ 메소드로 바꾼다면, __call__ 메소드는 클래스로부터 생성된 객체에 인수를 전달하여 호출할 수 있기 때문에 객체를 만든 다음, 객체에 클래스의 메소드를 적용시키지 않아도 계산을 할 수 있다.

class Add2:
    def __init__(self, first):
        self.first = first
        
    def __call__(self, second):
        return self.first + second
        
if __name__ == "__main__":
    a5, a6 = Add2(5), Add2(6)
    print(a5(5))
    print(a6(6))

```#결과#```
10
12
`````````````

a10 = Add2(10)
a10(10)
```#결과#```
20
````````````

 

■ 이와 같이 일반적으로 클래스를 만들어서 사용하지만, 다음과 같이 클로저 함수를 만들면 더 간단하게 계산할 수 있다.

■ 먼저 내부 함수를 반환할 외부 함수를 정의하고, 이 외부 함수 안에 실제로 계산을 수행할 내부 함수를 정의하면 된다.

def Add3(first):
    def wrapper(second):
        return first + second
    return wrapper
if __name__ == "__main__":
    a7,a8 = Add3(7), Add2(8)
    
    print(a7(7))
    print(a8(8))
    
```#결과#```
14
16
````````````

a9 = Add3(9)
a9(9)
```#결과#```
18
````````````

 

- 클로저의 외부 함수인 Add3에서 내부 함수인 wrapper를 반환할 때 Add3 함수 호출 시 받은 'first = 9'라는 값을 내부 함수 wrapper에서 저장하고 계산을 수행하는 것을 볼 수 있다.

- 이는 특정 값을 설정한 Add2 클래스로부터 생성한 객체를 만드는 과정과 비교했을 때, Add2 클래스를 사용하여 덧셈을 계산하는 작동 방식( ex) a10 = Add2(10) ) 과 클로저(Add3)를 사용해 덧셈을 계산하는 작동 방식( ex) a9 = Add3(9) ) 이 매우 유사하다. 

 

2. 데코레이터(decorator)

■ 데코레이터는 기존 함수를 바꾸지 않고 추가 기능을 구현할 때 사용한다. 

■ 예를 들어 두 객체를 더하는 함수를 이용해서 넘파이 배열을 더한다면 다음과 같이 

import numpy as np

def add(a, b):
    return a + b
    
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = add(a, b)

c
```#결과#```
array([5, 7, 9])
````````````

print(a.shape, b.shape)
print(c.shape)
```#결과#```
(3,) (3,)
(3,)
````````````

 

원소 3개를 갖는 벡터 형태끼리의 원소가 더해져서 5, 6 ,7이라는 원소 3개 갖는 새로운 벡터 형태가 반환되는 것을 볼 수 있다.

■ 만약, 기존 함수인 add 함수를 수정하지 않으면서, 벡터 형태를 두 개 더했을 때, 행렬 형태로 변환되는 추가 기능을 구현하고자 한다면 다음과 같이 할 수 있다.

def stack_arrays(func):
    def wrapper(i, j, k):
        return np.vstack((i, j, k))  # 원래의 덧셈 결과를 무시하고 배열을 수직으로 결합
    return wrapper
@stack_arrays # 데코레이터 적용
def add(a, b, c):
    return a + b + c
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
arr3 = np.array([7, 8, 9])
result_2 = add(arr1, arr2, arr3)

result_2
```#결과#```
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
````````````

print(result_2.shape)
```#결과#```
(3, 3)
````````````

- 데코레이터를 이용하여 add 함수를 이용하니 기존과 달리 3 by 3 matrix 형태가 반환되는 것을 볼 수 있다.

- stack_arrays라는 함수를 만들어 기존에 정의한 add 함수 위에 '@+함수명' 형태로 @stack_arrays라는 데코레이터를 추가했다. 이렇게 함수 위에 '@+함수명'이 있으면 파이썬은 이를 데코레이터 함수로 인식하게 되어서 add 함수는 stack_arrays 데코레이터를 통해 다음과 같은 방식으로 수행된다.

- @stack_arrays를 붙인 순간, add 함수를 호출해도 add 함수가 아닌 stack_arrays 함수가 실행된다. 

- 이때, stack_arrays 함수의 인자로 add 함수를 받으며, 내부 함수인 wrapper에서 계산하고 반환하기 때문에 사실상 add 함수는 wrapper 함수로 대체된 것이라 볼 수 있다. 

따라서 add(arr1, arr2, arr3)를 호출하면 실제로는 wrapper(arr1, arr2, arr3)이 호출되는 것이다.

■ 함수에는 여러 개의 데코레이터를 지정할 수 있으며, 데코레이터가 여러 개일 경우, 데코레이터가 실행되는 순서는 위에서 아래 순이다.

def print_matrix(func):
    def wrapper(*args, **kwargs):
        print('3 by 3 matrix') 
        return func(*args, **kwargs)
    return wrapper
    
@print_matrix
@stack_arrays
def add(a, b, c):
    return a + b + c
    
result_3 = add(arr1, arr2, arr3)
```#결과#```
3 by 3 matrix
````````````

print(result_3)
```#결과#```
[[1 2 3]
 [4 5 6]
 [7 8 9]]
 ```````````

- 데코레이터 실행 순서가 위에서 아래이기 때문에 먼저 @print_matrix가 실행되고 @stack_arrays가 실행된다.

- print_matrix의 내부 함수 wrapper의 인수를(*args, **kwargs)로 설정한 이유는 기존 함수의 입력 인수에 상관없이 동작하도록 만들기 위해서 *args와 **kwargs 매개변수를 이용한 것이다. 이렇게 함수에 *args와 **kwargs 매개변수를 지정하면 다양한 입력 인수를 모두 처리할 수 있다.

- 만약, 데코레이터로 감쌀 함수가 고정된 인수를 받는 다면, *args와 **kwargs 매개변수 대신 다음과 같이 그 함수의 인수들을 맞춰 사용해도 된다.

def print_matrix2(func):
    def wrapper(i, j, k):
        print('3 by 3 matrix2') 
        return func(i, j, k)
    return wrapper
    
@print_matrix2
@stack_arrays
def add(a, b, c):
    return a + b + c
    
result_4 = add(arr1, arr2, arr3)
```#결과#```
3 by 3 matrix2
````````````

print(result_4)
```#결과#```
[[1 2 3]
 [4 5 6]
 [7 8 9]]
 ```````````

 

3. 이터레이터(iterator)

이터레이터는 데이터의 항목에 차례대로 접근할 수 있는 객체를 말하며, 파이썬에서는 반복 가능한 객체(iterable)를 이터레이터로 만들 수 있다.

■ 중요한 것은 반복 가능하다고 해서 이터레이터가 되는 것은 아니다. 대표적인 반복 가능한 객체(iterable)로 문자열, 리스트, 튜플, 딕셔너리 등이 있다. 이들을 생성하고 next( ) 함수를 적용하면, 다음과 같이 이터레이터가 아니라는 오류가 발생한다.

a = [1, 2, 3, 4]
b = (1, 2, 3, 4)
c = 'string'
d = {'a':1,'b':2, 'c':3}

next(a)
```#결과#```
TypeError                                 Traceback (most recent call last)
----> 1 next(a)

TypeError: 'list' object is not an iterator
`````````````

next(b)
```#결과#```
TypeError                                 Traceback (most recent call last)
----> 1 next(b)

TypeError: 'tuple' object is not an iterator
`````````````

next(c)
```#결과#```
TypeError                                 Traceback (most recent call last)
----> 1 next(c)

TypeError: 'str' object is not an iterator
````````````

next(d)
```#결과#```
TypeError                                 Traceback (most recent call last)
----> 1 next(d)

TypeError: 'dict' object is not an iterator
`````````````

■ 따라서 파이썬에서 이터레이터를 만들기 위해선 반복 가능한 객체(iterable)에 iter( ) 함수를 적용시켜야 한다.

iter_a, iter_b, iter_c, iter_d = iter(a), iter(b), iter(c), iter(d)
print(type(iter_a), type(iter_b), type(iter_c), type(iter_d))
```#결과#```
<class 'list_iterator'> <class 'tuple_iterator'> <class 'str_ascii_iterator'> <class 'dict_keyiterator'>
````````````

■ 이터레이터의 요소를 확인하는 방법은 next 함수와 for 문을 사용하는 방법이 있으며, next 함수는 적용할 때마다 이터레이터의 요소를 순서대로 반환한다. 만약, 더 이상 반환할 요소가 없다면 StopIteration 예외가 발생한다.

print(next(iter_a), next(iter_b), next(iter_c), next(iter_d))
print(next(iter_a), next(iter_b), next(iter_c), next(iter_d))
print(next(iter_a), next(iter_b), next(iter_c), next(iter_d))
print(next(iter_a), next(iter_b), next(iter_c), next(iter_d))
```#결과#```
1 1 s a
2 2 t b
3 3 r c
StopIteration                             Traceback (most recent call last)
----> 1 print(next(iter_a), next(iter_b), next(iter_c), next(iter_d))

StopIteration: 
``````````````

■  for 문을 이용하면 StopIteration 예외가 발생하지 않지만, for 문을 한 번 이용하여 반복이 끝난 후에는 다시 반복해도 더 이상 이터레이터 요소를 반환하지 않는다. 즉 for 문이나 next 함수를 이터레이터에 적용해서 이터레이터의 요소를 한 번 반환하면, 다시 반환하지 않는 특징이 있다.

iter_a, iter_b, iter_c, iter_d = iter(a), iter(b), iter(c), iter(d)

for i in iter_a:
    print(i, end = "")
    
for i in iter_b:
    print(i, end = "")
    
for i in iter_c:
    print(i, end = "")
    
for i in iter_d:
    print(i, end = "")

```#결과#```
1234
1234
string
abc
`````````````
    
for i in iter_a:
    print(i, end = "")
    
for i in iter_b:
    print(i, end = "")
    
for i in iter_c:
    print(i, end = "")
    
for i in iter_d:
    print(i, end = "")
    
```#결과#```

````````````

■ 이터레이터의 특징은 1) 자기 자신을 반환하며, 2) 다음 반복을 위한 값을 반환하고 더 이상 값이 없으면 StopIteration 예외가 발생한다는 점이다.

■ 이러한 특징을 바탕으로 __iter__() 메소드와 __next__() 메소드를 사용하여 iter 함수와 동일한 기능을 수행하는 클래스를 만들 수 있다.

■ __iter__ 메소드를 클래스에 구현하면, 해당 클래스로 생성된 객체는 반복 가능한 객체가 되며, __next__ 메소드는 반복 가능한 객체의 요소를 차례대로 반환하는 역할을 수행한다.

class Iterator:
    def __init__(self, item):
        self.item = item
        self.pos = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if len(self.item) <= self.pos: # 더 이상 항목이 없으면 StopIteration 예외 발생
            raise StopIteration
        result = self.item[self.pos]
        self.pos += 1
        return result
        
Iterator_a = Iterator([1, 2, 3, 4])

print(next(Iterator_a))
print(next(Iterator_a))
print(next(Iterator_a))
print(next(Iterator_a))
```#결과#```
1
2
3
4
````````````

print(next(Iterator_a))
```#결과#```
StopIteration                             Traceback (most recent call last)
Cell In[83], line 1
----> 1 print(next(Iterator_a))

Cell in Iterator.__next__(self)
      9 def __next__(self):
     10     if len(self.item) <= self.pos: # 더 이상 항목이 없으면 StopIteration 예외 발생
---> 11         raise StopIteration
     12     result = self.item[self.pos]
     13     self.pos += 1

StopIteration: 
```````````````

 

4. 제너레이터(generator)

■ 제너레이터는 yield 키워드를 사용해서 함수로부터 이터레이터를 생성하는 방법이다.

def Gen():
    yield 'a'
    yield 'first'
    yield 'b'
    yield 'second'
    
g = Gen()

print(type(g))
```#결과#```
<class 'generator'>
````````````

for g in Gen():
    print(g, end = " ")
    
```#결과#```
a first b second 
````````````

print(next(g))
print(next(g))
print(next(g))
print(next(g))
```#결과#```
a
first
b
second
```````````

print(next(g))
```#결과#```
StopIteration                             Traceback (most recent call last)
----> 1 print(next(g))

StopIteration: 
`````````````

■ 제너레이터를 def 함수를 이용해 만들 수도 있지만, 튜플 표현식으로 만들 수도 있다. 이를 제너레이터 표현식이라고 한다.

def Gen():
    for a in range(1, 3):
        for b in range(1, 3):
            if (a % 2 == 0):
                result = a + b
            else:
                result = a - b
            yield result
            
 g = Gen()
 
 print(next(g))
 print(next(g))
 print(next(g))
 print(next(g))
 ```#결과#```
 0
-1
3
4
``````````````

print(next(g))
```#결과#```
StopIteration                             Traceback (most recent call last)
----> 1 print(next(g))

StopIteration: 
````````````

위에서 함수로 정의한 제너레이터를 다음과 같이 제너레이터 표현식으로 나타낼 수 있다.

gen = ((a + b if a % 2 == 0 else a - b) for a in range(1, 3) for b in range(1, 3))

 for result in gen:
    print(result)

```#결과#```
0
-1
3
4
`````````````

이터레이터와 제너레이터는 유사하지만, 이터레이터를 클래스 기반으로 작성하면 더 복잡한 기능을 구현할 수 있다.

반면, 제너레이터를 사용하면 간단하게 이터레이터를 만들 수 있으므로, 이터레이터의 복잡도에 따라 클래스를 이용할지, 제너레이터를 사용할지 선택하면 된다. 간단한 경우에는 가독성이 좋은 제너레이터 표현식을 사용하는 것이 더 좋다.

 

5. 파이썬 타입 어노테이션(type annotation)

■ 파이썬은 프로그램 실행 중에 변수의 타입을 동적으로 바꿀 수 있어 파이썬을 동적 프로그래밍 언어라고 한다.

■ 예를 들어 정수 객체를 참조하는 변수를 만든 다음, 이 정수 객체를 문자열 객체로 변경하면 변수는 문자열 객체를 참조한다.

a = 123
print(type(a))
a = '123'
print(type(a))

```#결과#```
<class 'int'>
<class 'str'>
````````````

이렇게 실행 중에 변수의 타입을 동적으로 바꿀 수 있다.

■ 반면, 자바같은 정적 프로그래밍 언어는 변수의 타입을 선언하면 선언한 타입 외에 다른 타입은 사용할 수 없다. 따라서 동적 언어는 실행 중에 타입을 변경하면 실행 결과가 달라지는 것에 비해 정적 언어는 이에 대한 안정성을 가지고 있다.

■ 이런 단점을 극복하기 위해 등장한 것이 타입 어노테이션이며, 타입 어노테이션은 정적 언어처럼 타입을 확인하는 게 아니라 '이 변수의 타입은 어떤 것이다'라는 타입에 대한 힌트를 명시하는 정도의 기능만 지원한다. 따라서 타입 어노테이션으로 변수의 타입을 명시해도 다른 타입의 인수를 입력으로 받을 수 있다.

■ 타입 어노테이션은 다음과 같이 사용한다.

num: int = 123

def type_annotation_add(a: int, b: float, c: int) -> int:
    return a + b + c


num2 = type_annotation_add(a=1.2, b=1.2, c=3)
print(type(num)); print(num2); type(num2)
```#결과#```
<class 'int'>
5.4
float
````````````

- 위와 같이 변수명 바로 뒤에 콜론과 타입을 명시하여 변수가 어떤 타입인지 명시할 수 있고, 함수의 매개변수에도 동일하게 타입을 명시할 수 있다. 그리고 -> int처럼 함수의 리턴 값의 타입도 명시할 수 있다.

- 그러나 명시하는 정도의 역할이므로 다른 타입의 인수를 입력받고 다른 타입의 값을 리턴할 수 있다.

■ 만약 파입 어노테이션보다 좀 더 적극적으로 타입을 확인하고 싶으면 정적 타입 검사를 제공하는 mypy를 사용하면 된다.

사용 방법은 다음과 같이 타입을 검사할 파일을 저장하고 mypy를 적용하면 된다.

예를 들어 위의 type_annoation_add 가 mypy_test라는 파일에 저장되어 있다면 다음과 같이 타입을 검사할 수 있다.

mypy test_mypy.py
```#결과#```
test_mypy.py:5: error: Argument "a" to "type_annotation_add" has incompatible type "float"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
````````````

코드를 다음과 같이 타입 어노테이션으로 명시한 타입에 알맞게 변경하여 타입 검사를 실행하면 오류가 없는 것을 확인할 수 있다.

def type_annotation_add(a: int, b: int, c: int) -> int:
    return a + b + c
    
num2 = type_annotation_add(a = 1, b = 2, c = 3)
print(num2)
mypy test_mypy.py
```#결과#```
Success: no issues found in 1 source file
`````````````

'파이썬' 카테고리의 다른 글

객체와 클래스  (0) 2025.04.08
예외 처리  (0) 2024.09.11
내장 함수, 정렬과 탐색, 람다식  (1) 2024.09.07
함수  (1) 2024.09.06
파일 읽고 쓰기  (0) 2024.09.03