8.1 Python의 스코프 규칙 이해 부족
Python의 스코프는 LEGB 규칙이라 불리는 것에 기반을 두고 있어:
-
Local
(함수 (def
또는lambda
) 내에서 어느 방법으로든 할당된 이름이며, 그 함수에서 전역적으로 선언되지 않은 것); Enclosing
(내포된 함수(def
또는lambda
)의 로컬 스코프 내의 이름, 안쪽에서 바깥쪽으로);Global
(모듈 파일의 최상위 또는def
내에서global
문을 실행하여 할당된 이름);-
Built-in
(내장 이름 모듈에 사전 할당된 이름:open
,range
,SyntaxError
, 등).
꽤 간단해보이지, 그치?
하지만 Python에서 이것이 어떻게 작동하는지에 대해 몇 가지 미묘한 점이 있어서, 이는 Python 프로그래밍의 복잡한 문제로 이어져. 다음 예제를 살펴보자:
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
문제가 뭐야?
위 오류는 여러분이 스코프 내에서 변수에 값을 할당할 때
, Python이
그것을 자동으로 로컬로 간주하기 때문이야
그리고 어떤 상위 스코프에서도 동일한 이름의 변수를 숨겨버려.
그래서 대다수는 UnboundLocalError
를 이전에 작동하던 코드에서 받게 되면 함수 본문 어딘가에 할당 연산자를 추가함으로써 수정한 후 놀라게 돼.
이 기능은 특히 리스트를 사용할 때 개발자들을 혼란시켜. 다음 예제를 살펴봐:
lst = [1, 2, 3]
def foo1():
lst.append(5) # 이건 잘 작동해...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... 그런데 이건 안돼!
foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
왜 foo2
는 실패하고 foo1
은 잘 작동할까?
이전 예제와 마찬가지로, 그러나 더 미묘하게도, foo1
은 lst
에 할당 연산자를 사용하지 않고, foo2
는 그렇지. lst += [5]
는 사실 lst = lst + [5]
의 축약형이라는 것을 기억하면, 우리는 lst
에 값을 할당하려고 하고 있다는 것을 알 수 있어 (따라서 Python은 그것이 로컬 스코프에 있다고 가정해). 하지만 우리가 lst
에 할당하고자 하는 값은 lst
자체에 기반하고 있어 (다시 말해, 이제 그것이 로컬 스코프에 있다고 가정해), 아직 정의되지 않았어. 그리고 우리는 오류를 얻게 돼.
8.2 리스트를 반복하는 동안 변경하기
다음 코드 조각에서 문제는 꽤 명백할 거야:
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: 반복 중 리스트에서 항목 삭제
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
리스트 또는 배열을 반복하는 동안 요소를 삭제하는 것은 경험 많은 소프트웨어 개발자에게 잘 알려진 Python 문제야. 하지만 위의 예제가 충분히 명백할 수는 있어도, 심지어 경험 많은 개발자들조차 더 복잡한 코드에서 이 문제와 마주할 수 있어.
다행히도, Python은 올바르게 사용될 때 코드의 상당한 단순화와 최적화를 가져오는 우아한 프로그래밍 패러다임을 포함하고 있어. 이로 인해 더 단순한 코드에서는 리스트 요소를 반복 중 실수로 삭제할 가능성이 상당히 줄어들어.
이러한 패러다임 중 하나는 리스트 내포야. 또한, 리스트 내포의 작동 원리를 이해하는 것은 이 특정 문제를 피하는 데 특히 유용해, 아래의 대체 구현은 잘 작동하는 것처럼 보여:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # 그냥 새로운 요소를 선택하는 거야
print(numbers)
# [0, 2, 4, 6, 8]
중요해!
여기서는 새로운 리스트 객체가 할당되지 않아. numbers[:]
사용은 리스트의 모든 요소에 새 값을 동시에 할당하는 거야.
8.3 Python이 클로저에서 변수를 바인딩하는 방법 이해 부족
다음 예제를 살펴봐:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #함수 리스트를 반환해!
for multiplier in create_multipliers():
print(multiplier(2))
다음과 같은 출력이 기대될 거야:
0
2
4
6
8
하지만 실제로는 이렇게 나올 거야:
8
8
8
8
8
놀랐지!
이는 Python에서의 지연 바인딩 때문이야, 이는 클로저에서 사용된 변수의 값이 내부 함수가 호출될 때 조회된다는 것을 의미해.
따라서 위 코드에서는 반환된 함수가 호출될 때마다 i
의 값이 그 시점의 주변 스코프에서 조회되어 (이 시점에서는 루프가 이미 끝났기 때문에 i
는 최종 결과로서 4가 할당된 상태이기 때문에) 항상 같은 결과가 나오게 돼.
이와 같은 Python의 일반적인 문제를 해결하려면 다음과 같이 해야 해:
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
짠! 여기서는 익명의 함수 생성을 위해 기본 인수를 사용하여 원하는 동작을 얻었어. 어떤 사람들은 이것을 우아하다고 부르지. 어떤 사람들은 미묘하다고 불러. 어떤 사람들은 이런 걸 싫어해. 하지만 Python 개발자라면 어쨌든 이걸 이해해야 해.
GO TO FULL VERSION