클래스와 객체

이 장에 나오는 코드 예제들은 http://thinkpython.com/code/Point1.py에 있습니다; 연습의 답은 http://thinkpython.com/code/Point1_soln.py에 있습니다.

사용자 정의 형

[point]

지금까지 파이썬에 내장된 형들을 여러 가지 사용해봤습니다; 이제 우리는 새로운 형을 정의하려고 합니다. 예로, 이차원 평면 위의 점을 표현하는 Point 라는 형을 만들 것입니다.

수학적인 표기법으로, 점은 종종 콤마로 분리된 좌표가 있는 괄호로 표시됩니다. 예를 들어, \((0,0)\) 은 원점을 나타내고, \((x,y)\) 는 원점으로부터 오른쪽으로 \(x\) 단위, 위로 \(y\) 단위 떨어진 점을 표현합니다.

파이썬에서 점을 표현하는데 사용할 수 있는 방법은 여러 가지가 있습니다:

  • 좌표를 두 변수 x 와 y 에 따로 저장할 수 있습니다.
  • 좌표를 리스트나 튜플의 요소에 저장할 수 있습니다.
  • 점을 객체로 표현하는 새로운 형을 만들 수 있습니다.

새 형을 만드는 것이 다른 방법들보다 (약간) 더 복잡합니다만, 곧 드러나게 될 장점들이 있습니다.

사용자 정의 형을 클래스(class)라고도 합니다. 클래스 정의는 이런 식입니다:

class Point(object):
    """Represents a point in 2-D space."""

헤더는 새 클래스가 Point 이고 내장형인 object 의 일종임을 나타냅니다.

바디는 클래스의 용도를 설명하는 주석문자열입니다. 클래스 정의 안에서 변수와 함수를 정의할 수 있지만, 뒤에서 보기로 하겠습니다.

Point 클래스 정의는 클래스 객체를 만듭니다.

>>> print Point
<class '__main__.Point'>

Point 가 최 상단에서 정의되었기 때문에, “전체 이름(full name)”은 __main__.Point 이 됩니다.

클래스 객체는 객체를 만드는 공장과 같은 것입니다. 점(Point)을 만들려면 마치 함수인 것처럼 Point를 호출합니다.

>>> blank = Point()
>>> print blank
<__main__.Point instance at 0xb7e9d3ac>

반환 값은 Point 객체를 가리키는 참조인데 blank에 대입했습니다. 새 객체를 만드는 것을 인스턴스화(instantiation)라고 하고, 객체는 클래스의 인스턴스(instance)입니다.

인스턴스를 인쇄하면, 파이썬은 속한 클래스와 메모리에 저장된 장소(앞에 붙은 0x는 뒤따라오는 숫자들이 16진수임을 뜻합니다)를 알려줍니다.

애트리뷰트

[attributes]

점 표기법(dot notation)으로 인스턴스에 값을 대입할 수 있습니다:

>>> blank.x = 3.0
>>> blank.y = 4.0

이 문법은 math.pi 나 string.whitespace처럼 모듈에서 변수를 선택할 때의 문법과 비슷합니다. 이 경우는 객체의 이름 붙은 요소에 값을 대입하고 있기는 하지만 말입니다. 이 요소들을 애트리뷰트(attribute)라고 합니다.

다음 다이어그램은 이 대입의 결과를 보여줍니다. 객체와 애트리뷰트를 보여주는 상태 도를 객체 다이어그램(object diagram)이라고 부릅니다; 그림 [fig.point]을 보세요.

point

[fig.point]

변수 blank는 Point 객체를 가리키는데, 두 개의 애트리뷰트를 갖고 있습니다. 각 애트리뷰트는 실수를 가리킵니다.

같은 문법으로 애트리뷰트의 값을 읽을 수 있습니다:

>>> print blank.y
4.0
>>> x = blank.x
>>> print x
3.0

표현식 blank.x는 “blank가 가리키는 객체로 가서 x의 값을 취하라” 라는 뜻입니다. 이 경우에, 그 값을 변수 x에 대입합니다. 변수 x 와 애트리뷰트 x 사이의 충돌은 없습니다.

점 표기법은 어떤 표현식의 일부로도 사용될 수 있습니다. 예를 들어:

>>> print '(%g, %g)' % (blank.x, blank.y)
(3.0, 4.0)
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> print distance
5.0

일상적인 방식으로 인스턴스를 인자로 전달할 수 있습니다. 예를 들어:

def print_point(p):
    print '(%g, %g)' % (p.x, p.y)

print_point 는 인자로 점을 받아서 수학적인 표기법으로 인쇄합니다. 호출하려면 blank를 인자로 전달합니다:

>>> print_point(blank)
(3.0, 4.0)

함수 내부에서, p가 blank의 에일리어스(alias)이기 때문에, 만약 함수가 p를 수정한다면 blank도 바뀝니다.

[연습 15.1.]

두 개의 점을 인자로 받아서 그들간의 거리를 돌려주는 함수 distance_between_points를 작성하세요.

직사각형

[rectangles] 때로 객체의 애트리뷰트가 무엇이 되어야 할지 뻔합니다만 어떤 경우에는 결정이 필요할 수 있습니다. 예를 들어, 직사각형을 표현하기 위한 클래스를 설계하고 있다고 가정해보세요. 어떤 애트리뷰트들을 사용해서 직사각형의 위치와 크기를 지정해야 할까요? 각도는 무시해도 좋습니다; 간단하게 하기 위해, 직사각형은 가로나 세로 중 한 방향이라고 가정하세요.

적어도 두 개의 가능성이 있습니다:

  • 직사각형의 한 꼭짓점(또는 중심), 폭, 높이를 지정할 수 있습니다.
  • 서로 마주보는 두 꼭짓점을 지정할 수 있습니다.

이 시점에 둘 중 어느 것이 낫다고 말하기는 어렵기 때문에, 단지 예로서 첫 번째 것을 구현하겠습니다.

여기 클래스 정의가 있습니다:

class Rectangle(object):
    """Represents a rectangle.

    attributes: width, height, corner.
    """

주석문자열은 애트리뷰트들을 나열합니다: width 와 height 는 숫자입니다; corner는 왼쪽 아래 꼭짓점을 지정하는 Point 객체입니다.

직사각형을 표현하기 위해, Rectangle 객체를 인스턴스화하고 값들을 애트리뷰트들에 대입해야 합니다:

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

표현식 box.corner.x은 “box가 가리키는 객체로 가서 corner라는 이름의 애트리뷰트를 고르세요; 그런 다음 그 객체로 가서 x라는 이름의 애트리뷰트를 고르세요”라는 뜻입니다.

rectangle

[fig.rectangle]

그림 [fig.rectangle]는 객체의 상태를 보여줍니다. 다른 객체의 애트리뷰트인 객체를 내장되었다(embedded)고 합니다.

반환 값으로의 인스턴스

함수는 인스턴스를 돌려줄 수 있습니다. 예를 들어, find_center 는 Rectangle을 인자로 받아서 Rectangle의 중심의 좌표가 들어있는 Point를 돌려줍니다:

def find_center(rect):
    p = Point()
    p.x = rect.corner.x + rect.width/2.0
    p.y = rect.corner.y + rect.height/2.0
    return p

여기에 box를 인자로 전달하고 돌아오는 Point를 center에 대입하는 예가 있습니다:

>>> center = find_center(box)
>>> print_point(center)
(50.0, 100.0)

객체는 수정된다

애트리뷰트 중 하나에 대입함으로써 객체의 상태를 바꿀 수 있습니다. 예를 들어, 위치 변경 없이 직사각형의 크기를 바꾸려면, width 와 height의 값을 수정하면 됩니다:

box.width = box.width + 50
box.height = box.width + 100

객체를 수정하는 함수도 만들 수 있습니다. 예를 들어, grow_rectangle는 Rectangle 객체와 두 숫자, dwidth 와 dheight,를 받아들여서 직사각형의 폭과 높이에 숫자들을 더합니다:

def grow_rectangle(rect, dwidth, dheight):
    rect.width += dwidth
    rect.height += dheight

여기 그 효과를 보여주는 예가 있습니다:

>>> print box.width
100.0
>>> print box.height
200.0
>>> grow_rectangle(box, 50, 100)
>>> print box.width
150.0
>>> print box.height
300.0

함수 안에서, rect가 box의 에일리어스이기 때문에 함수가 rect를 수정하면 box가 바뀝니다.

[연습 15.2.]

Rectangle 과 dx, dy라는 이름의 두 숫자를 받아들이는 함수 move_rectangle를 작성하세요. dx 를 corner 의 x좌표에, dy를 corner의 y좌표에 더해서 직사각형의 위치를 바꿔야 합니다.

복사

[copying]

에일리어싱은 프로그램을 읽기 어렵게 만들 수 있는데, 한 곳에서의 변경이 다른 곳에서 예상하지 못한 효과를 만들어낼 수 있기 때문입니다. 주어진 객체를 가리킬 수 있는 모든 변수들을 추적하는 것은 어렵습니다.

객체를 복사하는 것은 종종 에일리어싱의 대안이 됩니다. copy 모듈에는 copy 라는 이름의 함수가 있는데, 모든 객체의 복제를 만들 수 있습니다:

>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0

>>> import copy
>>> p2 = copy.copy(p1)

p1 과 p2는 같은 데이터를 갖습니다만 같은 Point 는 아닙니다.

>>> print_point(p1)
(3.0, 4.0)
>>> print_point(p2)
(3.0, 4.0)
>>> p1 is p2
False
>>> p1 == p2
False

is 연산자는 p1 와 p2가 같은 객체가 아니라고 알려주는데, 우리 예상대로 입니다. 하지만 두 점들이 같은 데이터를 갖고 있기 때문에 == 는 True를 줄 거라고 기대할 수 있습니다. 그런 경우에, == 연산자의 내정된 동작이 is 연산자와 같아서, 객체의 동등성(equivalence)이 아니라 동일성(indetity)을 검사한다는 것을 배우게 돼서 실망스러울 겁니다. 이 동작은 바뀔 수 있습니다–어떻게 하는지는 나중에 보게 됩니다.

Rectangle 을 복제하는데 copy.copy를 사용한다면, Rectangle 객체는 복사하지만 내장된 Point는 복사하지 않는 것을 발견하게 될 겁니다.

>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True

rectangle2

[fig.rectangle2]

그림 [fig.rectangle2]는 객체 다이어그램을 보여줍니다. 이 연산을 얕은 복사(shallow copy)라고 부르는데, 객체와 그 객체가 포함하는 모든 참조들을 복사하지만 내장된 객체 자체는 복사하지 않기 때문입니다.

대부분의 응용에서, 이 것은 여러분이 원하는 것이 아닙니다. 이 예에서, Rectangle 들 중 하나에 grow_rectangle를 호출하는 것은 다른 것에 영향을 주지 않습니다만, move_rectangle 를 어느 하나에 대해 호출하면 둘 다 영향 받습니다! 이런 동작은 혼란스럽고 오류를 일으키기 쉽습니다.

다행히도, copy 모듈에는 deepcopy라는 이름의 메쏘드가 있는데, 객체뿐만 아니라 그 객체가 가리키는 객체들, 그리고 다시 객체가 가리키는 객체들, 등등... 을 모두 복사합니다. 아마도 여러분이 이 연산을 깊은 복사(deep copy)라고 부른다는 것을 배워도 놀라지는 않을겁니다.

>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False

box3 와 box는 완전히 분리된 객체들입니다.

[연습 15.3.]

예전 것을 수정하기 보다는 새 Rectangle을 만들어 돌려주는 버전의 move_rectangle을 작성하세요.

디버깅

[hasattr]

객체로 작업하기 시작할 때, 몇 가지 새로운 예외들을 만나기 쉽습니다. 존재하지 않는 애트리뷰트에 접근하려고 하면, AttributeError를 얻습니다:

>>> p = Point()
>>> print p.z
AttributeError: Point instance has no attribute 'z'

객체의 형이 무엇인지 확실하지 않다면, 물어볼 수 있습니다:

>>> type(p)
<type '__main__.Point'>

객체가 측정한 애트리뷰트를 갖고 있는지 확실하지 않다면, 내장 함수 hasattr를 사용할 수 있습니다:

>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False

첫 번째 인자에는 어떤 객체건 올 수 있습니다; 두 번째 인자는 애트리뷰트의 이름이 저장된 문자열입니다.

용어

클래스 class:
사용자 정의 형. 클래스 정의는 새 클래스 객체를 만든다.
클래스 객체 class object:
사용자 정의 형에 관한 정보를 담고 있는 객체. 클래스 객체는 그 형의 인스턴스를 만드는데 사용할 수 있다.
인스턴스 instance:
클래스에 속하는 객체.
애트리뷰트 attribute:
객체와 연결된 이름 붙은 값들 중 하나.
내장된 embedded (객체 object):
다른 객체의 애트리뷰트로 저장된 객체.
얕은 복사 shallow copy:
객체의 내용을 복사하는 것으로, 내장된 객체들에 대한 참조를 포함하고 copy 모듈의 copy 함수로 구현되어 있다.
깊은 복사 deep copy:
객체의 내용뿐만 아니라 내장된 객체들과 그들에 내장된 객체들, 등등을 모두 복사하는 것으로, copy 모듈의 deepcopy 함수로 구현되어 있다.
객체 다이어그램 object diagram:
객체들과 그들의 애트리뷰트들과 애트리뷰트들의 값들을 보여주는 다이어그램.

연습

[연습 15.4.] [canvas]

스왐피(Swampy) ([turtlechap] 장 을 보세요)는 World라는 이름의 모듈을 제공하는데, 역시 World라고 불리는 사용자 정의 형을 정의합니다. 이렇게 들여올 수 있습니다:

from swampy.World import World

또는, 어떻게 스왐피를 설치했는지에 따라, 이렇게 할 수 있습니다:

from World import World

다음에 나오는 코드는 World 객체를 만들고 mainloop 메쏘드를 호출하는데, 사용자를 기다립니다.

world = World()
world.mainloop()

타이틀 바와 비어있는 사각형이 있는 창이 나타나야 합니다. 이 창을 점(Point)과 직사각형(Rectangle)과 다른 모양들을 그리는데 사용할 것입니다. 다음과 같은 줄들을 mainloop 를 호출하기 전에 삽입하고 프로그램을 다시 실행하세요.

canvas = world.ca(width=500, height=500, background='white')
bbox = [[-150,-100], [150, 100]]
canvas.rectangle(bbox, outline='black', width=2, fill='green4')

검은 테두리의 녹색 직사각형이 보여야만 합니다. 첫 번째 줄은 Canvas 를 만드는데, 창에 하얀 정사각형으로 나타납니다. Canvas 객체는 여러 모양을 그리는데 사용되는 rectangle 과 같은 메쏘드들을 제공합니다.

bbox 는 “경계 상자(bounding box)”를 표현하는 리스트들의 리스트입니다. 첫 번째 좌표 쌍은 직사각형의 왼쪽 위 꼭짓점입니다; 두 번째 쌍은 오른쪽 아래 꼭짓점입니다.

이렇게 원을 그릴 수 있습니다:

canvas.circle([-25,0], 70, outline=None, fill='red')

첫 번째 매개변수는 원의 중심의 좌표 쌍입니다; 두 번째 매개변수는 반지름입니다.

이 줄을 프로그램에 추가하면, 결과는 방글라데시의 국기(http://en.wikipedia.org/wiki/Gallery_of_sovereign-state_flags를 보세요)와 비슷해야 합니다.

  1. Canvas 와 Rectangle 을 인자로 받아서 Canvas 에 Rectangle 의 표현을 그리는 함수 draw_rectangle 을 작성하세요.

  2. Rectangle 객체에 color 라는 애트리뷰트를 추가하고, 색을 채우는데 color 애트리뷰트를 사용하도록 draw_rectangle을 수정하세요.

  3. Canvas 와 Point 를 인자로 받아서 Canvas 에 Point 의 표현을 그리는 함수 draw_point 를 작성하세요.

  4. 적절한 애트리뷰트들 갖는 Circle 이라는 이름의 새 클래스를 정의하고 Circle 객체를 몇 개 인스턴스화하세요. 캔버스에 원을 그리는 함수 draw_circle 을 작성하세요.

  5. 체코 공화국의 국기를 그리는 프로그램을 작성하세요. 힌트: 이런 식으로 다각형을 그릴 수 있습니다:

    points = [[-150,-100], [150, 100], [150, -100]]
    canvas.polygon(points, fill='blue')
    

제가 사용 가능한 색깔들을 나열하는 작은 프로그램을 작성했습니다; http://thinkpython.com/code/color_list.py 에서 다운로드 할 수 있습니다.