Walrus 연산자


이 글은 파이썬 코딩의 기술(브렛 슬라킨 지음)을 읽고 정리하는 글입니다.

Walurs 연산자란?

대입식 이라고 하며 파이썬의 고질적인 코드중복 문제를 해결하기 위해 3.8에 새로 도입된 구문이다. 아래와 같이 사용한다.

1
a := b

바다코끼리를 연상시켜서 walrus라고 한다. :=

기능

일반 대입문이 사용되지 못하는 위치에서 변수에 값을 대입할 수 있다.

  • while, if문의 조건식
    이를 통하여 코드의 길이를 줄이고 가독성을 높여 두마리 토끼를 잡을 수 있다

예시

일반적인 while break 문

1
2
3
4
5
6
while True:
flag = get_flag()
if flag:
break
else:
# Task 코드

walrus를 사용한 while문

1
2
while flag := get_flag():
# Task 코드

reference

cudatoolkit 버전별로 관리하기


Anaconda를 이용한 환경별 Cuda Version 관리

  1. conda-forge 채널을 추가
    • nvidia 에서도 받을수 있지만 다운로드 속도 이슈가 존재하여 conda-forge가 안정적임
1
conda config --append channels conda-forge
  1. cudatoolkit 설치
    • 원하는 버전의 cudotoolkit 설치
1
conda install cudatoolkit=[Version] -c conda-forge
  1. cudatoolkit-dev 설치
  • cuda Version에 따른 사용 채널 명
    • 11.x : conda-forge
    • 10.2 : trenta3
1
conda install cudatoolkit-dev=[Version] -c [Channel]

여기까지 진행하면 conda에서 설정되지 않았던 nvcc가 동작한다.
추가적으로 환경별로 다른버전의 cuda Version을 사용할 수 있다.

Functools.Partial


과제를 하다가 functools의 partial을 쓸 기회가 있었는데 오류를 뱉어서 나름대로 해결을 하고 정답지를 보니 작성한 코드가 달라서 한번 조사해보고 쓰는 글입니다.

Functools의 Partial에 대해 알아보자

  • functools Module의 내장 함수인 Partial은 기존에 존재하는 함수에 추가적인 인수를 지정하여 새로운 버전의 함수로 만들어주는 기능을 가지고 있다.

Partial의 예시

말로만 설명하면 이해하기 힘드니 코드와 같이 보자

다음과 같이 n진수를 10진수의 형태로 바꾸어 주는 함수가 있다고 하자

1
2
def to_int(num, base):
return int(num, base=base)

이때 특정한 수 2와 3을 Base로 하는 함수를 원한다고 할 때 다음과 같이 사용할 수 있다.

1
2
3
4
5
def two_to_int(num):
return to_int(num, base=2)

def three_to_int(num):
return to_int(num, base=3)

하지만 매번 특정한 수를 Base로 하는 함수를 이렇게 여러줄로 정의하면 코드의 줄도 길어지고 귀찮을수 있는데 이때 Partial을 사용하면 다음과 같이 함수를 새로 정의할 수 있다.

1
2
3
4
5
6
7
two_to_int = partial(to_int, base=2)
three_to_int = partial(to_int, base=3)

two_to_int("10")
2
three_to_int("120")
15

내부 구조

Partial 함수는 아래 코드와 같이 정의되어있다

여기서 2가지 방법으로 함수의 인자를 미리 설정할 수 있다.

  • partial(함수이름, 변수)
    • newfunc.args에 변수를 저장한다
  • partial(함수이름, 변수명=변수)
    • newfunc.keywords에 변수명이 Key, 변수가 value로 저장된다
    • 기존에 있는 변수를 변수명을 통해서 다시 지정할 때 그 변수의 입력위치가 중간에 존재할 경우 Error가 발생한다
1
2
3
4
5
6
7
8
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
  • 함수가 실행될때는 newfunc.args에 저장된 args, 함수에 입력되는 parameter, newfunc.keywords 순으로 순차적으로 입력된다
1
2
3
4
5
6
7
8
9
10
11
12
def tempfunc(one, two, three, four):
print(one, two, three, four)

a = partial(tempfunc, 1, 2)
a(5, 3)
output
1 2 5 3

a = partial(tempfunc, two=1)
a(5, 3, 2)
output
TypeError: tempfunc() got multiple values for argument 'two'

reference

윈도우, Linux 그리고 Docker


부스트 캠프를 진행하던 도중 질문게시판에 mlflow를 윈도우에서 돌렸는데 에러가 발생했다고 올라와서 궁금해서 조사하던 도중 알게된 사실과 추가적으로 궁금해서 찾아본 것들에 대해 정리해 보고자 한다

발생한 문제점

  • docker: Error response from daemon: invaild mode: 경로와 같은 에러가 발생하였다.

조사과정

  • 평소 python에서 코딩을 할 경우에도 /와 \를 혼용해서 사용하면 매우 쉽게 에러가 발생하는 모습을 보았기 때문에 Linux기반인 docker에서 windows 명령어를 사용해서 에러가 나는것이 아닐까 생각하였다

알게된 점

  • 원인은 : symbol에 존재했다. 윈도우에서는 : symbol을 C:\directory식으로 드라이브를 표기할 때 사용하지만, mount할때에는 : 를 연동할 대상의 구분자로 사용을 하기 때문이다
  • docker 에서 volumn mount를 시행할 시 입력되는 경로는 무조건 절대경로로 적어야한다. pwd로 입력해야한다고 doc에 나와있었다

해결 방법

  • C: 형태를 //C/directory 형태로 바꿔서 입력을 한다
    • 기존 윈도우에서 사용하는 형태를 리눅스 커맨드 형태로 바꿔서 입력한다
    • 어차피 docker 내부에서 작동할 커맨드이기 때문에 윈도우 커널에서도 잘 돌아간다
  • “”로 한번 감싸준다
    • “”로 싸주어서 전체 C:\가 링크인것을 알려준다

결론

  • 기본적으로 docker는 Linux 기반이기 때문에 windows로 사용할 경우 많은 제약이 당연하게도 뒤따른다
  • windows에서 Docker를 쓸 때는 WSL을 이용하자

reference

Garbage collection


서론

이 글에서는 Python의 Garbage Collector에 대해서 다룬다.

Garbage collection

Garbage Collector(이하 GC)는 메모리 관리 기법중 하나로 동적으로 할당했던 메모리 영역 중에 필요없게 된 부분을 해제하는 기능이다. C, C++등의 언어들은 수동메모리 관리를 전재로 설계되어 malloc, free등의 함수로 직접 메모리를 관리 해주어야 하지만, Python, Java, C# 같은 고급 언어들은 GC가 내장되어 있어 자동으로 메모리 관리를 해준다.

Python GC의 작동방식

Python의 GC는 reference count + Generational GC 방식으로 메모리를 관리한다.

reference count

1
2
3
4
5
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt; /* reference count */
struct _typeobject *ob_type;
} PyObject;

python의 객체들은 모두 reference count를 가지고 있으며 이것을 통해서 GC가 언제 메모리에서 제거할지를 판단한다. 참조가 이루어지면 reference count가 1증가하고, 참조가 끝나면 1감소한다. 최종적으로 0이 되면 GC에서 더이상 필요없는 오브젝트로 판단하고 메모리에서 삭제한다.

이러한 방식은 프로그래머가 어느정도 오브젝트의 메모리 할당 해제 시점을 유추 할 수 있고, 대부분 사용된 직후 해제되기 때문에 캐시에 저장되어 있을 확률이 높아 빠르게 할당 해제가 이루어지는 장점이 존재한다.

하지만 아래와 같은 단점 또한 존재한다. 두개 이상의 객체가 서로 가리키고 있을 경우(순환 참조) reference count가 0까지 내려가지 않는 상황이 발생 할 수 있다. 또한 Multi Thread 환경에서는 공유자원에 대한 참조가 되기 때문에 추가적인 Lock 등이 필요하거나, 지속해서 GC에서 판단하는 프로세스를 거쳐야 하기 때문에 수행 성능의 저하 등의 문제가 발생 할 수 있다.

이러한 단점들을 해결하기 위해 Python에서는 순환 참조를 탐지하는 알고리즘과, 보조적인 Generational GC를 두었고, GIL로 Multi Threading에 제한을 걸어 버렸다.

Generational GC

새롭게 할당된 오브젝트일수록 금방 메모리에서 해제될 확률이 높다는 통계에서 기반한 메모리 관리방법이다. 각각의 오브젝트가 GC가 실행하고나서 메모리에서 해제되지 않으면 다음 세대로 넘어가게 되는 방식인데 더 젊은 세대(금방 생성)일수록 자주 GC의 프로세스에서 할당 해제할 오브젝트인지 판단이 내려지게 된다.

이 과정을 보조적으로 추가하면서 python에서는 조금더 효율적으로 메모리를 관리 하고 있다.

Generational GC

reference

Global Interpritor Lock


GIL. Global Interpritor Lock.

Global Interpritor Lock(GIL)이란 Python에서 오직 하나의 Thread만 동작하도록 컨트롤하는 Lock(또는 Mutex)이다.
이 말은 타임라인상에 오직 하나의 Thread만 실행될 수 있다는 것을 말한다. Single Thread를 사용하는 코드에서는 그렇게 큰 영향을 주지 않지만, Multi-Thread를 사용하는 코드에서는 병목현상을 일으킬 수 있다.

GIL이 어떻게 동작하는지 한번 살펴보자

위에서 이야기 한 것처럼, GIL에서는 오직 하나의 Thread만 활성화되어서 코드를 실행하는데 이는 아래의 그림처럼 그려낼 수 있다.

GIL

Thread가 전환 될 경우에는 기존의 Thread의 작동이 멈추고 동작을 시작하게 된다.
기존의 하던 작업들을 멈추고 다른 Thread로 넘겨야 하기 때문에 context switch가 일어나게 된다.

그러면 왜 GIL을 사용할까?

먼저 왜 GIL을 사용하는지 알기 위해서는 Python의 Garbege Collector(GC)에 대해 알고 있어야 한다. (GC에 대한 자세한 내용은 추후에 다룰 예정이다) 고급언어인 python에서는 C언어와 다르게 자동으로 메모리 관리를 해주는 GC라는 것을 사용한다.

이 GC는 오브젝트가 얼마나 많이 참조(실행)가 되었는지를 카운팅하는 reference count 라는 것으로 메모리에서 삭제할지 유지할지를 결정한다. 하지만 동시에 여러군데에서 참조가 이루어 질 경우 Race Condition 등의 문제들이 발생 할 수 있기 때문에 이를 방지하기위해 Mutex나 Semaphore등의 Lock이 필요했는데 이것을 Python에 존재하는 수많은 Object에 전부 적용하는것은 성능상으로도 좋지 않을 뿐만 아니라, Deadlock 같은 매우 치명적인 위험상황이 발생 할 수 있었다

그래서 선택한것이 따로 Object에 Lock을 두지 않고, 타임라인에서 실행되는 Thread를 딱 하나만 두도록 Interpritor를 Mutex로 잠궈버렸다. 이렇게 되면 여러곳에서 동시 참조가 되지않으니 성능의 하락 없이 위의 동시참조의 문제를 해결하게 된것이다

그러면 python에서는 병렬화된 코드는 어떻게 써야하나

  1. Multi Process를 사용한다
    Thread를 막아버린것 이기 때문에 Multi Processing을 사용하면 잘 돌아간다. 물론 Process간에 공유자원을 가지기 위해서는 많은 작업들이 필요로 하기 때문에 context switching이 발생하여 Thread에 비해 속도가 늦을 것이다. 하지만 Windows는 OS 보안상 이유로 이걸 막아버렸다.
  2. Multi Threading을 사용한다
    아까 Multi Threading을 막아놨다고 했는데 뭔 소리냐 할 수 있지만, CPU에서 대부분의 연산이 돌아가는 코드를 제외한 I/O Bound 계열의 문제들은 file system과 Network의 하위 컴포넌트에서 돌아가기 때문에 Single보다 더 빠르게 진행 할 수 있다.
  3. 굳이 사용하고 싶다면 PyPi나 Jython 같은 다른 Python implementation으로 사용하는 방법도 존재하지만, 그 코드가 python에서와 똑같이 동작할 보장은 없다
  • 그리고 numpy나 Scipy등의 ML에서 많이 사용하는 Module들은 C기반으로 만들어져서 GIL의 굴레에서 자유롭게 연산이 된다고 한다.

reference