관리 메뉴

HAMA 블로그

파이썬에서 가장 쉽게 범할 수 있는 10가지 실수들 [번역] 본문

Python

파이썬에서 가장 쉽게 범할 수 있는 10가지 실수들 [번역]

[하마] 이승현 (wowlsh93@gmail.com) 2017. 6. 15. 18:47



파이썬에서 가장 쉽게 범할 수 있는 10가지 실수들 [번역]

https://www.toptal.com/python/top-10-mistakes-that-python-programmers-make
(번역중 제가 간략하게 의역한 부분도 있고, 10가지중 앞의  '6가지 실수' 를 번역되었습니다.) 


실수 #1: 디폴트 함수인자에 대한 실수 

파이썬은 함수인자로 디폴트 값을 사용 할 수 있게 해주는데 , 꽤나 괜찮은 문법이지. 다만 그 디폴트값이 mutable 일 경우 혼동을 주기도 하는데 다음 예를 보자고.

>>> def foo(bar=[]):       # bar 는 디폴트로 [] 를 갖는데 아직 구체화 되지 않았어.
...        bar.append("baz")    # 그 경우 이 라인은 좀 문제가 될 수 있지..
...        return bar

일반적인 실수는 옵셔널 인자가 구체적인 디폴트 식으로 세팅 될 것이라고 생각하는 것인데, 함수는 사실 구체적 매개변수가 제공되지 않은 상태에서 호출 될 수 있거든.

위의 코드에서 누군가 매개변수 없이 foo() 를 반복적으로 호출하면 항상 "baz" 를 리턴 한다고 생각하는게 당연한데, (즉 항상 새로운 빈 리스트가 할당될 것이라고 생각할테지? ) 그건 적어도 파이썬에선 착각이야. 

자 어떤 일이 일어나는지 확인해 보자고. 

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

어때?  골때리지? 

그냥 이전의 "baz" 요소를 계속 지닌채로 작동하잖아? 함수를 호출 할 때마다 쌓이고~
이유는 말이지. 함수 매개변수에 대한 디폴트 값은 함수가 정의되는 시점에 오직 한번만 평가가 되어져. 그래서 bar 매개변수는 foo() 함수가 처음 정의 될 때 
디폴트로 초기화 되지. 그리고 그 뒤로 foo() 함수가 호출 되면 (bar 매개변수가 구체화 되지 않은 채) 계속해서 동일한 리스트를 사용 하게 되는거야. 

따라서 보통 다음처럼 구현하는게 좋아.
(역주: 파이썬의 이디엄이라 볼 수 있을거 같다. 디자인 패턴이 객체지향 언어 모두의 공통된 습관이라고 보면, 이디엄은 특정 언어에 해당하는 좀 좁은 의미의 습관으로 볼 수 있다. 예를들어 C++ 의 스마트 포인터 같은것이 이디엄이다.) 

>>> def foo(bar=None):
...       if bar is None:		
...          bar = []             # 호출 될 때마다 새로 만든다.
...        bar.append("baz")
...        return bar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]

실수 #2: 클래스 변수 사용에 대한 실수 

다음 예를 살펴보자

>>> class A(object):
...       x = 1
...
>>> class B(A):
...       pass
...
>>> class C(A):
...       pass
...
>>> print A.x, B.x, C.x
1 1 1

다음 처럼 작동하길 기대하겠지

>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

좋아~!!  근데 아래는 ~??? 

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

왓더~??  난 오직 A.x 만 바꾸었는데 왜 C.x 도 바뀐거야? 아니면 부모가 바뀌었으니 그냥 다 바뀌거나~

그건 말이쥐~ 파이썬에서는 말야 클래스 변수는 내부적으로 딕셔너리이며 Method Resolution Order (MRO)로 작동한다고 알려져있지.  예제를 보면 먼저 B.x 는 B.x 에 값을 할당하는 순간부터는 A.x 와는 다른 독립적인 속성이되. 그래서 A.x =3 이 대입되어도 영향을 안받은거고,  C 의 경우는 일단 C 의 클래스에서 x 가 발견되지 않으면, 부모로 올라가서 찾아보게 되는데, 그 곳에서 찾으면 일단 C.x 값은 A.x 의 값을 참조하게되. 결국 이 상태에서 A.x 에 무엇인가 대입을 하게 되면  C.x 도 따라서 바뀌게 되는거지. 물론 C.x 도 안바뀌게 하려면 B클래스처럼 스스로 대입받으면 되는거야. 

더 자세한것은 요기를 참고하도록:  class attributes in Python.

실수#3: 예외 블럭에서 파라미터 리스트에 대한 실수 

다음과 같은 코드가 있다고 하자.

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except ValueError, IndexError:  # 둘 중에 하나의 예외를 잡는다? 
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
IndexError: list index out of range

여기서 문제는except구문은 특정 예외의 리스트를 가질 수 없다는것이다. 파이썬 2.x 에서 except Exception, e구문은 옵셔널 파라미터로 예외에 바인드 되어서 예외가 가진 속성을 조사하기 위해서 사용 된다. 결과적으로 위의 코드는IndexError예외는except구문에서 잡히지 않는다.

여러가지 예외를 하나의 except 구문에서 사용하기 위한 방법은 예외들을 포함한  tuple 로써 구성하는것인데 as 키워드를 사용할 수 있으며, (예외에 대한 구체적인 조사가 필요 없다면 as e 는 할 필요 없다)  python 2, 3에서 지원된다. 코드는 다음과 같다.

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except (ValueError, IndexError) as e:  
...     pass
...
>>>

실수 #4: 파이썬 스코프 룰에 대한 오해 

파이썬 스코프 범위는 LEGB 룰에 준한다. 간략히 말해서 Local, Enclosing, Global, Built-in. 의 약자이며, 그대로 이해하면 될 것이다. 하지만 약간의 미묘한 부분이 있는데 아래를 살펴보자.

>>> x = 10
>>> def foo():
...       x += 1
...       print x
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

무슨 문제일까?

The 위에 발생된 문제는 변수에 값을 할당하려다 생긴 문제인데, 저것은 x = x+1 아닌가? 여기서 x+1 을 먼저하게되고 이때 x 를 찾지 못한다는 얘기다. 우리가 보통 생각 할때는 로컬에 없으면 글로벌로 자동적으로 찾아가지 않을까 생각하는데 파이썬에서는 그렇지 않다.

특히 이문제는 리스트를 사용함에 있어서 종종 나타난다. 다음 코드를 보자.

>>> lst = [1, 2, 3]
>>> def foo1():
...       lst.append(5)   # 좋습니다.
...
>>> foo1()
>>> lst
[1, 2, 3, 5]

>>> lst = [1, 2, 3]
>>> def foo2():
...       lst += [5]      # 문제가 생겼네요 ;;
...
>>> foo2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

엥? 왜foo2 는 문제가 생길까?foo1 는 괜찮아 보이는데 말이지

대답은foo1에서는lst를 할당(assignment) 하지 않는다. 반면foo2하고 있다. 알다시피lst += [5] 는 lst = lst + [5] 의 약자이다. 즉lst 에 값을 assign 하고 있다. 그래서 아하~ 로컬 스코프에서 찾아야하는구나~~ 라고 행동하지만 로컬에는 아직 정의되지 않았기 때문에 "펑~사망하였습니다" 라는 외마디 비명소리만..

참고로 global 로 지정해주면 해결 할 수 있으며, 글로벌에서 찾으라고 지침해준다.

lst = [1,2,3]
def foo2():
global lst
lst += [5]
print lst # [1, 2, 3, 5]


실수#5: 리스트를 반복(iterating)하다 수정하면서 생기는 문제 

아래코드에서 생기는 문제는 꽤나 명백해 보입니다.

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i in range(len(numbers)):
...     if odd(numbers[i]):
...         del numbers[i]  # BAD: Deleting item from a list while iterating over it
...
Traceback (most recent call last):
  	  File "<stdin>", line 2, in <module>
IndexError: list index out of range

순회하던 중간에 지워 버렸으니, 리스트 길이는 줄어들었는데 순회는 갯수는 바꾸지 않았기 때문에 IndexError 가 생겨버렸습니다. 사실 이런문제는 다양한 언어에서도 마찬가지로 발생되죠. 언어별로 조심해야 할 부분입니다.

운좋게도 파이썬에서는 좀 더 우아한 처리 방식이 있는데요.  list comprehensions. 라고 합니다.동일한 일을 하는 코드를 그것으로 만들어보면 아래와 같습니다.

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)]  # ahh, the beauty of it all
>>> numbers
[0, 2, 4, 6, 8]


실수#6:  클로저안에 변수들을 바인딩하는 동작에 관한 문제 

다음 예제를 보시죠.

>>> def create_multipliers():
...     return [lambda x : i * x for i in range(5)]  # 빨강색 람다함수 5개가 생깁니다. 
>>> for multiplier in create_multipliers(): # 람다함수 5개중 하나씩 토해냄. 
...     print multiplier(2) # 토해진 람다함수의 i 값은 0,1,2,3,4 가 아니라 모두 4가 됩니다.
...

아래와 같기를 기대 할 테지만

0
2
4
6
8

사실 다음과 같습니다.

8
8
8
8
8

깜놀했죠?

이것은 파이썬의 늦은 바인딩 때문입니다. 즉 함수 호출 할 때 클로저에서의 변수 값들은 안쪽 함수가 호출 될 때 찾아 진다는 것입니다. 그래서 위의코드는 리턴된 함수가 호출 될 때마다, i 의 값이 해당 시간에 둘러쌓인 스코드 내에서 찾아지는데 이미 그 값은 (가장 마지막 값인 ) 4로 할당되어 있지요.

다음처럼 꼬아서 해결 할 수 있습니다.

>>> def create_multipliers():
...     return [lambda x, i=i : i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
...     print multiplier(2)
...
0
2
4
6
8

해결은 됬지만, 좀 꼬아놓은 듯한 모양새 때문에 찬반이 갈립니다. 쿨하다~~ 가독성이 떨어져서 지저분하다, 굳이 저렇게 까지? 


Comments