본문 바로가기

~ 공부 ~/Computer

역시 일기는 몰아써야 제맛 6편(마지막, 즐추요) - 파이썬("리스트에 왜 빼기는 없죠?")

후... 이렇게 엄청나게 긴 글쓰기 시간이 지나고.. 정말 오래 걸리네요. 정말 레이아웃이나 컬러/사진 같은 것들 넣고 하면 진짜 엄청 오래 걸리겠네요 ㄷㄷㄷ.

아 언리얼 해야 하는데 진짜... 집에가서 좀 해보고 자야겠습니다. 오늘은 성묘 2군데 갔다왔으니 내일은 제사 지내고 하겠네요. 진짜 내일은 해야되는데... (아 참고로 저희팀은 비행기 슈팅 겜같은건데 비행기의 움직임이 어렵습니다.. 비행기도 하고 콘솔겜 같은 3인칭 FPS 인간형도 해보고 싶고 카메라도 시점도 여러게로 바꾸고 싶고 건담 같은것도 해보고 싶고마크로스 같은것도 해보고 와 진짜 하고 싶은건 많은데...) 진짜 해야합니다!!! 내일 하겠습니다!! ㄷㄷㄷㄷ 진짜 내일도 못하면 어떻게 하지 ... ㅠ.ㅠ

그럼 이만 즐거운(?) 추석 보내세요. 물론 잔소리로 부터 벗어날수 있다면 말이죠... (오늘 친가 쪽 으로부터 방어전 1차전 했습니다 ㅋㅋㅋ 내일은 외가 쪽에서 방어전 또 치루겠네요)


파이썬

 원래는 언리얼 엔진 프로젝트를 해야하는데... 하다가 하도 막혀서 약간 쥐쥐치고 있는 상황에(울팀들 미안요 ㅠ.ㅠ) 단톡방에 "파이썬에 왜 빼기는 없는가?" 라는 원초적(?) 질문에 2시간 해멨네요..  역시 해야되는게 있을때 딴길로 새는건 재밌네요. (파이썬은 학원 못 다니고 scrapy한후 안했었는데 말이죠. 아 맞다 오늘 단톡방에서 tweepy하는법 때문에 또 한번 해보게 됬네요 ㅎㅎ 이것도 포스팅 해야겠네요)

일단 제가 간단히 쓴 대답으로는

역시 해야되는게 있는데 딴길로 샐때가 뭐든 재밌네요..

1. 가장 간단한 대답으로는.. "-" 빼기를 쓰던 뭘 쓰던 어떻게 해야되는지 몰라서 아닐까요? list1 - list2 를 했을때 뭘 해야하는지요. list1에서 list2와 중복되는걸 다 빼고 거기다가 뺸칸만큼 앞으로 땡겨서 새로운 list를 만들지 어떨지요. 어떻게? 뺄지가 관건인데 이건 직관적이지 않고 여러 방향성이 있어서 그런것 같습니다. (중간부터 뺴는지 뒤부터 없애는지 앞에서부터 없애는지 중복된거 없애는지 등등...)

2. list1 + list2 를 한다는거는 __add__ 를 써서 연산을 한다는건데 리스트 + 리스트 의 경우는 list1을 바꾸지 않고 새로운걸(두개를 다 합친값)내놓는 건데...     이걸 굳이 만들자면 만들어도 되는거 같습니다.   "난, 리스트 - 리스트 를 보고싶다!" 라고 하시면.. 연산자 오버로딩을 해야하는거 같습니다. 아마 리스트는 __sub__ 같은게 없을테니 상관없지 않을까합니다..      그나저나 이런 원초적(?)질문은 답이 끝이 없네요...

...이렇게 썼습니다만... 굉장히 깊게 들어가데요.  리스트에 append, extend, +, += 이렇게 크게 4개 있다면 어떤걸 써야하는지 어떻게 다른지요. 또 굉장히 흥미로운거는 이거 하나하나가 실행되는데 걸리는 시간 + 기계어 레벨인가(컴파일런가, bytecode인가 대부분 비슷하죠?)에 대해서도 살짝 알게되서 흥미진진하네요. 또한 dunder (__???__) 이것들하고 그냥 함수들의 차이라던지 같은거요.


일단 마지막에 궁금해서 해본거는..    a = 10, b = 6 일때    a-b  와  print(a-b)  와  함수로 불러왔을때를 해서 빼기를 했을때 이렇게 3가지 입니다. 사실 클래스에다가 연산자 오버로딩까지 해봐야 하는데.... 이건 안해봤습니다. 제 예상으로는 느릴거 같습니다. 원래내장 +, +=는 최적화가 된건데 덮어씌운거는 그게 안되있으니까요..(근데 생각해보니 똑같을거 같기도 하고...)


1. a,b에 10,6씩 할당하고 a-b 를 해보면

>>>dis.dis(compile("a,b = 10,6;  a-b", '', 'exec'))

 1           0 LOAD_CONST               3 ((10, 6))

             3 UNPACK_SEQUENCE    2

             6 STORE_NAME               0 (a)

             9 STORE_NAME               1 (b)

            12 LOAD_NAME                0 (a)

            15 LOAD_NAME                1 (b)

            18 BINARY_SUBTRACT

            19 POP_TOP

            20 LOAD_CONST               2 (None)

            23 RETURN_VALUE

a,b 에 10,6을 할당하고 a-b를 하는데 파이썬 코드가 C/C++에서 저런 컴파일 순서대로 되는거 같은데(기계어의 지식 전무)  

시간초로는

timeit.Timer('a-b', 'a,b = 10,6').timeit()

0.04296481181245326   (할때마다 다른데 0.04주변인거 같습니다)


2.  a,b에 10,6씩 할당하고 print(a-b) 를 해보면

>>>dis.dis(compile("a,b = 10,6;  print(a-b)", '', 'exec'))

 1           0 LOAD_CONST               3 ((10, 6))

             3 UNPACK_SEQUENCE       2

             6 STORE_NAME                0 (a)

             9 STORE_NAME                1 (b)

            12 LOAD_NAME                2 (print)

            15 LOAD_NAME                0 (a)

            18 LOAD_NAME                1 (b)

            21 BINARY_SUBTRACT

            22 CALL_FUNCTION            1 (1 positional, 0 keyword pair)

            25 POP_TOP

            26 LOAD_CONST                2 (None)

            29 RETURN_VALUE

이렇게 나옵니다. 여기서 추가된건  LOAD_NAME을 하고(print요) 밑에 CALL_FUNCTION(아마 print함수를 불렀겠죠?) 이란걸 또 한거 같습니다

timeit.Timer('print(a-b', 'a,b = 10,6').timeit()  ???? 하니까 계속 4 나오면서 무한 루프 도나보네요. 그전에 해놓은거 보려고 하는데 무한루프 때문에 그전꺼 지워져서..... 다시 찾기 귀찮네요 도큐먼트 보고 하는 삽질은 안하겠습니다 별로 안 중요하니 ㅎㅎ  하튼 뭐 몇초작 나오겠죠

3. a,b에 10,6씩 할당하고 def mysubtract(a,b): 라는 함수를 만드어서 그냥 print(a-b) 를 해보았습니다

dis.dis(compile("a,b = 10,6;  mysubtract(a,b)", '', 'exec'))

 1           0 LOAD_CONST               3 ((10, 6))

             3 UNPACK_SEQUENCE       2

             6 STORE_NAME                0 (a)

             9 STORE_NAME                1 (b)

            12 LOAD_NAME                2 (mysubtract)

            15 LOAD_NAME                0 (a)

            18 LOAD_NAME                1 (b)

            21 CALL_FUNCTION            2 (2 positional, 0 keyword pair)

            24 POP_TOP

            25 LOAD_CONST                2 (None)

            28 RETURN_VALUE

요번에는 CALL_FUNCTION 이 2번 됬네요??  아마도 mysubtract 함수를 부르고 print를 해서 그런것 같습니다. 이것도 시간초 재는건 복잡해 보입니다. name 'mysubtract' is not defined 요렇게 나오면서 아마도 timeit.Timer 이거를 할때 주어 진것들로만 하나보네요. 시간 걸리거 같으니 패스하고..


요번에 이것들 뒤지면서 저렇게 컴파일이 되는구나 하고 시간초도 보고 신기했습니다. 뭔가 둑흔둑흔 하네요.

그나저나 왜 이것까지 오게 됬냐면...  append, extend, +=, + 이거 차이였는데요 이걸 다 해봤습니다

timeit.Timer('s.append("something")', 's = []').timeit()    0.09    LOAD_ATTR /CALL_FUNCTION  

timeit.Timer('s += ["something"]', 's = []').timeit()         0.15    BUILD_LIST                                 

timeit.Timer('s.extend(["something"])', 's = []').timeit()   0.18    LOAD_ATTR /CALL_FUNCTION

timeit.Timer('s + ["something"]', 's = []').timeit()           0.19     BUILD_LIST                            

비슷한 것 끼리 먼저 해보면(새로운 값을 만들어서 할당이 아니라 외쪽값에다가 더하는 경우)

두개의 리스트(list1, list2) 라는 가정하에

+=는 __iadd__를 실행하고  extend()는 함수를 실행하는데

  

1. list1+= list2 같은경우  __iadd__가 실행되면서 왼쪽 처럼

  lits1.extend(list2) 같은경우 extend()함수를 써서 실행되면 오른쪽 처럼 되나봅니다


>>> dis.dis(compile("s=[];  s += ['something']" , '', 'exec'))

 1           0 BUILD_LIST                  0

             3 STORE_NAME               0 (s)

             6 LOAD_NAME                0 (s)

             9 LOAD_CONST               0 ('something')

            12 BUILD_LIST                  1

            15 INPLACE_ADD

            16 STORE_NAME               0 (s)

            19 LOAD_CONST               1 (None)

            22 RETURN_VALUE

>>> dis.dis(compile("s=[];  s.extend(['something'])" , '', 'exec'))

 1           0 BUILD_LIST                  0

             3 STORE_NAME               0 (s)

             6 LOAD_NAME                0 (s)

             9 LOAD_ATTR                 1 (extend)

            12 LOAD_CONST               0 ('something')

            15 BUILD_LIST                  1

            18 CALL_FUNCTION           1 (1 positional, 0 keyword pair)

            21 POP_TOP

            22 LOAD_CONST               1 (None)

            25 RETURN_VALUE


           

보면은 dunder(__iadd__)를 쓴경우는 BUILD_LIST를 써서 하고  함수 extend() 를 쓴거는 LOAD_ATTR 와 CALL_FUNCTION으로 하나 봅니다. 시간초는 BUILD_LIST(dunder인 __iadd__)를 쓴게 0.03초 정도 빨랐습니다. 여러글을 짜집기해본 결과 역시 특별한 함수(?) 메소드(?) 인 __???__는 파이썬단에서 특별히 만들어 놓은거라서 확실히 최적화가 되있는가 봅니다.


2. 리스트의 + 는 __add__가 실행되는데 += 보다 대충0.04 정도 느렸습니다.

>>> dis.dis(compile("s=[];  s + ['something']" , '', 'exec'))

 1           0 BUILD_LIST                  0

             3 STORE_NAME               0 (s)

             6 LOAD_NAME                0 (s)

             9 LOAD_CONST               0 ('something')

            12 BUILD_LIST                  1

            15 BINARY_ADD

            16 POP_TOP

            17 LOAD_CONST               1 (None)

            20 RETURN_VALUE

두개의 차이는 +=는 원래있떤 값을 바꿔주고 +는 원래값에 더한다음에 새로운 거에다가 할당 한다고 합니다. 차이로는 +=는 INPLACE_ADD 가 사용되고 +는 BINARY_ADD가 사용된점이 다릅니다 (뭐 POP_TOP하고 STORE_NAME도 다르지만요)  흠... POP_TOP은 아마 +해서 새로운거 만들었는데 재가 어떤 변수에다가 할당을 안해주니 바로 터트린(?)나 봅니다. 이런게 가비지 콜렉션 일까요...  그리고 STORE_NAME은 나온값을 원래 값에다가 재 할당 해주는거 같습니다.  

 실제로 id()해보면 list1+=list2는 원래 메모리 주소이고 list1 = list1 + list2  는   list1에다가 재할당을 해서 원래 lits1의 id()와 나중에 list1 + list2 해서 생긴 list1의 id()이 다릅니다. 재밌네요


3. append의 경우는 이렇게 되는데요.. 일단 가장 빠른가 봅니다?

dis.dis(compile("s=[];  s.append(['something'])" , '', 'exec'))

 1           0 BUILD_LIST               0

             3 STORE_NAME               0 (s)

             6 LOAD_NAME                0 (s)

             9 LOAD_ATTR                1 (append)

            12 LOAD_CONST               0 ('something')

            15 BUILD_LIST               1

            18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)

            21 POP_TOP

            22 LOAD_CONST               1 (None)

            25 RETURN_VALUE

extend와 다른점은 extend는 extend 부르고 이건 append를 부르는건데... append같은 경우는 그냥 다 때려밖는 거여서 빠른거 같습니다. list1.extend(list2) 는 왼쪽 처럼되고  list1.append(list2)는 오른쪽 처럼요. 아마 extend는 list끼리만 되고 append는 꼭 list가 아니여도 된다는걸 본 것같네요...(이건 확실 안합니다. 물론 제가 하는 얘기가 대부분...)   더 깊게가면 sequance인지 container인지 emulator인지 iterable 한지 뭔지 복잡한거 같습니다. 대충 제 생각으로는 append는 정말 뭐든지 따 때려밖는거고 extend는 iterate하면서 하나 하나 확인하면서 join하기 떄문에 더 느린거 같습니다.(아님 말구요...)


>>> list1 = [1,2,3]

>>> list1.extend([6,7,8])

>>> list1

[1, 2, 3, 6, 7, 8]

>>> list1 = [1,2,3]

>>> list1.append([7,8,9])

>>> list1

[1, 2, 3, [7, 8, 9]]

추가내용 

  아 그리고 또 하나 있는데 기억이 잘 안나네요. +=, +, extend 이 세가지 였나 좀 다릅니다. 전역변수 지역변수 그거 때문에 좀 다르더라고요. 전역변수를 지역변수로 만든다던지..  변수에 접근 못 한다던지  구글링 하면 나올겁니다.. 요것도 꽤 중요한 내용인데 제가 이거 쓸때 기억을 못해서 지금 이렇게 추가하네요

결론

후......... 굉장히 길어졌는데 결론 따윈 없습니다. ㅎㅎㅎ기계어를 처음봐서 뭔가 신기하네요. (이러다가 포인터와 동적할당, 가비지 콜렉터 같은것도 하게 되는건지.... 물론 궁금하네요 ㅎㅎ. 생각해보니 제 성격상 할 것 같습니다. 흠... 아마 10월말부터 다니는 유니티 국비 다닐떄 C++/C# 하니까 그때 하기로 하죠.)