처음 사용한 후 재 할당 할 때 로컬 변수의 UnboundLocalError


208

다음 코드는 Python 2.5 및 3.0에서 예상대로 작동합니다.

a, b, c = (1, 2, 3)

print(a, b, c)

def test():
    print(a)
    print(b)
    print(c)    # (A)
    #c+=1       # (B)
test()

그러나 (B) 줄의 주석을 해제 하면 UnboundLocalError: 'c' not assignedat (A) 줄이 나타납니다 . a및 의 값이 b올바르게 인쇄됩니다. 이것은 두 가지 이유로 완전히 당황했습니다.

  1. (B) 라인의 이후 명령문으로 인해 (A) 라인에서 런타임 오류가 발생하는 이유는 무엇 입니까?

  2. 왜 변수 ab동안 예상대로 인쇄는 c오류를 발생시킵니다?

내가 설명 할 수있는 유일한 설명 은 할당에 의해 지역 변수 c가 생성 된다는 것입니다. 이것은 지역 변수 가 생성 되기 전에 c+=1"전역"변수보다 우선합니다 c. 물론 변수가 존재하기 전에 범위를 "훔치는"것은 의미가 없습니다.

누군가이 행동을 설명해 주시겠습니까?

답변:


216

Python은 함수 내부 또는 외부에서 값을 할당하는지에 따라 함수의 변수를 다르게 처리합니다. 변수가 함수 내에 할당되면 기본적으로 로컬 변수로 처리됩니다. 따라서 줄의 주석을 해제하면 로컬 변수를 참조하려고합니다c 값이 할당되기 전에 합니다.

변수 cc = 3함수 앞에 할당 된 전역을 참조하도록하려면

global c

함수의 첫 번째 줄로.

파이썬 3은 지금 있습니다

nonlocal c

c변수 가있는 가장 가까운 둘러싸는 함수 범위를 참조하는 데 사용할 수 있습니다 .


3
감사. 빠른 질문. 이것은 파이썬이 프로그램을 실행하기 전에 각 변수의 범위를 결정한다는 것을 의미합니까? 기능을 실행하기 전에?
tba

7
변수 범위는 컴파일러에서 결정하며 일반적으로 프로그램을 처음 시작할 때 한 번 실행됩니다. 그러나 프로그램에 "eval"또는 "exec"문이 있으면 컴파일러가 나중에 실행될 수도 있습니다.
Greg Hewgill

2
좋아 감사합니다. 나는 "해석 된 언어"가 생각했던 것만 큼 의미가있는 것은 아니라고 생각합니다.
tba

1
아아 '비 로컬'키워드는 내가 찾던 것과 정확히 같았습니다. 파이썬에는 이것이 누락 된 것 같습니다. 아마도이 키워드를 사용하여 변수를 가져 오는 각 범위를 통해 '캐스케이드'합니까?
Brendan

6
@brainfsck : 변수 "찾아보기"와 "할당하기"를 구분하는 것이 가장 쉬운 방법입니다. 이름이 현재 범위에서 발견되지 않으면 조회가 더 높은 범위로 폴백합니다. 할당은 항상 로컬 범위에서 수행됩니다 ( 전역 또는 비 로컬 할당 을 사용 global하거나 nonlocal강요 하지 않는 한 )
Steven

71

파이썬은 모든 것을 다양한 범위의 사전에 보관한다는 점에서 조금 이상합니다. 원래의 a, b, c는 최상위 범위에 있으므로 최상위 사전에 있습니다. 함수에는 자체 사전이 있습니다. 당신이 도달 할 때 print(a)print(b) 문에 사전에 해당 이름의 이름이 없으므로 Python은 목록을 찾아 전역 사전에서 찾습니다.

이제 우리 c+=1는 물론에 해당합니다 c=c+1. 파이썬이 그 라인을 스캔 할 때, "aha, c라는 변수가 있는데, 이것을 내 로컬 스코프 사전에 넣겠습니다"라고 말합니다. 그런 다음 할당의 오른쪽에서 c에 대한 c에 대한 값을 찾으면 아직 값이없는 c라는 로컬 변수를 찾아서 오류를 발생시킵니다.

global c위에서 언급 한 내용 은 파서에게 c전역 범위 의 from을 사용 하므로 새로운 구문이 필요하지 않다는 것을 파서에게 알려줍니다 .

그것이 라인에 문제가 있다고 말하는 이유는 코드를 생성하기 전에 효과적으로 이름을 찾고 있기 때문에 어떤 의미에서는 아직 그 라인을 실제로하고 있다고 생각하지 않기 때문입니다. 나는 이것이 유용성 버그라고 주장하지만 일반적으로 컴파일러의 메시지를 너무 심각하게 받아들이지 않는 법을 배우는 것이 좋습니다 .

그것이 위안이라면, Guido가 모든 것을 설명하는 사전에 대해 쓴 것을 발견하기 전에이 같은 문제를 파고 실험하는 데 하루를 보냈습니다.

업데이트, 의견 참조 :

코드를 두 번 스캔하지는 않지만 lexing과 parsing의 두 단계로 코드를 스캔합니다.

이 코드 줄의 구문 분석이 어떻게 작동하는지 고려하십시오. 어휘 분석기는 소스 텍스트를 읽고 문법의 "가장 작은 구성 요소"인 렉 세스로 나눕니다. 그래서 라인에 도달하면

c+=1

그것은 그것을 다음과 같이 나눕니다.

SYMBOL(c) OPERATOR(+=) DIGIT(1)

파서는 결국 이것을 구문 분석 트리로 만들고 실행하려고하지만 할당이기 때문에 로컬 사전에서 c라는 이름을 찾고 그것을 보지 않고 사전에 삽입하여 표시합니다. 초기화되지 않은 것으로. 완전히 컴파일 된 언어에서는 기호 테이블로 이동하여 구문 분석을 기다릴 수 있지만 두 번째 패스의 사치가 없기 때문에 렉서 (Lexer)는 나중에 인생을 더 쉽게하기 위해 약간의 추가 작업을 수행합니다. 그런 다음 운영자 만 볼 수 있고 규칙에 "운영자 + =가있는 경우 왼쪽이 초기화되어야합니다"라고 말하고 "누가!"라고 말합니다.

여기서 요점은 아직 라인의 구문 분석을 시작하지 않았다는 것입니다. 입니다. 이것은 실제 구문 분석에 대한 모든 준비 과정이므로 행 카운터가 다음 행으로 진행되지 않았습니다. 따라서 오류를 알리더라도 여전히 이전 라인에서 오류를 생각합니다.

내가 말했듯이, 그것은 유용성 버그라고 주장 할 수 있지만 실제로는 매우 일반적인 것입니다. 일부 컴파일러는 그것에 대해 더 정직하고 "XXX 라인 또는 그 주변에 오류"라고 말하지만, 그렇지 않습니다.


1
응 답 해줘서 고마워 파이썬에서 범위에 대해 몇 가지 사항을 정리했습니다. 그러나 여전히 오류가 왜 줄 (B)가 아닌 줄 (A)에서 발생하는지 이해하지 못합니다. 파이썬은 프로그램을 실행하기 전에 변수 범위 사전을 작성합니까?
tba

1
아니요, 표현 수준에 있습니다. 나는 대답에 추가 할 것이고, 나는 이것을 주석에 넣을 수 있다고 생각하지 않는다.
Charlie Martin

2
구현 세부 사항에 대한 참고 사항 :에 CPython과는 로컬 범위가 보통으로 처리되지 않고 dict, 내부적 단지 배열의 ( locals()을 채 웁니다 dict반환에 있지만, 변경은 새로운 작성하지 않습니다 locals). 구문 분석 단계는 각 할당을 로컬로 찾아서 해당 배열의 이름에서 위치로 변환하고 이름을 참조 할 때마다 해당 위치를 사용합니다. 함수에 진입하면 인수가 아닌 로컬이 자리 표시 자로 초기화되고 UnboundLocalError변수를 읽고 관련 인덱스에 자리 표시자가있을 때 발생합니다.
ShadowRanger

44

분해를 살펴보면 무슨 일이 일어나고 있는지 알 수 있습니다.

>>> def f():
...    print a
...    print b
...    a = 1

>>> import dis
>>> dis.dis(f)

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_GLOBAL              0 (b)
              8 PRINT_ITEM
              9 PRINT_NEWLINE

  4          10 LOAD_CONST               1 (1)
             13 STORE_FAST               0 (a)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE

당신이 볼 수있는 바와 같이, 액세스하기위한 바이트 코드는 LOAD_FAST,와 b에 대한 LOAD_GLOBAL. 컴파일러가 함수 내에서 a가 할당되었음을 식별하고이를 로컬 변수로 분류했기 때문입니다. 지역에 대한 액세스 메커니즘은 전역에 대해 근본적으로 다릅니다. 프레임의 변수 테이블에 오프셋이 정적으로 할당됩니다. 즉, 전역에 비해 값 비싼 딕트 조회가 아니라 조회가 빠른 색인임을 의미합니다. 이 때문에 Python은 print a"슬롯 0에 보유 된 로컬 변수 'a'의 값을 가져 와서 인쇄"로 해당 행을 읽 습니다.이 변수가 아직 초기화되지 않았 음을 감지하면 예외가 발생합니다.


10

파이썬은 전통적인 전역 변수 의미를 시도 할 때 다소 흥미로운 동작을합니다. 세부 사항은 기억 나지 않지만 '전역'범위에서 선언 된 변수의 값을 잘 읽을 수는 있지만 수정하려면 global키워드 를 사용해야합니다 . test()이것으로 변경 하십시오 :

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)

또한이 오류가 발생하는 이유는 해당 함수 내에서 '전역'과 동일한 이름으로 새 변수를 선언 할 수 있기 때문에 완전히 분리되어 있기 때문입니다. 인터프리터는이 범위에서 새로운 변수를 호출 c하고 한 번의 조작으로 변수를 모두 수정 하려고한다고 생각합니다.이 새로운 변수는 c초기화되지 않았기 때문에 파이썬에서는 허용되지 않습니다 .


귀하의 답변에 감사드립니다.하지만 왜 변수를 인쇄하려고하는 (A) 줄에 오류가 발생했는지 설명하지 않습니다. 프로그램은 초기화되지 않은 변수를 수정하려고하는 행 (B)에 도달하지 않습니다.
tba

1
파이썬은 프로그램 실행을 시작하기 전에 전체 함수를 읽고 해석하고 내부 바이트 코드로 변환하므로 "c를 로컬 변수로 바꾼다"는 값을 인쇄 한 후에 텍스트로 발생한다는 사실은 중요하지 않습니다.
Vatine

6

가장 분명한 예는 다음과 같습니다.

bar = 42
def foo():
    print bar
    if False:
        bar = 0

호출 할 때 foo(),이 또한 제기 UnboundLocalError 우리가 라인에 도달하지 않습니다 있지만,bar=0 그렇게 논리적으로 지역 변수가 생성해서는 안됩니다.

수수께끼는 " Python is Interpreted Language "에 있으며 함수 선언은 foo단일 명령문 (예 : 복합 명령문)으로 해석되며,이를 어리석게 해석하고 로컬 및 글로벌 범위를 만듭니다. 그래서bar 실행 전에 로컬 범위에서 인식됩니다.

에 대한 더 많은 예제 이 읽기이 게시물 같은 : http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

이 게시물은 변수의 파이썬 범위 지정에 대한 완전한 설명 및 분석을 제공합니다.


5

도움이 될만한 두 가지 링크는 다음과 같습니다.

1 : docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-w-the-variable-has-a-value

2 : docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

링크 1은 오류 UnboundLocalError를 설명합니다. 링크 2는 테스트 기능을 다시 작성하는 데 도움이 될 수 있습니다. 링크 2를 기반으로 원래 문제를 다음과 같이 다시 작성할 수 있습니다.

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)

4

이것은 귀하의 질문에 대한 직접적인 대답은 아니지만, 확장 할당과 기능 범위 사이의 관계로 인해 발생하는 또 다른 문제이므로 밀접한 관련이 있습니다.

대부분의 경우 증강 된 과제 ( a += b)는 단순한 과제 (a = a + b ) 있습니다. 그러나 한 모퉁이의 경우이 문제에 어려움을 겪을 수 있습니다. 설명하겠습니다 :

파이썬의 간단한 할당이 작동하는 방식은 a함수에 전달 되면 ( func(a)파이썬은 항상 참조 로 전달됨에 유의하십시오) 전달 된 것을 a = a + b수정하지 않습니다 a. 대신에 로컬 포인터를 수정합니다.a .

그러나를 사용하면 a += b때로는 다음과 같이 구현됩니다.

a = a + b

또는 때로는 (방법이 존재하는 경우) 다음과 같이 :

a.__iadd__(b)

첫 번째 경우 ( a글로벌로 선언되지 않은 한)에 대한 할당 a은 포인터 업데이트 일 뿐이 므로 로컬 범위 외부의 부작용은 없습니다 .

두 번째 경우 a실제로 실제로 수정되므로 모든 참조 a는 수정 된 버전을 가리 킵니다. 이것은 다음 코드로 설명됩니다.

def copy_on_write(a):
      a = a + a
def inplace_add(a):
      a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

따라서 트릭은 함수 인수에 대한 할당을 늘리지 않는 것입니다 (로컬 / 루프 변수에만 사용하려고합니다). 간단한 과제를 사용하면 모호한 행동으로부터 안전합니다.


2

파이썬 인터프리터는 함수를 완전한 단위로 읽습니다. 클로저 (로컬 변수)를 수집 한 다음 다시 바이트 코드로 변환하기 위해 두 번의 패스로 읽는 것으로 생각합니다.

이미 알고 계시 겠지만 '='왼쪽에 사용 된 이름은 암시 적으로 로컬 변수입니다. 변수 액세스를 + =로 변경하여 한 번 이상 잡히면 갑자기 다른 변수입니다.

또한 실제로 글로벌 범위와 관련이 없다는 것을 지적하고 싶었습니다. 중첩 함수와 동일한 동작을 얻습니다.


2

c+=1 할당하다 c , python은 할당 된 변수가 로컬 인 것으로 가정하지만이 경우 로컬로 선언되지 않았습니다.

global또는 nonlocal키워드를 사용하십시오 .

nonlocal 파이썬 3에서만 작동하므로 파이썬 2를 사용하고 변수를 전역으로 만들지 않으려면 가변 객체를 사용할 수 있습니다.

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()

1

클래스 변수에 도달하는 가장 좋은 방법은 클래스 이름으로 직접 액세스하는 것입니다

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1

0

파이썬에서는 모든 유형의 변수 로컬, 클래스 변수 및 전역 변수에 대해 비슷한 선언이 있습니다. 메소드에서 전역 변수를 참조 할 때 파이썬은 실제로 정의되지 않은 메소드 자체에서 변수를 참조한다고 생각하므로 오류가 발생합니다. 전역 변수를 참조하려면 globals () [ 'variableName']을 사용해야합니다.

귀하의 경우에는 a, b 및 c 대신 globals () [ 'a], globals () ['b '] 및 globals () ['c ']를 각각 사용하십시오.


0

같은 문제가 나를 귀찮게합니다. 사용 nonlocal하고 global문제를 해결할 수 있습니다.
그러나의 사용에주의를 기울 nonlocal이면 중첩 함수에 적용됩니다. 그러나 모듈 수준에서는 작동하지 않습니다. 여기 예를 참조 하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.