MLOps 강의를 정리하고 docker, k8s를 공부하려했지만
당연하게 사용해왔던 파이썬을 조금 더 공부해보자 !
간단하게 변성윤님 MLOps의 마지막 강의 더 공부해보면 좋을것들에서부터 시작한다 !
후에는 모르는걸 찾는것도 실력!
Python
Generator, Decorator, GIL 등
멀티프로세싱은 어떻게 동작하는가
메모리 영역에서 어떤일이 발생하는가
1번 Generator부터 간다 !
역시 다짜고짜 검색해보기, docs가 있다면 읽어보기.
Python docs에서는 다음과 같이 설명한다.
generator - docs
generator - wikidocs
iterator를 반환해주는 함수 generator.
iterator
반복가능한 객체로 next() 메소드를 통해 순차적으로 접근이 가능하다.
대강 yield를 제외하면 그저 그런놈이라는데 yield는 뭘까
찾아보니 docs보다 예제를 직접 확인해보는게 이해하기 편해보인다.
위의 예시를 확인해보자.
먼저 yield i을 선언하지 않았을때의 흐름을 생각해보면
genenerator 함수는 반환하는 값이 없어 None값에 대한 에러를 보여줄 것이다.
그렇다면 yield i 는 무슨짓을 한걸까
위의 예시에서 yield가 하는짓 3번을 확인해보자.
generator함수는 종료하지 않고 유지된다
그렇다면 generator 함수 내부를 함수 밖에서 건드릴 수 있을까?
send method를 사용한다면 가능하다 !
received_value = yield
위의 예시에서 next(generator)로 yield를 시작!
결과를 확인해보면 yield를 만나 None값을 반환할 것이다 !
다시 한 번 next(generator)를 시도하면 print 다음 yield를 만나고 None ** 2 연산에 대한 오류를 보여준다.
이제 send 메소드를 사용해보자 !
처음 next(generator)로 시작하여 received_value = yield 에 접근한다.
yield를 만난 함수는 값을 반환하고 yield로 아무 값도 선언되지 않았으므로 None을 반환하고 함수를 일시정지(?)한다.
generator.send(7)을 통해 yield에 7을 전달하고 received_value는 7을 저장하게 된다.
print(f"Received_value : {received_value}")를 통해 received_value를 출력한다. => Received_value : 7
다시 yield를 만나 received_value**2를 반환한다. => 49
1부터 반복.
조금 헷갈리지 않는가.. 나는 엄청 헷갈려서 여러번 돌려봤다..
실험내용은 마지막에
generator를 사용하는 방법은 정말 간단하다. list를 표현하는 []대신 ()를 사용하면 된다 !
접근하려면 next()메소드를 사용해야 하는 번거로움이 있는데 왜?!
list의 경우 사이즈가 커질수록 메모리를 많이 잡아먹는 사실은 자명하다!
generator는 어떨까?
0부터 499의 정수 리스트보다 0부터 4999의 정수 리스트가 메모리를 많이 사용하는 것은 당연하다.
generator를 확인해보면 500개의 정수, 5000개의 정수 상관없이 일정한 메모리를 사용하게 된다.
list와 generator의 동작방식 차이인데
list의 경우 list 내부에 속한 데이터를 모두 메모리에 적재한다. list의 크기가 늘어난다면, 사용하는 메모리도 커진다.
generator는 메모리를 한 번에 모두 적재하는 것이 아닌 next()메소드를 통해 접근할때마다 메모리에 적재한다. 따라서 next()를 더 많이 부르게 되겠지만, 처음 메모리에 적재되는 양은 일정하다.
계산결과 값이 필요할때까지 계산을 늦출 수 있다 !!
어느정도 감은 오지만 확실한 이해를 위해 예시를 보면
수행시간이 긴 연산을 원할때 할 수 있다 !
입맛대로 코드를 작동시킬 수 있다 !
yield 두개면 벌써 어지럽다.
차근차근 해보자
기본적으로 코드의 흐름을 보면 '=' 뒷부분에 연산을 끝내고 '=' 앞에 값에 저장한다.
'='뒷부분에 연산을 보면 yield g1을 만나 g1을 반환하고 대입하기 전 연산을 멈추는 것으로 보인다 !
따라서
next(ge)를 통해 yield를 시작한다 !
g = yield g1 을 통해 g1을 반환 !
next(ge)의 반환은 g1인 1이 온다.
다시 한 번 next(ge)!
g1은 1을 반환하고 다음 아무 입력없으므로 yield g1은 None !
yield g1을 g(==None)에 저장하고 f = yield g2를 만나 g2(==2)를 반환 !
이때 ge.send(5)를 시도한다면?! f에 g2를 저장하기 전 g2에 5를 전달한다 !
f에 저장된 값은 g2(==5) !
print문에서 출력되는 값은 그럼 "None None None 5" 인가?!
아니다 g1과 g2는 위에 1과 2로 저장되어있지 않은가 ! 위의 g1과 g2를 참고하여 "1 2 None 5"를 출력하게 된다 !
위의 예시에서 g1,g2의 주소를 yield에서 참고하여 새로운 변수(?) g1',g2'를 만들고 반환한다. 그리고 g1' g2'은 다음 생성될 값을 대기한다. 입력이 없다면 None을 g와 f에 저장한다. g1과 g2 변수의 값, 주소는 변하지 않는다. 따라서 print(g1,g2,g,f)는 1,2,None,5를 출력한다.
아직 헷갈린다면 지역성을 생각해보자 !...
예시 설명하려다가 알게된 사실인데..
파이썬의 -5~256까지의 정수는 미리 메모리에 할당되어있고 항상 동일한 id를 가진다 !
예를 들어
a = 200
b = 200
print(id(a)==id(b))
출력 결과는 True다 !!
'is'와 '==' 의 연산 차이도 확인해보자 !!