Python Stack 구현

C++에서는 STL (Standard Template Library)에서 Stack 컨테이너를 지원해주지만
Python에서는 따로 제공해주지 않습니다.
List로 간단히 Stack에서 필요한 연산을 구현할 수 있기 때문인데요.

  • 스택 (Stack)은 LIFO 구조입니다.
  • List에서 관련 함수가 많기 때문에 간단하게 Class를 이용해서 구현하여 사용하거나 아니면 List 자체로 그냥 사용하기도 합니다.
  • Stack에서 많이 활용되는 5가지 함수에 대해서 직접 구현해봅시다.
  • Stack은 괄호, 접시, 짝짓기 등의 문제에 사용됩니다.

Top

top은 제일 마지막에 있는 원소를 반환해주면 됩니다. List의 가장 마지막 원소는 -1 Index를 통해 접근할 수 있습니다.

1
stack[-1]

Push

push는 제일 마지막 위치에 원소를 추가해주면 됩니다. List의 append 함수를 이용해 추가합니다.

1
stack.append(num)

Pop

pop은 제일 마지막 원소를 반환해주면 됩니다. List의 pop 함수를 이용할 수 있습니다.

1
stack.pop()

Size

size는 Stack의 현재 사이즈를 반환합니다. List의 len 함수를 이용하면 됩니다.

1
len(stack)

Empty

empty는 stack이 비었는지 판단합니다. List의 len과 0을 비교해주면됩니다.

1
len(stack) == 0

Class로 직접 구현하면 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Stack():
def __init__(self):
self.stack: list = []

def top(self) -> any:
return self.stack[-1]

def push(self, arg: any) -> None:
self.stack.append(arg)

def pop(self) -> None:
self.stack.pop()

def size(self) -> int:
return len(self.stack)

def empty(self) -> bool:
return len(self.stack) == 0

Short Circuit Evaluation 개념 및 주의점 (단축 평가)

AND(&&) 연산자 혹은 OR(||) 연산자와 같은 논리 연산자, Logical Operator를 사용할 때, 굳이 모든 연산을 다하지 않더라도 결과를 알게 되는 경우가 있습니다.

하나라도 False를 만족하는 AND 연산자하나라도 True를 만족하는 OR 연산자의 경우입니다.

이처럼 앞 부분의 결과만으로 조건을 판단할 수 있을 때 뒷 부분은 생략하는 것을 Short Circuit Evaluation, 단락(단축) 평가라 합니다.

1
2
3
4
5
if(conditionA && conditionB)
// conditionA가 False인 경우 conditionB 검사없이 넘어감

if(conditionA || conditionB)
// conditionA가 True인 경우 conditionB 검사없이 넘어감

Short Circuit Evaluation은 아래와 같이 응용할 수 있습니다.

1
if (myStack.empty() || myStack.top() != num)

만약 myStack이 empty일때 top을 호출하게 되면 런타임 에러가 발생하게 됩니다.
하지만 위와 같이 empty를 먼저 검사하게되면 Short Circuit Evaluation에 의해 empty일 경우 top이 호출되지 않기 때문에 에러를 막을 수 있습니다.

사용시 주의점

앞 부분으로만 결과가 판단되는 경우에 뒷 부분의 구문은 실행되지 않습니다.

아래 예시 코드를 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream> 

bool LeftCondition(bool b) {
std::cout << "Left Condition Executed " << b << std::endl;
return b;
}

bool RightCondition(bool b) {
std::cout << "Right Condition Executed " << b << std::endl;
return b;
}

int main(void) {
std::cout << "# Case A (false && true) " << std::endl;
if (LeftCondition(false) && RightCondition(true)) {
// Left
}

std::cout << "# Case B (true && true) " << std::endl;
if (LeftCondition(true) && RightCondition(true)) {
// Left, Right
}

std::cout << "# Case C (false || true) " << std::endl;
if (LeftCondition(false) || RightCondition(true)) {
// Left, Right
}

std::cout << "# Case D (true || true) " << std::endl;
if (LeftCondition(true) || RightCondition(true)) {
// Left
}
}

Case A와 D같은 경우 Short Circuit Evaluation에 의해 Left 조건만 실행됩니다.
다시 말해, Right 함수가 호출이 되지 않을 때가 있습니다.
만약 Right 함수이 꼭 호출되어야 한다면 위와 같이 작성할 경우 문제가 되고, 원인 파악이 어렵습니다.
따라서 앞의 조건과 뒤의 조건을 모두 호출하려는 경우엔 코드를 다른 방식으로 작성하여야 합니다.

ref) https://en.wikipedia.org/wiki/Short-circuit_evaluation

Namespace

네임스페이스는 코드에서 이름이 서로 충돌하는 문제를 해결하기 위해 나온 개념입니다. 예를 들어 foo라는 이름의 함수를 만들어서 사용하다가 외부 라이브러리를 참조했을 때, 거기에도 foo라는 함수가 있다면 충돌이 발생합니다. 이럴 경우, 컴파일러는 foo가 어떤 함수를 가리키는지 알 수 없게 됩니다.

아래와 같은 간단한 예제 코드를 살펴봅시다.

[namespace 예제 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

namespace nameA {
void foo() {
std::cout << "It is in nameA" << std::endl;
}
};

namespace nameB {
void foo() {
std::cout << "It is in nameB" << std::endl;
}
};

int main(void) {
nameA::foo();
nameB::foo();

return 0;
}

[실행 결과]

1
2
It is in nameA
It is in nameB

🎊 Scope Resolution Operator인 :: 기호를 이용해서 사용하면 됩니다.

nested namespace라고 해서 namespace를 중첩해서 사용하는 것도 가능합니다. namespace alias로 이름을 다르게 표현할 수 있는데 아래 예제를 통해 두가지를 한번에 확인해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

namespace A {
namespace B {
namespace C {
void foo() {
std::cout << "Nested namespace example" << std::endl;
}
}
}
}

namespace a::b::c {
void foo() {
std::cout << "Supported C++17" << std::endl;
}
}

int main(void) {
namespace ABC = A::B::C;
namespace abc = a::b::c;

A::B::C::foo();
a::b::c::foo();
ABC::foo();
abc::foo();

return 0;
}
1
2
3
4
Nested namespace example
Supported C++17
Nested namespace example
Supported C++17

🎊 만약 사용하는 빈도수가 높은 경우 using 이라는 지시자를 통해 컴파일러에 처리하도록 하면 됩니다. 보통 c++ 코드에서 쓰이는 using namespace std; 도 std라는 namespace를 :: 기호 없이 사용하겠다는 뜻으로 쓰인 구문입니다.

[using 예제 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

using std::cout;

namespace nameA {
void foo() {
cout << "It is in nameA" << std::endl;
}
};

namespace nameB {
void foo() {
cout << "It is in nameB" << std::endl;
}
};

using namespace nameA;

int main(void) {
foo();
nameB::foo();

cout << "using std::cout (not endl)" << std::endl;

return 0;
}
1
2
3
It is in nameA
It is in nameB
using std::cout (not endl)

using namespace를 통해 nameA:: 를 생략하여 사용하였습니다. (using을 했어도 nameA를 붙여도 무방합니다.) 또한, using namespace가 아니라 using 후 특정 항목만 지정해서 사용가능합니다. using std::cout 을 통해 컴파일러에게 지시를 하여 cout은 std::없이 사용했지만 endl의 경우 std::endl로 사용하였습니다.

🧨 웬만하면 using 지시자는 사용하지 않는 방향으로 개발하시는 것이 좋습니다. 예상치 못한 변수나 함수명이 겹치는 경우가 발생할 수 있기 때문입니다.

ref) https://en.cppreference.com/w/cpp/language/namespace