https://school.programmers.co.kr/learn/courses/30/lessons/43165

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

프로그래머스 '타겟넘버' 문제를 풀며, 제목과 같은 에러를 만났었습니다.

 

이에, 해당 문제를 해결할 수 있는 방법인 nonlocal과 global 변수 범위 지정 키워드에 대해서 정리하고자 합니다.

def solution(numbers, target):
    answer = 0
    length = len(numbers)

    def dfs(i, val):
        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

 

처음에는 위와 같이 코드를 작성하였습니다. 

저는 solution 함수 내에 answer 변수가 선언 되어 있으므로, dfs 함수 내에서도 사용할 수 있을 것이라 생각하였습니다.

 

에러 코드를 보면 '할당되지 않아서' 에러가 발생함을 알 수 있습니다.

 

파이썬 컴파일러는 dfs 내의 answer 변수를 solution 함수 내의 answer 변수와 동일한 변수로 보지 않고, dfs 함수 내의 '로컬 변수'로 인식하여 발생하는 에러였습니다.

 

def solution(numbers, target):
    answer = 0
    length = len(numbers)
    visited = [False] * length

    def dfs(i, val):
        nonlocal answer
        
        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

첫번째 해결 방법은 dfs 함수 내에 answer라는 변수에 nonlocal 이라는 keyword를 지정해주는 것입니다.

해당 키워드를 지정하게 되면, answer이라는 변수를 dfs 함수 내의 로컬 변수로 취급하지 않고 solution 함수 내의 answer 변수를 의미한다고 설정해주는 것입니다.

 

def solution(numbers, target):
    global answer
    answer = 0
    length = len(numbers)
    visited = [False] * length

    def dfs(i, val):
        global answer

        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

두번째 해결 방법은 global이라는 keyword를 지정해주는 것입니다. 

global 변수는 해당 변수를 전역 변수로 사용하겠다는 것을 의미합니다.

따라서 위와 같이 각 함수 내에 global이라는 keyword를 사용하여 answer이라는 변수를 로컬 변수가 아닌 전역 변수로 설정해주는 방법이 있습니다.

코딩 테스트 연습을 하던 중 다음과 같은 상황을 맞이하여 엄청 당황했었습니다.

s = [1,2,3]
for i in range(len(s)):
    a = s
    print(a,s)
    a.pop()

 

위 코드를 동작 시켰을 때, 제가 기대한 결과는 a라는 list에만 변화가 생기는 것이었습니다. 하지만 다음과 같은 결과가 나온 것입니다.

a: [1, 2, 3] s: [1, 2, 3]
a: [1, 2] s: [1, 2]
a: [1] s: [1]

파이썬의 copy 개념이 제대로 잡혀있지 않아 발생한 문제였습니다.

 


copy가 동작하는 방식은 데이터가 immutable / mutable 인지 그리고 shallow copy / deep copy인지에 따라 달라집니다.

  • immutable한 자료형은 int형, str형 과 같은 자료형이며 mutable한 자료형은 list, set, dictionary와 같은 자료형입니다.
  • shallow copy에는 '=', [:] , copy.copy(), 객체.copy() 연산이 있습니다.

 

shallow copy일 경우, 다음과 같이 동작합니다.

서로 다른 변수가 같은 객체를 참조하고 있는 형태 입니다. 바로 문제가 발생했던 상황이죠.

변수가 immutable할 경우 이 상황에서 문제가 발생하지 않습니다. immutable한 자료형은 바뀌지 않기 때문에 데이터에 변화가 생길 경우 새로운 객체가 생성됩니다.

 

하지만 문제는 mutable한 자료형에서 발생합니다. mutable한 자료형은 말 그대로 바뀔 수 있기 때문에 객체 자체가 바뀌게 됩니다.

 

코드로 살펴보겠습니다.

 

<immutable한 자료형에서의 shallow copy>

a = 3
b = a

print('a,b:',a,b)
print('ID of a and b:',id(a),id(b))

# a,b: 3 3
# ID of a and b: 140423687498096 140423687498096

b = 5
print('a,b:',a,b)
print('ID of a and b:',id(a),id(b))

# a,b: 3 5
# ID of a and b: 140654810294640 140654810294704

처음에는 동일한 객체를 참조하고 있지만, 값이 변하게 되면서 b가 새로운 객체를 참조하고 있음을 알 수 있습니다.

 

<mutable한 자료형에서의 shallow copy>

listA = [1,2,3]
listB = listA

print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [1, 2, 3]
# ID of listA and listB: 140613739648320 140613739648320

listB.append([1,2,3])
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3, [1, 2, 3]] [1, 2, 3, [1, 2, 3]]
# ID of listA and listB: 140613739648320 140613739648320

mutable한 자료형의 경우, 객체가 새로 생성되는 것이 아니라 객체 자체에서 변화가 생깁니다. 

저는 이러한 것을 고려하지 않고 '=' 을 통해 shallow copy를 하였기 때문에 문제가 발생했던 것이었습니다.

 

이러한 문제를 [:]을 통해 간단히 해결할 수 있습니다.(완벽한 방식은 아닙니다.)

listA = [1,2,3]
listB = listA[:]

print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [1, 2, 3]
# ID of listA and listB: 140613739648320 140613739648320

listB[0] = 99
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [99, 2, 3]
# ID of listA and listB: 140457913193792 140457913193600

listB.append([1,2,3])
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [99, 2, 3, [1, 2, 3]]
# ID of listA and listB: 140457913193792 140457913193600

listA = listB[:]

listB[3].append(999)

print('listA,listB:',listA,listB)
# ID of listA and listB: 140524217406784 140524217406592
# listA,listB: [99, 2, 3, [1, 2, 3, 999]] [99, 2, 3, [1, 2, 3, 999]]

보시면 [:]를 활용할 경우, listA와 listB의 주소가 다르기 때문에 Deep copy로 보이기도 합니다.

하지만 마지막을 보시면 [1,2,3] 객체에 999를 append하였을 때 두 리스트 모두 반영되는 것을 알 수 있습니다.

따라서, 완벽한 방식은 아니지요. 하지만 때에 따라 활용할 수는 있을 것 같습니다.


해결 방안

 

  • 자료형을 고려하여 shallow copy와 deep copy를 적절히 사용하는 것이 가장 좋은 방법이라고 생각합니다.

 

Reference

https://blockdmask.tistory.com/576

'파이썬 > 이론' 카테고리의 다른 글

[Python] Generator  (0) 2023.03.02
[Python] Python의 메모리 관리(Garbage Collector)  (0) 2023.02.14

파이썬 알고리즘 강의를 듣던 중 generator가 메모리 효율성에 큰 영향을 미친다는 얘기를 듣고 한번 정리해보고자 합니다.


 

generator란?

간단하게 말하자면, 데이터에 순차적으로 접근할 수 있는 객체인 iterator를 반환해주는 함수 입니다.

 

그렇다면 generator를 어떻게 활용할까요?

 


generator 활용법

제너레이터 활용법은 크게 두가지 입니다.

  • yield 와 next()의 활용
  • () 의 활용

 

1. yield와 next()를 활용한 코드

def func(l:list):
    for i in l:
        yield i +10


nums = func([1,2,3,4,5])

print(func(nums))   #<generator object func at 0x7fe75008f580>
print(next(nums))   #11
print(next(nums))   #12
print(next(nums))   #13
print(next(nums))   #14
print(next(nums))   #15

 

첫번째 print() 구문을 보시면 yield 구문을 활용하여 generator가 생성되신 것을 확인하실 수 있습니다.

 

def func(l:list):
    for i in l:
        yield i +10


nums = func([1,2,3,4,5])

print(func(nums))   #<generator object func at 0x7fe75008f580>

for i in nums:
    print(i)

 

위와 같이 반복문을 활용할 수도 있는데요. 결과는 위 코드와 같은 결과를 보입니다.

 

 

2. ()를 활용한 코드

nums = (i + 10 for i in range(1,6))

print(nums) # <generator object <genexpr> at 0x7fc340036190>

for i in nums:
    print(i)

이렇듯 구문을 ()로 감싸주게 되면 generator가 생성되는 것을 확인 하실 수 있습니다.

 

generator의 동작 방법은 알아보았습니다.

근데 이러한 동작 방법이 어떻게 메모리 효율성에 영향을 미치는 것일까요?

 

 


generator가 memory 효율성에 미치는 영향

 

Python 주로 데이터를 다룰 때 활용하는 언어입니다(데이터 수집, 인공지능 등). 데이터에서 유의미한 결과를 얻기 위해서는 정말 방대한 데이터를 활용할텐데요. 큰 양의 데이터를 한번에 담게 되면 메모리 부족 현상이 발생하여 프로그램이 갑자기 중단되는 등의 문제가 발생할 수 있습니다.

 

이때 generator는 개발자에게 엄청난 도움을 주게 됩니다.

generator는 모든 값의 순서를 기억한 상태로 동작하기 전까지 메모리를 할당하지 않기 때문입니다.

 

그렇기에 generator를 활용하게 되면 데이터를 한번에 적재하지 않아도 되기 때문에 메모리 소비를 줄일 수 있습니다. 즉, 한번에 적재하고 활용하는 방식 보다 훨씬 안정적인 방법이라고 할 수 있습니다.

 

또한 필요할 때 마다 호출하여 사용하는 지연 평가 방식이 가능하기 때문에 메모리를 효율적으로 사용할 수 있습니다.

'파이썬 > 이론' 카테고리의 다른 글

[Python] copy  (0) 2023.03.24
[Python] Python의 메모리 관리(Garbage Collector)  (0) 2023.02.14

C언어를 사용할 때는 malloc() 함수와 free()함수를 통해 메모리를 직접 할당/해제를 해주었습니다. 

그런데 Python을 사용할 때는 별도로 메모리를 관리하지 않더라구요.

문득 왜일까? 라는 생각이 들어서 알아보고 정리를 해보려고 합니다.

 

 

 

Python의 메모리 관리


파이썬은 Garbage Collector가 메모리를 관리해줍니다. C언어와 달리 사람이 메모리를 직접 할당/해제 하는 것이 아니라 언어 자체에서 직접 관리해주는 방식인데요. 

 

가장 많이 활용하는 방식은 Reference Counting 이라는 방식입니다.

 

 

Reference Counting 이란?


Reference Counting 방식이란, 객체를 얼마나 참조하는지를 통해 메모리를 관리하는 방식입니다.

 

Python은 객체를 생성하면 Heap 메모리에 객체를 생성하는데요. 

예를 들어, 다음과 같은 코드가 있을 때 s라는 변수가 객체를 참조하는 것을 counting 하여 메모리를 관리합니다.

 

s = 'hello world'

 

위와 같은 코드를 작성하면 다음과 같이 변수가 객체를 참조하게 됩니다.

'hello world'라는 객체를 s에 할당했을 때, Garbage Collector는 'hello world'라는 객체의 reference counting을 1 증가시키고 메모리에 할당합니다.

 

반대로 s라는 변수가 다른 객체를 참조하게 되었을 때는 reference counting이 0이 될 것이고 Garbage Collector는 이를 통해 메모리를 해제하게 됩니다.

 

직접 확인해보면 다음과 같습니다.

 

s가 호출된 함수가 종료되면 reference counting은 0이 되어 메모리는 해제되게 됩니다.

 

 

 

 

Garbage Collector의 장점 및 단점


 

사람이 직접 관리하지 않아도 메모리 관리가 되기 때문에 편하지만, 장점만 존재하는 것은 아닙니다.

Garbage Collector의 장점이라 함은 다음과 같습니다.

  • reference counting이 0 이 될 때마다 메모리를 해제해주기 때문에 실시간 작업이 가능하다.
  • 위와 같은 이유로 메모리 관리가 간편하며 즉시 메모리에서 해제가 가능하다.

반면 단점 또한 존재합니다.

  • Object 마다 reference counting을 수행해주어야 하기 때문에 관리 비용이 많이 듭니다.
  • Object의 reference count가 0이 될 경우, 연쇄적인 Garbage Collecting이 발생할 수 있습니다.
  • 사람이 직접 할당/해제를 하지 않기 때문에 세밀한 메모리 관리가 어렵다.

'파이썬 > 이론' 카테고리의 다른 글

[Python] copy  (0) 2023.03.24
[Python] Generator  (0) 2023.03.02

안녕하세요.

오늘은 백준 알고리즘을 풀며 만났던 TypeError: cannot unpack non-iterable int object 에 대해 글을 써보려 합니다.

 

먼저, 해당 에러를 접한 상황은 다음과 같습니다. (백준 알고리즘 1916번, 18352번)

저는 deque에 (distance,node)의 값을 넣고, pop() 할 때마다 두 값을 한번에 얻고 싶었습니다. 다음과 같이 말이죠.

q = deque((distance,node))
newDistance , newNode = q.pop()

하지만, TypeError가 발생하여서 문제는 풀어야해서 항상 다음과 같이 풀었습니다.

q = deque()
q.append((distance,node))
newDistance , newNode = q.pop()
print(newDistance,newNode)

 

문제를 풀고 나서, 왜 그렇지 하고 여러가지 테스트를 해보았는데 역시나.. deque를 잘못사용하고 있었습니다.

테스트를 위해 아래와 같이 코드를 돌려보았습니다.

q = deque((0,99))
print(q)
q = deque([0,99])
print(q)
q = deque([(0,99)])
print(q)

 

그랬더니 위와 같은 결과 값이 나왔습니다. 여기서 아뿔사 했습니다..(역시 기본이 제일 중요하다는 것을 뼈저리게 느끼며...)

 

파이썬의 deque() 함수는 선언할 때 들어왔던 인자를 자동으로 deque화 시켜서 저장한다는 것을 놓쳤던 것입니다.

 

다음과 같이 사용하면, q[0]에 0이 q[1]에 99가 들어가는 것이죠.

print를 통해 출력해보면 이런 결과값이 나옵니다.

q = deque((0,99))
print(q[0],q[1])

q = deque([0,99])
print(q[0],q[1])

 

 

 

q = deque()
q.append((distance,node))
newDistance , newNode = q.pop()

***

(3번째줄) 이렇게 사용한다면, 입력받고자 하는 변수 갯수와 입력하고자 하는 변수 갯수가 맞지 않기 때문에 Error가 발생하는 것이었습니다.

 

'TypeError: cannot unpack non-iterable int object' -> ' iterable 하지 않은 int형 객체를 unpack 할 수 없다' Error 내용은

입력하고하 하는 변수 갯수는 1개인데, 입력받고자 하는 변수 갯수가 2개 여서 값을 할당할 수 없다라는 것입니다.

 

 

그렇다면 제가 의도한 방식대로 하려면 어떻게 했어야 할까요?

 

정답은 다음과 같이 [] 안에 다시 tuple형태로 감싸서 값을 담는 것입니다.

q = deque([(0,99)])

한번 값을 확인해보면 다음과 같이 나옴을 알 수 있습니다.

q = deque([(0,99)])

print(q)

distance , node = q.pop()
print('distance:',distance,'node:',node)

 

 

오늘은 deque를 사용하며, 발생했던 TypeError에 대해서 정리해보았습니다.

진짜 오늘도 기본이 중요함을 뼈저리게 깨달으면서 포스팅을 마치겠습니다.

읽어주셔서 감사합니다.

문제를 푸시다보면 다양한 에러를 만나실텐데요. 

오늘은 백준알고리즘 1697번을 풀면서 발생한 메모리 초과에 대해서 글을 써보려고 합니다.

 

메모리 초과가 발생하는 이유를 간단히 말씀드리자면 '스택 오버플로우'가 발생하기 때문입니다.

문제를 푸시면서 가장 많이 접할 수 있는 경우는 다음 두 경우라고 생각합니다.

 

1.  너무 많은 변수들을 배열 등에 저장할 경우.

2. DFS 등에서 재귀적 호출을 통해 너무 많은 함수들을 호출할 경우.

 

저 같은 경우에는, 백준 알고리즘 1697번에서 1번에 의한 메모리 초과가 발생하였는데요.

시작점으로부터 도착점까지 도달할 때 까지, (시작점-1, 시작점+1, 시작점*2)에 해당하는 값들을 탐색하는 과정에서 실수를 했습니다.

 

시작점 = 5, 도착점 =17을 기준으로 처음 들어간 (5-1,5+1,5*2)에 대해서 다음 싸이클 값들이 어떻게 저장되는지 출력한 것인데요.

아래 스크린샷을 보시면, 하나의 경우의 수에 대하여 계속해서 3가지의 새로운 경우의 수가 들어가고 있습니다.

해당 문제의 시작점과 도착점이 될 수 있는 정수의 범위가 0~100,000 이었으니, 시작점이 1이고 도착점이 100,000일 경우 엄청나게 많은 값들이 큐에 저장되었겠죠?

 

이로 인해, 계속해서 메모리 초과가 발생하였습니다.

 

메모리 초과에 대한 해결 방안은 다음과 같습니다.

1. 배열에 너무 많은 값들이 들어가는 경우에는 값이 들어갈 수 있는 범위 제한 및 들어가는 데이터 수를 제한 하는 조건 등을 통해서 해결하실 수 있습니다.

 

1697번을 예시로 들자면,

Max = 10 ** 5
visitedCnt = [0]*(Max+1)

배열을 선언할 때, 적당한 크기로 배열의 사이즈를 할당합니다.

 

for i in pos:
    if 0 <= i <= Max and not visitedCnt[i]:
        visitedCnt[i] = visitedCnt[idx] + 1
        queue.append([i,visitedCnt[i]])

두번째로는 들어갈 수 있는 값들의 범위를 지정하여 무한 루프를 방지해주고, 해당 값을 처리한 적이 있는지 확인하여 중복된 데이터가 처리되지 않도록 하였습니다.

 

위, 두가지 방법을 통해 메모리 초과를 해결 할 수 있었습니다.

 

2. 재귀 등으로 인한 너무 많은 함수 호출로 인한 메모리 초과의 경우에는 다음과 같습니다.

첫번째로는 재귀가 너무 많은 횟수가 반복되지 않도록 적절한 조건 설정 해주어 메모리 초과가 발생하지 않도록 하는 방법이 있고,

두번째로는 재귀함수를 반복문으로 푸는 것입니다.

재귀함수를 통해 호출된 함수는 메모리 영역의 스택에 저장되는데, 동일한 함수여도 호출 될 때마다 스택영역에 계속해서 메모리가 할당되게 됩니다. 너무 많은 함수가 호출이 된다면 스택 영역의 메모리를 초과하게 되는 '스택 오버플로우'가 발생합니다.

 

즉, 핵심은 스택 영역의 메모리를 초과하지 않게 사용하는 것이죠.

 

첫번째 방법은 스택 영역을 활용하되 사용하는 메모리 영역을 초과하지 않게 하는 방법이고, 두번째 방법은 아예 스택 영역을 사용하지 않는 방법입니다.

 

 

메모리 초과라는 문제에 대해 조금이나마 도움이 되었으면 좋겠습니다. 

읽어주셔서 감사합니다.

백준 알고리즘 10026번을 풀던 와중, 채점 과정에서 런타임 에서(RecursionError)가 발생했습니다.

 

발생한 이유를 알아보니 다음과 같습니다.

-파이썬에서 정한 최대 재귀 깊이보다 재귀가 너무 많이 발생할 경우.

 

즉, 설정된 값보다 재귀가 많이 발생할 경우 RecursionError가 발생함을 알게되었습니다.

 

해결 방법은 다음과 같습니다.

- 재귀를 사용하지 않는 방식으로 코드 구현하기. (재귀를 활용한 코드를 반복문을 통한 코드로 변경)

- 파이썬 sys 모듈에 있는 sys.setrecursionlimit()을 이용하여 최대 재귀 깊이를 변경해주기

sys.setrecursionlimit(10000)

해당 문제에서 나올 수 있는 최대 깊이인 10000으로 값을 설정해주어 문제를 해결하였습니다.

 

 

*재귀함수의 경우 코드를 효율적으로 짤 수 있다는 장점이 있지만 반대로 함수에서 함수를 호출하는(자신을 재참조) 방식이기 때문에 메모리를 많이 잡아먹고 함수가 return 될 경우 메모리상에서 이전 함수 위치로 이동하기 때문에 반복문보다 시간이 오래걸릴 수 있습니다.

 

장점과 단점이 공존하는 만큼, 무조건적인 재귀의 활용보다는 재귀의 특성을 바탕으로 상황에 맞춰 적절히 활용하면 더욱 좋은 코드가 나오지 않을까 생각합니다.

 

읽어주셔서 감사합니다.

+ Recent posts