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
`````````````