저자 : Dr. Ullrich
한글판 johnsonj 2008.05.20 화

파이썬으로 처리하는 수학

언제인가 롸이트(Dr. Wright) 교수님이 수업 시간에 어떤 프로그래밍 언어가 얼마나 멋진가에 관하여 말씀하신 것을 들어 본 적이 있다. 나의 친구 켄트(Kent)가 "파이썬은 어때요?", 또는 그런 취지로 옹호한 것 같다. 나는 파이썬이 얼마나 놀랍도록 멋진가에 관하여 무언가 조금 언급해 두는 것이 좋다고 생각했다. 여러분에게도 도움이 되고 말이다 - 프로그래밍에 관심이 있다면 꼭 알아야 할 것이 있다. 프로그래밍에 관심이 없더라도 파이썬에 관하여 조금이라도 들어 두는 편이 더 좋을 것이라고 생각한다. 왜냐하면 앞으로 어떤 목적 때문에 프로그래밍 언어를 배우지 않을 수 없게 된다면 파이썬이 아마도 가장 훌륭한 선택이 될 것이기 때문이다. 딱 한가지 이유, 너무 쉽다는 이유 하나만으로도 가장 훌륭한 선택이다.

파이썬이 뭐가 그렇게 대단한데? 음, 본인은 프로그래밍 언어를 두 가지 알고 있고 얼마나 많은 것들이 어떻게 작동하는지 아주 잘 안다. 그리고 나에게는 다른 어떤 것보다도 파이썬이 훨씬 더 힘이 좋고/쉽다. 전문 프로그래머는 파이썬을 온갖 종류의 일에 사용한다. 여기에 가면 내가 파이썬을 어디에 사용해 왔는지 자세한 설명을 보실 수 있다 - 이 페이지에서는 파이썬으로 수학 프로그래밍을 하는 방법에 관하여 약간 언급해 볼 생각이다. (아니, Maple과 Mathematica 같은 것들을 대체하려는 것이 아니다. 이 페이지는 파이썬 프로그래밍을 위한 자습서가 아니다. 요점은 그냥 파이썬에 관심을 불러 일으키려는 것이다. 파이썬은 http://www.python.org/에서 내려 받을 수 있다; 아주 멋진 자습서도 함께 따라온다. 파이썬 프로그래밍 문제에 관하여 본인(Dr. Ullrich)에게 문의하셔도 되고, 또는 곳곳에 널린 인터넷이나 책에서 도움을 얻으시면 된다 (이런 자원들로 가는 링크는 http://www.python.org/에서 많이 볼 수 있다).)

어떤 언어가 좋은 프로그래밍 언어인지 내 말을 그대로 믿을 이유는 없다. 그러나 에릭 레이몬드(Eric Raymond)는 열린 소스 프로그래밍 세계에서 명성이 높다 - 여기에 파이썬이 왜 그렇게 위대한지에 관하여 그가 쓴 글을 요약해 놓았다 (원래 글로 가는 링크도 있음). 본인이 의견을 피력할만한 자격이 있는지는 잘 모르겠다. 어쨌든 여기에 개인적으로 파이썬에 관하여 멋지다고 생각한 것들에 관하여 논평을 해 놓는다:

벡터와 행렬

파이썬 코드가 작성하기 쉬운 이유중 하나는 "유형이 정의되지 않기 때문이다". (음, 기술적으로 변수는 유형이 없지만, 은 유형이 있다...) 이는 다음과 같이 말해도 된다는 뜻이다.
x = 2
print x
x = [1, 2, 3]
print x
변수 x를 정수나 "리스트"로 "선언할 필요가 없다"; 위의 첫 "print x"는 "2"를 인쇄하고, 두 번째는 "[1, 2, 3]"을 인쇄한다. 아무 문제가 없다. 벡터에 파이썬의 리스트 객체를 사용하겠다.

자, 파이썬의 리스트 객체는 벡터가 아니며, 리스트일 뿐이다. 그래서 예를 들어 다음과 같이 말하면

x = [1, 2, 3]
print x + x
인쇄되는 것은 리스트 [1, 2, 3, 1, 2, 3]이다. 이는 두 개의 리스트를 "더하는" 합리적인 방법이며, 리스트를 사용할 때는 유용하지만, 두 개의 벡터를 더하기 위해 사용하고 싶은 방법은 아니다. 파이썬의 리스트 객체를 벡터 클래스 안에 "싸 넣을" 생각이다. 그리고 나서 벡터(Vectors) 클래스에게 두 리스트를 더하는 올바른 방법을 가르쳐보자.

키워드 "class"를 사용하여 파이썬 클래스를 만든다 (흠). 제일 먼저 할 일은 Vector 클래스에 __init__ "메쏘드"를 주는 것이다. 이렇게 하면 Vector 객체를 구성할 때 데이터를 그 안에 삽입할 수 있다:

class Vector:
  
  def __init__(self, data)
    self.data = data
유용하지만 완전히 쓸모 없는 Vector 클래스이다. Vector 객체는 데이터 리스트를 건네면서 "Vector"를 함수로 호출해서 만든다:
class Vector:
  
  def __init__(self, data):
    self.data = data
    
x = Vector([1, 2, 3])    
("Vector(1, 2, 3)"이 "Vector([1,2,3])"과 똑같은 일을 하도록 사용할 만한 트릭이 있지만, 지금 당장은 신경쓰지 말자...)

위의 코드를 실행하면 x는 벡터(Vector) 객체가 된다. 이 객체는 "data" 필드가 있는데 리스트 [1, 2, 3]가 동등하다; 다음과 같이 말하면

print x.data
이 시점에서 리스트 [1, 2, 3]이 출력되는 것을 볼 수 있다. 그러나 "print x"라고 하면 원하는 것을 돌려주지 않는다. 대신에 다음과 같이 "<__main__.Vector instance at f1ae10>"을 돌려주는데, 별로 도움이 되지 않는다. 자, 지금까지 Vector 객체를 인쇄하면 어떤 일이 일어나기를 원하는지 말하지 않았다. 그래서 파이썬은 그냥 그게 벡터(Vector)라고 알려주는 것이다. __repr__ 메쏘드를 추가하여 이를 고치면 된다:
class Vector:
  
  def __init__(self, data):
    self.data = data
    
  def __repr__(self):
    return repr(self.data)  
    
x = Vector([1, 2, 3])    
print x
__repr__ 메쏘드는 파이썬에게 Vector 객체를 인쇄할 때 벡터에 들어 있는 데이터를 인쇄하기를 원한다고 알려준다. 그리고 이제 "print x"는 "[1, 2, 3]"을 인쇄하고, 이게 더 마음에 든다. (지금 당장은 무시해도 좋은 기술적 주의사항...)

물론 벡터를 더하고 싶다; 지금 당장 다음과 같이 말하면

class Vector:
  
  def __init__(self, data):
    self.data = data
    
  def __repr__(self):
    return repr(self.data)  
    
x = Vector([1, 2, 3])    
print x + x
[2, 4, 6] 또는 [1, 2, 3, 1, 2, 3]을 얻지 못한다. 대신에 에러 메시지를 받는데, 왜냐하면 벡터를 어떻게 더해야 하는지 파이썬에게 말해 주지 않았기 때문이다.

지금부터 멋진 부분이 시작된다. 벡터를 어떻게 더해야 하는지 클래스에 __add__ 메쏘드를 추가하여 알려준다:

class Vector:
  
  def __init__(self, data):
    self.data = data
    
  def __repr__(self):
    return repr(self.data)  
    
  def __add__(self, other):
  #이코드를 작성하는 방법으로 훨씬 더 좋은 방법이 있음에 주의하자
  #여기에서는 자기-설명적인 코드를 작성하려고 노력한다
  #"좋은" 코드 대신에 말이다
  
    data = [] #빈 리스트로 시작한다
    
    for j in range(len(self.data)):
    #다음 "for j = 0 to len(self.data) - 1"과 같은 일을 한다:
    
      data.append(self.data[j] + other.data[j])
      
    #지금까지 데이터는 결과 Vector에 담길 리스트이다. 
    #- 이제 그것을 벡터(Vector) 안에 싸 넣는다:
    
    return Vector(data)  
    
x = Vector([1, 2, 3])    
print x + x
이제 "print x+x"는 [2, 4, 6]를 인쇄한다. 만세, 벡터를 더할 수 있다.

음, 주석 때문에 코드가 실제보다 더 복잡해 보인다 - 물론 주석은 좋은 것이다. 그러나 이런 주석은 그냥 언어가 어떻게 작동하는지 모르는 독자를 위한 것이다. 실제 코드에서는 넣지 말아야 할 종류의 주석이다. 주석을 생략하면 그냥 다음과 같다

class Vector:
  
  def __init__(self, data):
    self.data = data
    
  def __repr__(self):
    return repr(self.data)  
    
  def __add__(self, other):
    data = []
    for j in range(len(self.data)):
      data.append(self.data[j] + other.data[j])
    return Vector(data)  
    
x = Vector([1, 2, 3])    
print x + x
벡터에게 더하는 법을 가르치려면 이 정도로 충분하다.

물론 벡터에게 가르치고 싶은 일이 많다. 예를 들어, 스클라 뺄셈과 곱셈 등등을 할 수 있으면 좋을 것이다. 그런 일들은 다양한 메쏘드를 벡터 클래스에 추가하면 가능하다; 다음은 몇 가지를 보여주고자 한다. 실제 작동하는 코드를 개발하려는 것이 아니므로 지금 당장은 Vector를 그대로 두고 행렬(Matrix)로 나아가 보자. 여기에서 파이썬 프로그래밍의 극도로 예민하고 강력한 또다른 측면을 보여주겠다.

행렬(Matrices)

앞 섹션에서 이른바 파이썬의 "연산자 오버로딩(operator overloading)"을 보여주었다 - " 벡터(Vector)에 __add__ 메쏘드를 정의하여 + 연산자를 "오버로드하였다. 이 섹션에서는 파이썬의 또다른 측면을 보여주고자 한다. 수학적 프로그래밍에 아주 유용한데, 즉 "다형성(polymorphism)"이라는 특징이다. (이 섹션에서는 벡터(Vector) 클래스가 앞 섹션의 마지막과 같다고 간주한다.)

지금까지 벡터에서 원소는 숫자였다. 그러나 꼭 그럴 필요는 없다! 벡터 클래스의 __add__ 메쏘드를 다시 한 번 살펴보자:

  def __add__(self, other):
    data = []
    for j in range(len(self.data)):
      data.append(self.data[j] + other.data[j])
    return Vector(data)
실제로 더하는 부분은 "self.data[j] + other.data[j]"인데, 이 부분은 한 벡터(self)의 원소 하나를 또다른 벡터(other)의 원소에 더한다. self.data[j]가 숫자일 거라고 생각하겟지만, 반드시 그럴 필요는 없으며, 이것은 self.data[j]와 other.data[j]가 어떤 종류의 값이기만하면 되며, 파이썬이 그 합이 무엇인지 알기만 하면 된다.

먼저 엉성하고 쓸모없는 예를 보여주고 다음에 유용한 예를 부여주겠다. 파이썬은 문자열을 연결해서 더한다: 'Hello ' + 'World'는 'Hello World'로 평가된다. 특히, 파이썬은 두 문자열의 합을 인지한다. 즉 내용이 문자열인 벡터를 더할 수도 있다는 뜻이다. 실제로 그렇게 할 수 있다: 다음과 같이 말하면

x = Vector(['Hello ', 'silly '])
y = Vector(['World', 'example'])
print x + y
x와 y의 원소가 구성요소별로 더해진다. 그리고 x + y는 ['Hello World', 'silly example']를 인쇄한다. (벡터의 구성요소는 값이 같을 필요가 없다. 예를 들어
x = Vector(['Hello ', 1])
y = Vector(['World', 2])
print x + y
['Hello World', 3]를 인쇄한다.)

왜 그렇게 하고 싶어하는지, 즉, 구성요소가 문자열인 벡터를 더하고 싶어하는지 실제로는 상상이 안 된다. 그러나, 벡터의 구성요소는 무엇이든 될 수 있다. 파이썬이 더하는 법을 알기만 하면 된다. 그래서 벡터의 구성요소는 그 자체로 벡터가 될 수 있으며, 이 덕분에 행렬을 표현할 수 있다! 벡터를 구성요소로 하는 두 개의 벡터를 더하면, 그 구성요소들은 올바르게 더해져서, 두 행렬의 합을 돌려줄 것이다 (행렬, 즉 2x2 배열을 숫자를 담은 연속열의 연속열로 생각하자.) 다음이 작동하는지 보자:

x = Vector([Vector([1, 2]), Vector([3, 4])])
print x
print x + x
물론 작동한다. "print x"는 [[1, 2], [3, 4]]를 인쇄하고, 다음 "print x + x"는 [[2, 4], [6, 8]]를 인쇄한다. 바로 원하는 바이다.

이것이 바로 "다형성(polymorphism)"이다: 파이썬 코드는 실제로 무엇인지 신경쓰지 않는다. 원하는 연산을 수행할 수만 있으면 된다.

여기가 참을 수 없을 만큼 멋진 곳이기도 하다: 벡터를 더하기 위해 회돌이를 작성하였고, 이제 이중 회돌이를 작성하여 행렬을 더할 필요가 있다고 생각할 것이다. 그러나, 아니다. 벡터 덧셈은 자동으로 작동하여 행렬을 더한다. 구성요소 역시 벡터인 벡터로 그 행렬들을 표현하기만 하면 된다.

물론 행렬을 정의하기 위해 "Vector([Vector([1, 2]), Vector([3, 4])])"로 표기하는 것은 고통이다. 대신에 그냥 "Vector([[1, 2], [3, 4]])"로 시도해 볼 수 있지만, 작동하지 않는데 그 이유는 그의 구성요소로 벡터 대신에 순수하게 파이썬 리스트 객체인 벡터를 얻기 때문이다 (퀴즈: 다음과 같이 말하면

x = Vector([[1, 2], [3, 4]])
print x + x
그 결과는 어떻게 되는가? 이 문서의 앞쪽에 해답이 있다...)

"Vector([Vector([1, 2]), Vector([3, 4])])" 문제를 해결하다 보면 마지막 주제에 다다른다: (하부클래스화("subclassing")라고도 알려진) 상속(inheritance)이라는 주제를 다루어 보겠다. 다음과 같이 선언하겠다

class Matrix(Vector)
즉 행렬이 벡터 "하부클래스(subclass)"라는 뜻이다: 행렬 객체는 벡터 객체와 정확하게 똑 같이 작동할 것이다. 단 새로운 메쏘드를 작성하여 명시적으로 바꾼 행위만 빼고 말이다. 여기에서 벡터는 초기화를 제외하고 행렬에게 원하는 행위와 똑 같이 행동한다 - Matrix([[1, 2], [3, 4]])라고 선언해서 앞의 예제의 행렬을 구성했으면 좋겠다. 그래서 __init__ 메쏘드를 오버라이드(재작성) 한다.

새 __init__ 메쏘드는 데이터가 리스트의 리스트라고 간주하며, Vector()를 호출하여 벡터의 리스트로 변환한다 (위의 __add__ 메쏘드처럼, 새 __init__ 메쏘드를 작성하는 훨씬 "더 좋은" 방법이 있다:)

class Matrix(Vector):
  def __init__(self, data):
    newdata = []
    for v in data:
      newdata.append(Vector(v))
    self.data = newdata
    
x = Matrix([[1, 2], [3, 4]])
print x + x
충분히 잘 작동하며, [[1, 2], [3, 4]]를 인쇄한다.

별로 감동을 받지 못했다면, 아마도 C나 Pascal 또는 Fortran 같은 언어로 같은 일을 하는 코드를 작성해 본 경험이 없으리라 생각한다 (이미 그런 경험이 있는데도 아직 감동이 없다면 나에게 알려주시라.)


이것으로 이 글을 쓸 때 염두에 둔 독자들에게 말하고자 했던 바를 마치겠다. "기능형 프로그래밍"이 무엇인지 아는 독자들에게 몇 마디 덧 붙이자면, 파이썬은 리스프(Lisp)가 아니지만, lambda, map, filter, 등등을 포함하고 있다는 것이다:

예를 들어, 더 "좋은" 방법으로 위에 언급한 Matrix.__init__() 메쏘드를 작성하면 다음과 같다:

class Matrix(Vector):
  def __init__(self, data):
    self.data = map(Vector, data)
    
x = Matrix([[1, 2], [3, 4]])
print x + x
"고급" 독자들에게 한 마디 더 하면: map()은 두개 (이상의) 인자를 취하는데, 함수와 그 함수를 적용할 리스트를 인자로 취한다. Vector는 함수는 아니지만, Vector (Vector 클래스 자체를 뜻하지, 클래스의 실체를 의미하는 것이 아님)는 "호출 가능 객체"이다. 즉 함수가 예상되는 곳이면 어디든지 사용할 수 있다는 뜻이다. 파이썬을 강력하게 만드는 것들중 하나를 더 보여주면: 무엇인지 신경쓰지 않는다. 단 요구한 것을 처리하는 법을 알기만 하면 된다! (여기에서 map()은 함수가 실제로 함수인지 아닌지 신경쓰지 않는다. 호출가능하기만 하면 된다. 물론이다. __call__ 메쏘드를 정의해 주면 객체를 호출가능하게 만들 수 있다.)

음, 이것은 map()에 건네는 두 번째 인자가 리스트 대신 Vector()가 되어도 된다는 뜻인가? 현재의 벡터 클래스로는 안되지만, __getitem__ 메쏘드와 __len__ 메쏘드를 추가하여 벡터의 실체를 "리스트-비슷한" 객체로 만들면 된다:

class Vector:
  
  def __init__(self, data):
    self.data = data
    
  def __repr__(self):
    return repr(self.data)  
    
  def __add__(self, other):
    return Vector(map(lambda x, y: x+y, self, other))

  def __getitem__(self, index):
    return self.data[index]

  def __len__(self):
    return len(self.data)

x = Vector([1,2,3])
y = map(lambda x: x**2, x)
print y
[1, 4, 9]가 인쇄된다: __getitem__이 있다는 것은 벡터가 인덱스 될 수 있다는 뜻이다; 정의한 방식에 의하면 x가 벡터라면 x[0]은 x.data[0]과 같다. __len__ 메쏘드는 벡터의 "길이"를 지정한다. 두 가지 메쏘드가 다 있는 클래스는 마치 클래스처럼 행위하기 때문에 map에 건넬 수 있다. (주목하자. 벡터가 "리스트-비슷하게" 되었기 때문에 개정된 __add__ 메쏘드에 map(lambda x, y: x+y, self.data, other.data)이 아니라 map(lambda x, y: x+y, self, other)이라고 말할 수 있는 것이다.)

멋지다.


무시해도 좋은 페이지

솔직히, 이 글을 쓰면서 본인이 염두에 둔 수준의 독자라면 지금 당장 메인 페이지로 돌아가도 좋다 - 이 페이지는 파이썬 프로그래머를 위하여 그냥 "기록을 남겨 두는 곳"이다. 파이썬 프로그래머라면 본인이 Vector.__repr__을 작성할 때 잘못했다는 것을 눈치채셨으리라 생각하기 때문이다.

좋다. 계속 읽어 보자. "기록을 남겨 두기 위하여", 다음과 같이

x = Vector([1, 2, 3])
그러면 x.__repr__은 그 자체가 "[1, 2, 3]"을 돌려주도록 되어 있지 않다. repr(x)는 x를 재구성할 수 있는 문자열이 되어야 한다; 이 예제에서 x.__repr__()은 "[1, 2, 3]"이 아니라 문자열 "Vector([1, 2, 3])"를 돌려주어야 한다는 뜻이다. x.__str__()이 "[1, 2, 3]"을 돌려주어도 문제는 없지만, 그냥 __repr__ 대신에 __str__ 메쏘드를 호출하더라도 이 페이지의 모든 벡터 예제들이 똑같은 방식으로 작동할 것이다.

이런 식으로 처리한 이유는 행렬을 위하여 코드를 되도록이면 간단하게 만들려고 했기 때문이다; x.__repr__()에게 올바른 것을 돌려주도록 만들면 나중에 이렇게 말할 때

x = Matrix([[1, 2], [3, 4]])
print x + x
내가 원한 "[[2, 4], [6,8]]" 대신에 "[Vector([2,4]), Vector([6, 8])]"를 돌려 받게 된다. (물론 행렬(Matrix)에 적절한 __str__() 메쏘드를 주면 그를 고칠 수 있다...여기에서는 그에 관해 신경쓰고 싶지 않다.)

예제에서 올바르지 않은 면을 말한다면, Vector.__add__는 실제로 Vector(data) 대신에 self.__class__(data)를 돌려주어야 한다. 실제 벡터를 더할 때 그 두 개는 같지만, 나중에 Matrix를 Vector의 하부클래스로 만들 때 두 행렬 객체의 합이 벡터가 아니라 행렬이 되기를 원했다. (그래서 Matrix에 적절한 __mul__ 메쏘드를 주면 다음과 같이 말할 때 요청될 것이다

x = Matrix(whatever)
print (x + x)*(x + x);
지금 당장은 두 Matrix 객체의 합은 Vector이며, 작동하지 않는다.)

파이썬 프로그래머가 아님에도 계속 읽고 있다면: Vector(data) 대신에 self.__class__(data)를 돌려주도록 Vector.__add__를 재작성한다면 그것은 self가 어쩌다가 벡터가 된다면 Vector(data)를 돌려주고 self가 행렬이라면 Matrix(data)를 돌려준다는 뜻이다. 본인이 이렇게 할 수 있다는 사실은 파이썬 객체가 허용하는 경이로운 "내부검사"의 예를 보여준다. (레이몬드가 "메타클래스" 프로그래밍이라고 부르는 것의 자잘한 예라고 간주해도 좋다.)


한글판 johnsonj 05.20 화