Python으로 객체지향 프로그래밍(oop)를 공부하고 있다. 오늘은 간단하게 자료구조 중 하나인 Stack 구현하면서 typing이나 상속 개념을 정리해보겠다. (이 글은 "타입 파이썬! 올바른 class 사용법과 객체지향 프로그래밍- 인프런" 강의를 참조하여 작성했습니다.)
Stack이란 쌓아 올린 형태의 자료구조를 말한다. Python에서는 연결리스트를 직접 구현할 필요없이 리스트 자료형의 append와 pop 매서드로 Stack을 사용할 수 있다. 박스를 쌓고 꺼내는 것처럼, 스택 자료구조는 가장 마지막에 들어간 데이터가 가장 먼저 나가는 LIFO(Last In First Out) 구조이다.
데이터를 모두 꺼내서 더이상 pop할 수 없으면 스택이 Empty하다, 라고 표현하고 반대로 꽉 차서 더이상 스택에 push를 할 수 없는 상황은 stack overflow가 발생했다고 한다.
박스처럼 생긴 데이터 단위는 노드(Node)라고 한다. 그리고 앞으로 구현할 연결리스트(Linked List)는 노드로 이루어져있다.
노드는 item, pointer 총 2가지의 속성을 가지고 있는데, pointer가 노드와 노드를 "연결"해주는 역할을 한다. 위의 그림을 보면 Node1의 pointer가 Node2를, Node2의 poiner가 Node3를 가리키는 것을 볼 수 있다. Node라는 클래스를 정의하여 이를 구현하면,
from typing import Optional
class Node:
def __init__(self, item, pointer: Optional["Node"] = None):
self.item = item
self.pointer = pointer
if __name__ == "__main__":
# 노드 생성
node1 = Node(item=11)
node2 = Node(item=22)
node3 = Node(item=33)
# 노드 연결
node1.pointer = node2
node2.pointer = node3
생성자 Constructor (__init__
함수)
self
는 인스턴스가 생성되었을 때, 그 인스턴스를 지칭한다. Optional("Node")
. "Node"이거나 None 이거나."Node"
: 클래스를 typing하는 도중, 자기 자신을 타이핑해야한다면 큰 따옴표를 쓴다.노드 생성, 연결
typing
int_var: int = 88
str_var: str = 88 # 오류 안 남
Optional("Node")
을 사용하여 타이핑했는데, 예상하지 않은 타입을 쓰더라도 실행 시 오류가 나지 않는다. (node1.pointer = 3
를 실행해도 에러가 안 뜬다.)isinstance(88, int)
함수를 써도 되고, mypy나 pyright 라이브러리를 설치해도 타입 체크 가능 mypy hello.py && python hello.py
(공식문서 참고)구현 내용
- head 속성 : 가장 첫 번째 노드를 head라고 하고, 노드가 없다면 None을 저장.
- length : int 타입. 현재 노드(데이터)의 개수
- print 했을 때 ","로 구분하여 item 출력
class LinkedList:
def __init__(self):
self.head: Optional[Node] = None
@property
def length(self) -> int:
if self.head is None:
return 0
cur_node = self.head
count: int = 1
while cur_node.pointer is not None:
cur_node = cur_node.pointer
count += 1
return count
def __str__(self) -> str:
# read
result: str = ""
if self.head is None:
return result
cur_node = self.head
result += f"{cur_node.item}"
while cur_node.pointer is not None:
cur_node = cur_node.pointer
result += f", {cur_node.item}"
return result
생성자 Constructor (__init__
함수)
Optional("Node")
length
@property
데코레이터를 붙여서 속성으로 구현해도 됨.@property
: 접근(read)은 가능하지만, 업데이트는 불가. (캡슐화)+=1
같은 업데이트하면 set 속성이 없다고 에러남(AttributeError: can't set attribute)@length.setter
데코레이터로 setter 함수 추가해줌.self.head
가 없다면 0 출력__str__
class LinkedList:
== class LinkedList(Object):
)__class__
, __init__
, __str__
)__str__
이 실행됨. __str__
함수를 새로 정의하여 덮어쓰기(Method Overriding) 가능. 구현 내용
- LinkedList를 상속받는다.
- push(item) : Stack 자료구조에 item을 받아 Node로 만든 다음 Stack 에 넣는다.
- pop() : Stack 자료구조에서 마지막 Node를 제거하고, 해당 item을 반환한다.
class Stack(LinkedList):
def push(self, item) -> None:
new_node: Node = Node(item=item)
if self.head is None:
# head가 가리키는 게 아무것도 없을 때
self.head = new_node
return
cur_node = self.head
while cur_node.pointer is not None:
cur_node = cur_node.pointer
cur_node.pointer = new_node
def pop(self):
if self.head is None:
raise ValueError("stack is empty")
else:
cur_node = self.head
if cur_node.pointer is None:
self.head = None
return cur_node.item
while cur_node.pointer.pointer is not None: # pointer의 pointer
cur_node = cur_node.pointer
result = cur_node.pointer
cur_node.pointer = None
return result.item
push(item)
self.head
에 new_node를 정의한다. pop()
위에서 item의 타입은 int, str, bool 등 여러가지가 가능하다. item의 타입은 자유롭되, 한 스택 안에 포함된 노드들은 모두 동일한 타입을 가지도록 구현할 순 없을까? 예를 들어 int와 str이 섞여있는 (1, 2, '3', 4) 와 같은 형태는 허용하지 않는 것이다. 이 때, Generic Type을 사용할 수 있다. Generic Programming은 데이터 형식에 의존하지 않고, 일반성을 유지하며 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술이다.
Generic Type은 위의 예시처럼 item의 타입을 고정할 때도 쓸 수 있고, item과 동일한 타입인 또 다른 변수를 정의할 때도 쓸 수 있다. typing과 마찬가지로 타입 체크가 되진 않지만 코드로 협업할 때 타입 힌트로 유용하다는 장점이 있다.
from typing import Generic, TypeVar
T = TypeVar("T") # Generic Type
위처럼 TypeVar로 타입 변수 T를 만든다. TypeVar("T", int, str)
로 타입 후보를 제한해줄 수도 있다. (mypy 0.982 버전에서는 오류가 나서 TypeVar("T")
로 수정 했다.)
Node, LinkedList, Stack 클래스에 각각 item을 T로 타이핑한 코드는 다음과 같다. Node의 item을 LinkedList와 Stack에서도 쓰기 때문에 클래스를 정의할 때 아래와 같이 Generic[T]
를 넣어줘야한다.
from typing import Optional, TypeVar, Generic
T = TypeVar("T")
class Node(Generic[T]):
def __init__(self, item: T, pointer: Optional["Node"] = None):
self.item = item
self.pointer = pointer
class LinkedList(Generic[T]):
def __init__(self):
self.head: Optional[Node[T]] = None
@property
def length(self) -> int:
if self.head is None:
return 0
cur_node = self.head
count: int = 1
while cur_node.pointer is not None:
cur_node = cur_node.pointer
count += 1
return count
def __str__(self) -> str:
result: str = ""
if self.head is None:
return result
cur_node = self.head
result += f"{cur_node.item}"
while cur_node.pointer is not None:
cur_node = cur_node.pointer
result += f", {cur_node.item}"
return result
class Stack(Generic[T], LinkedList[T]):
def push(self, item: T) -> None:
new_node: Node[T] = Node[T](item=item)
if self.head is None:
self.head = new_node
return
cur_node = self.head
while cur_node.pointer is not None:
cur_node = cur_node.pointer
cur_node.pointer = new_node
def pop(self) -> T:
if self.head is None:
raise ValueError("stack is empty")
else:
cur_node = self.head
if cur_node.pointer is None:
self.head = None
return cur_node.item
while cur_node.pointer.pointer is not None:
cur_node = cur_node.pointer
result = cur_node.pointer
cur_node.pointer = None
return result.item
length 매서드를 구현할 때 self.head를 고정하지 않고 업데이트하는 바람에 초반에 애를 먹었다. 타이핑하며 구현하는게 아직 익숙하지 않아서 (특히 Generic) 좀 더 연습해야할 것 같다.