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) 좀 더 연습해야할 것 같다.