저자 : Dr. Ullrich
한글판 johnsonj 2008.05.20 화
파이썬이 뭐가 그렇게 대단한데? 음, 본인은 프로그래밍 언어를 두 가지 알고 있고 얼마나 많은 것들이 어떻게 작동하는지 아주 잘 안다. 그리고 나에게는 다른 어떤 것보다도 파이썬이 훨씬 더 힘이 좋고/쉽다. 전문 프로그래머는 파이썬을 온갖 종류의 일에 사용한다. 여기에 가면 내가 파이썬을 어디에 사용해 왔는지 자세한 설명을 보실 수 있다 - 이 페이지에서는 파이썬으로 수학 프로그래밍을 하는 방법에 관하여 약간 언급해 볼 생각이다. (아니, 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)로 나아가 보자. 여기에서 파이썬 프로그래밍의 극도로 예민하고 강력한 또다른 측면을 보여주겠다.
지금까지 벡터에서 원소는 숫자였다. 그러나 꼭 그럴 필요는 없다! 벡터 클래스의 __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 + yx와 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 같은 언어로 같은 일을 하는 코드를 작성해 본 경험이 없으리라 생각한다 (이미 그런 경험이 있는데도 아직 감동이 없다면 나에게 알려주시라.)
예를 들어, 더 "좋은" 방법으로 위에 언급한 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)이라고 말할 수 있는 것이다.)
멋지다.
좋다. 계속 읽어 보자. "기록을 남겨 두기 위하여", 다음과 같이
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)를 돌려준다는 뜻이다. 본인이 이렇게 할 수 있다는 사실은 파이썬 객체가 허용하는 경이로운 "내부검사"의 예를 보여준다. (레이몬드가 "메타클래스" 프로그래밍이라고 부르는 것의 자잘한 예라고 간주해도 좋다.)