[Final] PEP 465 - A dedicated infix operator for matrix multiplication
원문 링크: PEP 465 - A dedicated infix operator for matrix multiplication
상태: Final 유형: Standards Track 작성일: 20-Feb-2014
PEP 465: 행렬 곱셈을 위한 전용 중위(Infix) 연산자 @
추가 제안
요약
이 PEP는 Python에 행렬 곱셈을 위한 새로운 이항(binary) 연산자 @
를 도입할 것을 제안합니다. 이 연산자는 파이썬 3.5부터 도입되었습니다.
제안 (Specification)
@
연산자는 *
연산자와 동일한 연산자 우선순위와 좌측 결합성(left-associativity)을 가지며, __matmul__
메서드에 의해 구현됩니다. 또한, 해당 제자리(in-place) 버전인 @=
연산자는 __imatmul__
메서드를 통해 구현됩니다.
이러한 메서드는 내장 타입이나 표준 라이브러리 타입에는 추가되지 않습니다. 하지만 numpy
와 같은 다수의 수치 계산 라이브러리에서 이 연산자에 대한 권장 시맨틱(semantics)에 합의를 이루었습니다.
연산자 우선순위 및 결합성:
연산자 | 우선순위/결합성 | 메서드 |
---|---|---|
@ |
* 와 동일 |
__matmul__ , __rmatmul__ |
@= |
N/A | __imatmul__ |
동기 (Motivation)
개요 (Executive summary)
수치 계산 코드에서는 원소별 곱셈(elementwise multiplication)과 행렬 곱셈(matrix multiplication)이라는 두 가지 중요한 연산이 Python의 *
연산자 사용을 두고 경쟁해왔습니다. Numeric 라이브러리가 처음 제안된 이래 약 20년 동안 이 문제를 해결하기 위한 많은 시도가 있었지만, 만족스러운 해결책은 없었습니다.
현재 대부분의 수치 Python 코드에서는 원소별 곱셈에 *
를 사용하고, 행렬 곱셈에는 함수/메서드 호출 문법을 사용합니다 (numpy.dot(a, b)
또는 a.dot(b)
). 그러나 이로 인해 코드가 가독성이 떨어지고 복잡해지는 문제가 발생합니다. 이 문제는 너무 심각해서 일부 코드베이스에서는 반대 규칙(행렬 곱셈에 *
를 사용하는)을 계속 사용하며 API 파편화를 초래했습니다.
현재 Python 문법 내에서 수치 API를 설계하는 좋은 해결책은 없어 보이며, 단지 다양한 방식으로 나쁜 선택지만 존재합니다. 이러한 문제를 해결하기 위한 최소한의 Python 문법 변경은 행렬 곱셈을 위한 새로운 중위 연산자 하나를 추가하는 것입니다.
행렬 곱셈은 다른 이항 연산과 구별되는 독특한 특징들을 가지고 있으며, 전용 중위 연산자 추가에 대한 강력한 근거를 제공합니다:
- 기존 수치 연산자와 마찬가지로, 수학, 과학, 공학의 모든 분야에서 행렬 곱셈에 중위 표기법을 사용하는 방대한 선례가 있습니다.
@
는 Python의 기존 연산자 시스템의 빈틈을 조화롭게 채워줍니다. @
는 실제 코드의 가독성을 크게 향상시킵니다.@
는 가독성 낮은 코드와 API 파편화로 인해 특히 어려움을 겪는 비전문 프로그래머들을 위한 학습 진입 장벽을 낮춥니다.@
는 Python 사용자 커뮤니티의 상당하고 성장하는 부분에 이점을 제공할 것입니다.@
는 자주 사용될 것이며, 심지어//
나 비트와이즈 연산자보다 더 자주 사용될 수 있다는 증거가 있습니다.@
는 Python 수치 커뮤니티가 파편화를 줄이고, 모든 수치 배열 객체에 대한 단일 합의 오리 타이핑(duck type)을 표준화할 수 있도록 합니다.
현재 상황의 문제점 (Background: What’s wrong with the status quo?)
수치 계산 시, n
차원 배열(n-dimensional array)은 대량의 숫자에 대해 한 번에 간단한 연산을 적용할 수 있게 하는 기본 객체입니다. numpy
는 Python에서 이러한 배열을 제공하는 가장 유명한 라이브러리입니다.
n
차원 배열을 다룰 때, 곱셈을 정의하는 두 가지 방법이 있습니다:
- 원소별 곱셈 (Elementwise multiplication):
[[1, 2], [[11, 12], [[1 * 11, 2 * 12], [3, 4]] x [13, 14]] = [3 * 13, 4 * 14]]
이 방식은
for
루프 없이 대량의 값에 대해 빠르고 쉽게 곱셈을 수행할 수 있어 유용합니다.numpy
와 같은 라이브러리의 배열 객체를 사용할 때, 모든 Python 연산자는 모든 차원의 배열에 대해 원소별로 작동합니다. - 행렬 곱셈 (Matrix multiplication):
[[1, 2], [[11, 12], [[1 * 11 + 2 * 13, 1 * 12 + 2 * 14], [3, 4]] x [13, 14]] = [3 * 11 + 4 * 13, 3 * 12 + 4 * 14]]
행렬 곱셈은 2D 배열(행렬)에 대해서만 정의되며, 다른 연산들과 달리 중요한 “행렬” 버전이 있는 유일한 곱셈 연산입니다. 모든 수치 응용 분야에서 매우 중요하게 사용됩니다.
Python 문법은 현재 단일 곱셈 연산자 *
만 허용하므로, 배열과 유사한 객체를 제공하는 라이브러리는 *
를 원소별 곱셈에 사용할지, 아니면 행렬 곱셈에 사용할지 결정해야 합니다. 불행히도, 일반적인 수치 계산에서는 두 연산 모두 자주 사용되며, 두 경우 모두 함수 호출 문법보다는 중위 연산자 사용에 큰 이점이 있습니다. 이로 인해 어떤 규칙이 최적인지 불분명하며, 프로젝트 간 API 파편화가 발생합니다. 예를 들어 numpy.ndarray
는 *
를 원소별 곱셈에 사용하고, numpy.matrix
는 *
를 행렬 곱셈에 사용하여 코드 통합 시 문제를 야기합니다.
PEP 238이 /
를 /
와 //
두 연산자로 나눴던 것처럼, 이 PEP는 *
를 원소별 곱셈을 위한 *
와 행렬 곱셈을 위한 @
로 나눌 것을 제안합니다.
행렬 곱셈이 중위 연산자여야 하는 이유 (Why should matrix multiplication be infix?)
현재 대부분의 Python 수치 코드는 numpy.dot(a, b)
또는 a.dot(b)
와 같은 문법을 사용하여 행렬 곱셈을 수행합니다. 이 방법은 작동하지만, 전 세계 수학, 과학, 공학 분야에서 보편적으로 중위 표기법이 사용되어 왔기 때문에 가독성 측면에서 중위 연산자가 훨씬 우수합니다.
예를 들어, 통계 가설 검정에서 사용되는 OLS 회귀 모델의 선형 가설 검정 수식 S = (Hβ − r)T(HVHT) − 1(Hβ − r)
을 구현하는 경우를 살펴보겠습니다.
- 현재
dot
함수 사용 시:S = np.dot((np.dot(H, beta) - r).T, np.dot(inv(np.dot(np.dot(H, V), H.T)), np.dot(H, beta) - r))
- 현재
dot
메서드 사용 시:S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)
@
연산자 사용 시:S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)
@
연산자를 사용하면 원본 수식의 기호와 코드가 1:1로 투명하게 매핑되어 훨씬 읽기 쉽습니다.@
는 불필요한 괄호를 줄여 코드를 더 간결하고 명확하게 만들며, 전문가와 비전문가 모두에게 행렬 코드의 사용성을 크게 향상시킵니다.
비전문 프로그래머에게 투명한 문법이 특히 중요한 이유 (Transparent syntax is especially crucial for non-expert programmers)
많은 과학 분야 코드는 해당 분야의 전문가들이 작성하지만, 프로그래밍 전문가는 아닌 경우가 많습니다. 이러한 사용자들은 수학 공식과 코드 간의 투명한 매핑이 매우 중요하며, 이는 코드를 작성하는 데 성공하느냐 실패하느냐의 차이를 만듭니다. numpy.matrix
타입이 *
를 행렬 곱셈으로 정의하는 이유도 이러한 교육적 사용 사례 때문이며, @
연산자는 시작 및 고급 사용자 모두에게 더 나은 문법을 제공하고, 처음부터 동일한 표기법으로 표준화할 수 있도록 돕습니다.
행렬 곱셈이 틈새 시장 요구 사항에 불과한가? (But isn’t matrix multiplication a pretty niche requirement?)
세계는 연속 데이터로 가득 차 있으며, 컴퓨터는 이를 정교하게 처리해야 하는 요구가 점점 늘고 있습니다. 배열(Array)은 금융, 머신러닝, 3D 그래픽, 컴퓨터 비전, 로봇 공학, 운영 연구, 계량 경제학, 기상학, 전산 언어학, 추천 시스템, 신경 과학, 천문학, 생물 정보학 등 다양한 응용 분야의 공통 언어입니다. 이러한 분야의 대부분에서 Python은 전통적인 이산 데이터 구조와 현대적인 수치 데이터 유형 및 알고리즘을 우아하게 혼합할 수 있는 능력 덕분에 빠르게 지배적인 역할을 하고 있습니다.
2013년에는 수치 Python에 특화된 7개의 국제 컨퍼런스가 개최되었고, PyCon 2014에서는 튜토리얼의 약 20%가 행렬 사용과 관련되어 있었습니다. GitHub 코드 검색 결과(2014-04-10 기준
), numpy
는 sys
, os
, re
다음으로 가장 많이 임포트되는 모듈 중 하나이며, subprocess
, math
, pickle
, threading
과 같은 표준 라이브러리 모듈보다도 더 많이 임포트됩니다. 이는 수치 계산이 현대 Python 사용의 주류를 이루고 있음을 시사합니다.
또한, 정수 나눗셈 연산자 //
와 같이 특정 상황에서 매우 유용한 특수 산술 연산을 위해 중위 연산자를 추가한 선례가 있습니다. //
나 비트와이즈 연산자를 사용해본 적이 없는 Python 프로그래머가 많을 수 있지만, @
는 //
보다 더 틈새 시장에 속하지 않습니다.
@
는 행렬 공식에 유용하지만, 얼마나 자주 사용될까? (So @ is good for matrix formulas, but how common are those really?)
표준 라이브러리, scikit-learn, nipy의 코드 베이스에서 연산자 사용 빈도를 분석한 결과, 행렬 곱셈 연산(dot)은 이 두 수치 패키지에서만 약 780번 사용되었으며, 대부분의 비교 연산자(!=
, <
, <=
, >=
)보다 더 자주 사용됩니다. 표준 라이브러리까지 포함한 전체 코드 베이스에서는 행렬 곱셈이 비트와이즈 연산자들보다 더 자주 사용되고, //
보다 2배 더 자주 사용됩니다. 이는 수치 프로그래밍이 일반적이고 주류 활동임을 나타냅니다.
표준 라이브러리에서 사용되지 않는 연산자를 추가하는 것이 이상한가? (But isn’t it weird to add an operator with no stdlib uses?)
특이한 경우이기는 하지만, 중요한 것은 변경 사항이 사용자에게 이점을 제공하는지 여부입니다. @
가 많이 사용될 것이 분명하며, 이 PEP는 Python 수치 커뮤니티가 모든 배열과 유사한 객체에 대한 표준 오리 타이핑에 최종 합의에 도달할 수 있게 하는 중요한 조각을 제공합니다. 이는 표준 라이브러리에 수치 배열 타입을 추가하는 데 필요한 전제 조건입니다.
호환성 고려 사항 (Compatibility considerations)
현재 Python 코드에서 @
토큰의 유일한 합법적인 사용처는 데코레이터에서 문장 시작 부분입니다. 새로운 연산자는 모두 중위 연산자이므로 문장 시작 부분에는 나타날 수 없습니다. 따라서 이 연산자들을 추가해도 기존 코드가 손상되지 않으며, 데코레이터 @
와 새로운 연산자 사이에 구문 분석(parsing) 모호성도 없습니다.
또한, 행렬을 사용하지 않는 사용자가 이 변경 사항으로 인해 Python 언어에 대한 이해를 업데이트하는 데 드는 정신적 비용도 최소화됩니다.
의도된 사용법 세부 사항 (Intended usage details)
이 섹션은 정보 제공을 목적으로 하며, 배열 또는 행렬과 유사한 객체를 제공하는 여러 라이브러리가 @
를 구현하는 방법에 대한 합의를 문서화합니다.
numpy
용어를 사용하여 임의의 다차원 배열을 설명합니다.
- 2차원 입력: 일반적인 행렬로 간주하며, 전통적인 행렬 곱셈을 적용합니다. 예를 들어,
arr(2, 3) @ arr(3, 4)
는(2, 4)
형태의 배열을 반환합니다. - 1차원 벡터 입력: 형태에 ‘1’을 앞 또는 뒤에 붙여 2차원으로 승격된 후 연산이 수행됩니다. 이로 인해
matrix @ vector
와vector @ matrix
모두 허용되며 1차원 벡터를 반환하고,vector @ vector
는 스칼라를 반환합니다. 예를 들어:arr(2, 3) @ arr(3)
는arr(3)
을(3, 1)
형태의 행렬로 처리하여(2,)
형태의 1차원 벡터를 반환합니다.arr(3) @ arr(3)
는 내적(inner product)을 수행하여 스칼라 값을 반환합니다. 이러한 정의는 일부 경우@
를 비결합적(non-associative)으로 만들 수 있지만, 실제 사용 사례의 중요성 때문에 채택되었습니다.
- 2차원보다 큰 입력: 마지막 두 차원을 곱할 행렬의 차원으로 취급하고, 다른 차원에 걸쳐 ‘브로드캐스트(broadcast)’됩니다. 예를 들어,
arr(10, 2, 3) @ arr(10, 3, 4)
는 10개의 개별 행렬 곱셈을 수행하여(10, 2, 4)
형태의 배열을 반환합니다. - 0차원 (스칼라) 입력: 오류를 발생시킵니다. 스칼라와 행렬의 곱셈은
*
연산자로 이미 처리됩니다.
채택 (Adoption)
현재 원소별 곱셈과 행렬 곱셈에 다른 API를 사용하는 기존 Python 프로젝트들은 이 PEP의 승인에 따라 다음과 같이 전환할 계획을 가지고 있습니다.
*
를 원소별 곱셈에, 함수/메서드 호출을 행렬 곱셈에 사용하는 프로젝트:numpy
,pandas
,blaze
,theano
등의 개발자들은 위에서 설명한 시맨틱을 사용하여@
를 구현할 의사를 표명했습니다.
*
를 행렬 곱셈에, 함수/메서드 호출을 원소별 곱셈에 사용하는 프로젝트:numpy
(numpy.matrix
),scipy.sparse
,pyoperators
,pyviennacl
등의 프로젝트는 이 PEP가 수락되면 현재 API에서 원소별*
, 행렬 곱셈@
규칙으로 전환할 의사를 표명했습니다. 이는 이 PEP가 수락될 경우 API 파편화가 해소될 프로젝트 목록입니다.
*
를 행렬 곱셈에 사용하고, 원소별 행렬 곱셈에 크게 신경 쓰지 않는 프로젝트:sympy
,sage
와 같은 프로젝트는 추상 수학적 객체로서의 행렬에 초점을 맞추며, 원소별 연산의 필요성이 적습니다. 이들은@
가 수락되더라도*
를 행렬 곱셈에 계속 사용할 것이며,@
를 별칭으로 추가할 수 있습니다.
구현 세부 사항 (Implementation details)
operator.matmul
및operator.__matmul__
함수가 표준 라이브러리에 추가됩니다.PyObject* PyObject_MatrixMultiply(PyObject *o1, PyObject *o2)
함수가 C API에 추가됩니다.MatMult
라는 새로운 AST(Abstract Syntax Tree) 노드, 새로운 토큰ATEQUAL
, 그리고 새로운 바이트코드 opcodesBINARY_MATRIX_MULTIPLY
및INPLACE_MATRIX_MULTIPLY
가 추가됩니다.- 두 개의 새로운 타입 슬롯이 추가됩니다.
명세 세부 사항에 대한 근거 (Rationale for specification details)
연산자 선택 (Choice of operator)
@
를 선택한 이유는 다음과 같습니다:
- 미국 영어 키보드에 있는 기호 중 Python 표현식 컨텍스트에서 아직 의미가 없는
@
, 백틱,$
,!
,?
중에서@
가 가장 적합합니다.!
와?
는 프로그래밍 맥락에서 이미 다른 의미를 가지고 있고, 백틱은 BDFL에 의해 Python에서 금지되었으며,$
는*
나·
와 더 이질적이며 Perl/PHP의 영향이 있습니다. @
는 “스칼라/원소별 곱셈과 구별되는 행렬 곱셈”을 의미하는 데 필요한 연산자입니다. 프로그래밍이나 수학에서 이러한 의미를 가진 관습적인 기호는 없습니다.@
는 데코레이터에서 이미 익숙하게 사용되는 친숙한 문자이지만, 데코레이터 사용과 수학 표현식 사용은 실제에서 혼동하기 어려울 정도로 충분히 다릅니다.- 키보드 레이아웃에 관계없이 널리 접근 가능합니다 (이메일 주소에서의 사용 덕분에).
*
와·
처럼 둥근 형태를 가집니다.mATrices
라는 연상 기호가 귀엽습니다.- 회전하는 모양은 행렬 곱셈을 정의하는 행과 열에 대한 동시 스캔을 연상시킵니다.
- 비대칭성은 비결합적인 특성을 암시합니다.
우선순위 및 결합성 (Precedence and associativity)
@
연산자는 대부분의 Python 연산자와 마찬가지로 좌측 결합성(left-associative)을 가집니다.
비록 행렬 곱셈이 함수 적용/합성과 유사하여 일부 수학적으로 정교한 사용자들은 우측 결합성을 직관적으로 생각할 수 있고, Mat @ (Mat @ vec)
과 같이 평가될 때 효율성 이점이 있을 수 있다는 주장이 있었지만, 다음 이유들로 인해 좌측 결합성이 채택되었습니다:
- 실제 코드에서
Mat @ Mat @ vec
유형의 표현식이 지배적이라는 증거를 찾을 수 없었습니다. - R, Matlab, Julia, IDL, Gauss 등 선형 대수에 중점을 둔 다른 언어들도 행렬 곱셈 연산자를 압도적으로 좌측 결합성으로 만듭니다.
- 좌측 결합성은
@
가*
와 유사하게 작동한다고 배우고 기억하기 훨씬 쉽습니다.
내장 타입에 대한 (비)정의 ((Non)-Definitions for built-in types)
float
, int
등과 같은 내장 수치 타입이나 numbers.Number
계층에는 __matmul__
또는 __matpow__
가 정의되지 않습니다. 이러한 타입은 스칼라를 나타내며, @
의 합의된 시맨틱은 스칼라에 대해 오류를 발생시켜야 하기 때문입니다.
memoryview
또는 array.array
객체에는 현재 __matmul__
메서드가 정의되지 않습니다. 이는 해당 타입이 수치 작업에 사용되기 전에 상당한 추가 작업이 필요하며, 효율적인 행렬 곱셈 구현의 복잡성 (BLAS 라이브러리 링크 및 multiprocessing
문제) 때문입니다.
str
, list
등 __mul__
을 정의하는 비수치 Python 내장 타입에 대해서도 __matmul__
은 정의되지 않습니다.
행렬 거듭제곱의 미정의 (Non-definition of matrix power)
이 PEP의 초기 버전에서는 **
와 유사한 행렬 거듭제곱 연산자 @@
도 제안했지만, 그 유용성이 불분명하여 현재는 제외되었습니다. @
연산자에 대한 경험이 더 쌓인 후에 @@
의 필요성이 입증되면 다시 논의될 예정입니다.
새로운 연산자 추가에 대한 거부된 대안 (Rejected alternatives to adding a new operator)
지난 수십 년 동안 Python 수치 커뮤니티는 행렬 곱셈과 원소별 곱셈 연산 사이의 긴장을 해결하기 위한 다양한 방법을 모색했습니다. PEP 211과 PEP 225는 이 문제를 해결하기 위한 초기 시도였지만 심각한 결함이 있었습니다. 이후의 경험을 통해 수치 Python과 코어 Python 모두를 위한 최상의 해결책은 행렬 곱셈을 위한 단일 중위 연산자를 추가하는 것이라는 합의에 도달했습니다.
거부된 주요 대안들은 다음과 같습니다:
__mul__
을 행렬 곱셈으로 정의하는 두 번째 타입 사용:numpy.matrix
타입으로 수년 동안 시도되었지만, 배열에 대한 충돌하는 오리 타이핑으로 인해numpy
개발자와 하위 패키지 개발자들 사이에서numpy.matrix
를 거의 사용하지 말아야 한다는 강력한 합의를 이끌어냈습니다.- 많은 새로운 연산자 추가 또는 중위 연산자를 정의하기 위한 새로운 일반 문법 추가: 일반적으로 Pythonic하지 않으며, BDFL에 의해 반복적으로 거부되었습니다. 과학 Python 커뮤니티는 행렬 곱셈을 위한 하나의 연산자 추가만으로 해결할 수 없는 고통스러운 문제를 해결하기에 충분하다는 합의에 도달했습니다.
- 일반 Python에서 다른 의미를 가지는 새로운
@
연산자를 추가하고 수치 코드에서 오버로드: PEP 211이 취했던 접근 방식이었으나,itertools.product
가 전용 연산자를 필요로 할 만큼 중요하지 않다는 문제가 있었습니다. 행렬 곱셈은 중위 연산자로 포함될 만한 독특하고 강력한 근거를 가집니다. - 배열 타입에
.dot
메서드를 추가하여 “유사 중위(pseudo-infix)”A.dot(B)
문법 허용:numpy
에 수년 동안 존재했지만, 여전히 실제 중위 표기법보다 가독성이 떨어지며 과도한 괄호 문제가 있습니다. with
블록을 사용하여 단일 코드 블록 내에서*
의 의미 전환: 전역 상태 확인 문제와 동적 스코프(dynamically scoped) 문제로 인해 안전하게 사용하기 어렵습니다.- 수치 지향 연산자 및 기타 구문을 추가하는 언어 전처리기 사용: 하나의 이항 연산자를 지원하기 위해 새로운 언어를 정의하는 것은 비실용적입니다. Python의 강점은 전문화된 수치 코드를 다른 일반적인 코드와 혼합할 수 있다는 점인데, “수치 Python”을 “실제 Python”과 분리하는 것은 혼란을 야기하고 파편화를 증가시킵니다.
*dot*
과 같은 “새로운 중위 연산자”를 정의하기 위한 오버로딩 해킹 사용: 아름답지 않고 Pythonic하지 않으며, 초보자에게 특히 불친절합니다.arr.M * arr
과 같은 구문을 지원하기 위해 특수 “파사드(facade)” 타입 사용: “마법 같은” 특성으로 인해 위의 해킹과 유사한 반대를 받습니다. 또한, 파사드 객체가 다른 배열 객체와 파사드 객체 모두를 인식해야 하는 등 비직관적인 복잡성을 야기합니다.
참고 자료 (References)
https://peps.python.org/pep-0465/
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments