소개
이 실습에서는 NumPy 의 유니버설 함수 (Universal Functions), 흔히 ufuncs 라고 불리는 함수의 기본 사항을 배우게 됩니다. ufuncs 는 Python 에서 고성능 컴퓨팅의 초석이며, 전체 데이터 배열에 대해 빠르고 요소별 연산을 수행할 수 있게 해줍니다. 기본적인 산술 연산, 강력한 브로드캐스팅 (broadcasting) 개념, 집계 (aggregation) 메서드, 그리고 결과의 데이터 타입을 제어하는 방법을 다룰 것입니다. 이 실습이 끝나면 ufuncs 를 사용하여 더 깔끔하고 효율적인 데이터 처리 코드를 작성할 수 있게 될 것입니다.
Ufuncs 를 사용한 기본 산술 연산
핵심적으로 ufuncs 는 요소별 (element-wise) 연산을 수행합니다. 이는 두 배열에 연산을 적용할 때, 해당 연산이 각 대응되는 요소 쌍에 대해 수행됨을 의미합니다. 가장 일반적인 ufuncs 는 +, -, *, /와 같은 표준 산술 연산자입니다.
두 NumPy 배열에 대한 간단한 덧셈을 수행하는 것으로 시작하겠습니다.
먼저, 왼쪽 파일 탐색기에서 ufunc_examples.py 파일을 엽니다. 기존 내용을 다음 코드로 바꿉니다. 이 코드는 NumPy 를 가져오고, 두 배열을 생성하고, 이를 더합니다.
import numpy as np
## 두 배열 생성
arr1 = np.array([0, 2, 3, 4])
arr2 = np.array([1, 1, -1, 2])
## '+' 연산자는 배열을 요소별로 더하는 ufunc 입니다.
result = arr1 + arr2
## 결과 출력
print("Step 1 Result:")
print(result)
코드를 추가한 후 파일을 저장합니다. 이제 터미널에서 스크립트를 실행하여 출력을 확인합니다.
python ufunc_examples.py
요소별 덧셈의 결과를 보게 될 것입니다.
Step 1 Result:
[1 3 2 6]
이는 ufunc 의 기본적인 동작을 보여줍니다: arr1[0]은 arr2[0]에 더해지고, arr1[1]은 arr2[1]에 더해지는 식으로, 결과로 새로운 배열을 생성합니다.
브로드캐스팅 활용
브로드캐스팅은 NumPy 가 산술 연산 중에 서로 다른 모양 (shape) 을 가진 배열을 다룰 수 있게 해주는 강력한 메커니즘입니다. 내부적으로 NumPy 는 호환 가능한 모양을 갖도록 더 작은 배열을 더 큰 배열에 "브로드캐스트"합니다.
일반적인 예시는 배열의 모든 요소에 단일 숫자를 곱하는 것입니다. 또한 1D 배열이 2D 배열에 브로드캐스트되는 더 복잡한 경우도 살펴보겠습니다.
ufunc_examples.py 파일을 수정합니다. 스크립트 끝에 다음 코드를 추가합니다.
## --- Step 2 를 위한 추가 코드 ---
## 스칼라 (scalar) 를 배열에 브로드캐스팅
arr1 = np.array([1, 2, 3])
scalar_result = arr1 * 10
print("\nStep 2 결과 (스칼라 브로드캐스트):")
print(scalar_result)
## 1D 배열을 2D 배열에 브로드캐스팅
arr2d = np.array([[1], [2], [3]]) ## Shape (3, 1)
arr1d = np.array([1, 2, 3]) ## Shape (3,)
broadcast_result = arr2d * arr1d
print("\nStep 2 결과 (배열 브로드캐스트):")
print(broadcast_result)
파일을 저장하고 터미널에서 다시 실행합니다.
python ufunc_examples.py
Step 1 과 Step 2 에 대한 출력을 모두 보게 될 것입니다.
Step 1 Result:
[1 3 2 6]
Step 2 Result (Scalar Broadcast):
[10 20 30]
Step 2 Result (Array Broadcast):
[[1 2 3]
[2 4 6]
[3 6 9]]
두 번째 예시에서는 1D 배열 arr1d(shape (3,)) 와 2D 배열 arr2d(shape (3, 1)) 가 요소별 곱셈이 수행되기 전에 공통 모양인 (3, 3)으로 브로드캐스트됩니다.
.reduce() 를 사용한 배열 집계
요소별 연산 외에도 ufuncs 는 집계 연산을 수행하는 특별한 메서드를 가지고 있습니다. .reduce() 메서드는 가장 유용한 것 중 하나입니다. 이 메서드는 지정된 축 (axis) 을 따라 ufunc 를 반복적으로 적용하여 최종적으로 하나의 차원만 남깁니다.
예를 들어, np.add.reduce(arr)는 np.sum(arr)과 동일합니다. 2D 배열에서 어떻게 작동하는지 살펴보겠습니다.
ufunc_examples.py 파일에 다음 코드를 추가합니다.
## --- Step 3 을 위한 추가 코드 ---
## 3x3 배열 생성
arr = np.arange(9).reshape(3, 3)
print("\nStep 3 원본 배열:")
print(arr)
## axis=1 (열) 을 따라 배열을 축소 (reduce) 하여 합산합니다.
## 각 행의 요소들을 더하게 됩니다.
## 행 0: 0 + 1 + 2 = 3
## 행 1: 3 + 4 + 5 = 12
## 행 2: 6 + 7 + 8 = 21
reduced_result = np.add.reduce(arr, axis=1)
print("\nStep 3 결과 (axis=1 에 대한 reduce):")
print(reduced_result)
파일을 저장하고 실행합니다.
python ufunc_examples.py
이제 출력에 이 단계의 결과가 포함됩니다.
... (이전 출력) ...
Step 3 Original Array:
[[0 1 2]
[3 4 5]
[6 7 8]]
Step 3 Result (reduce on axis=1):
[ 3 12 21]
보시다시피, .reduce()는 지정된 축을 따라 배열의 요소에 add 연산을 적용하여 배열을 축소했습니다.
출력 데이터 타입 지정
NumPy 는 일반적으로 출력 배열의 데이터 타입을 자동으로 결정합니다. 하지만 dtype 인자를 사용하여 출력 데이터 타입을 명시적으로 지정할 수 있습니다. 이는 메모리 사용량을 제어하거나 수치적 정밀도를 보장하는 데 유용합니다.
곱셈을 사용하여 축소 연산을 수행하고, 입력이 정수 배열임에도 불구하고 출력을 부동소수점 (floating-point) 숫자로 강제해 보겠습니다.
ufunc_examples.py 파일 끝에 다음 코드를 추가합니다.
## --- Step 4 를 위한 추가 코드 ---
## Step 3 과 동일한 3x3 배열 사용
arr = np.arange(1, 10).reshape(3, 3) ## 0 을 곱하는 것을 피하기 위해 1-9 사용
print("\nStep 4 원본 배열:")
print(arr)
## 곱셈으로 축소하고, 출력을 float 으로 형변환합니다.
## 행 0: 1 * 2 * 3 = 6
## 행 1: 4 * 5 * 6 = 120
## 행 2: 7 * 8 * 9 = 504
multiply_result = np.multiply.reduce(arr, axis=1, dtype=float)
print("\nStep 4 결과 (dtype=float 를 사용한 multiply.reduce):")
print(multiply_result)
스크립트를 저장하고 실행합니다.
python ufunc_examples.py
Step 4 의 출력을 관찰합니다.
... (이전 출력) ...
Step 4 Original Array:
[[1 2 3]
[4 5 6]
[7 8 9]]
Step 4 Result (multiply.reduce with dtype=float):
[ 6. 120. 504.]
출력 배열 [ 6. 120. 504.]의 끝에 있는 점 (.) 에 주목하십시오. 이는 dtype=float로 지정했기 때문에 요소들이 이제 부동소수점 숫자임을 나타냅니다.
Ufunc 동작 재정의
NumPy 의 ufunc 시스템은 확장 가능합니다. ufunc 가 어떻게 작동해야 하는지 정의하는 자체 배열과 유사한 객체 (array-like objects) 를 만들 수 있습니다. 이는 일반적으로 NumPy 의 ndarray를 서브클래싱하고 __add__ ( + 연산자용) 와 같은 특수 메서드를 재정의하여 수행되는 고급 기능입니다.
덧셈 연산이 수행될 때마다 메시지를 출력하는 간단한 사용자 정의 배열 클래스를 만들어 보겠습니다.
ufunc_examples.py에 이 마지막 코드 블록을 추가합니다.
## --- Step 5 를 위한 추가 코드 ---
## np.ndarray 를 서브클래싱하여 사용자 정의 배열 클래스 정의
class MyArray(np.ndarray):
def __add__(self, other):
print("\nStep 5: Custom add method called!")
## 부모 클래스의 원래 구현 호출
return super().__add__(other)
## 사용자 정의 클래스의 인스턴스 생성
## ndarray 를 사용자 정의 클래스로 캐스팅하려면 .view() 를 사용해야 합니다.
my_arr = np.array([10, 20, 30]).view(MyArray)
## 덧셈 수행, 이는 사용자 정의 메서드를 트리거합니다.
override_result = my_arr + 5
print("Step 5 결과 (재정의된 Ufunc):")
print(override_result)
파일을 저장하고 마지막으로 한 번 더 실행합니다.
python ufunc_examples.py
최종 출력을 확인합니다.
... (이전 출력) ...
Step 5: Custom add method called!
Step 5 Result (Overridden Ufunc):
[15 25 35]
덧셈 결과 전에 사용자 정의 메시지가 출력된 것을 볼 수 있으며, 이는 __add__ 메서드가 호출되었음을 확인시켜 줍니다. 이는 ufunc 시스템의 강력한 유연성을 보여줍니다.
요약
이 실습에서는 NumPy 의 유니버설 함수 (ufuncs) 에 대한 필수 사항을 배웠습니다. 벡터화된 연산의 기초를 이루는 기본적인 요소별 산술 연산부터 시작했습니다. 그런 다음 NumPy 가 다른 모양의 배열에 대한 연산을 수행할 수 있도록 하는 핵심 기능인 브로드캐스팅 (broadcasting) 을 탐색했습니다. 또한 데이터 집계를 위한 .reduce()와 같은 ufunc 메서드를 사용하는 방법과 dtype 인자를 사용하여 출력 데이터 타입을 제어하는 방법을 다루었습니다. 마지막으로 np.ndarray를 서브클래싱하여 ufunc 동작을 사용자 정의하는 고급 예제를 살펴보았습니다. 이러한 기술을 통해 이제 NumPy 를 사용하여 효율적이고 읽기 쉬우며 강력한 수치 코드를 작성할 준비가 되었습니다.



