버그가 있는 Python 코드: Python 개발자가 저지르는 가장 일반적인 실수 10가지
게시 됨: 2022-03-11파이썬에 대하여
Python은 동적 의미를 사용하는 해석된 객체 지향 고급 프로그래밍 언어입니다. 동적 타이핑 및 동적 바인딩과 결합된 높은 수준의 내장 데이터 구조는 Rapid Application Development 뿐만 아니라 기존 구성 요소 또는 서비스를 연결하기 위한 스크립팅 또는 글루 언어로 사용하기에 매우 매력적입니다. Python은 모듈과 패키지를 지원하므로 프로그램 모듈성과 코드 재사용을 장려합니다.
이 기사에 대해
Python의 간단하고 배우기 쉬운 구문은 Python 개발자, 특히 언어를 처음 접하는 개발자로 하여금 Python의 미묘한 부분을 놓치고 다양한 Python 언어의 힘을 과소평가하도록 오도할 수 있습니다.
이를 염두에 두고 이 기사는 뒤에서 더 고급 Python 개발자를 물릴 수 있는 다소 미묘하고 포착하기 어려운 실수의 "상위 10" 목록을 제공합니다.
(참고: 이 기사는 Python 프로그래머의 일반적인 실수보다 고급 독자를 대상으로 하며 언어를 처음 접하는 사람들을 대상으로 합니다.)
일반적인 실수 #1: 표현식을 함수 인수의 기본값으로 잘못 사용
Python을 사용하면 기본값 을 제공하여 함수 인수가 선택 사항 임을 지정할 수 있습니다. 이것은 언어의 훌륭한 기능이지만 기본값이 mutable 이면 혼동을 일으킬 수 있습니다. 예를 들어 다음 Python 함수 정의를 고려하십시오.
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
일반적인 실수는 선택적 인수에 대한 값을 제공하지 않고 함수가 호출될 때마다 선택적 인수가 지정된 기본 표현식으로 설정될 것이라고 생각하는 것입니다. 예를 들어 위의 코드에서 foo()
를 반복적으로 호출하면(즉, bar
인수를 지정하지 않고) 항상 'baz'
가 반환될 것이라고 예상할 수 있습니다. 왜냐하면 foo()
가 호출될 때 마다 ( bar
없이) 지정된 인수) bar
는 []
로 설정됩니다(즉, 새로운 빈 목록).
하지만 이렇게 하면 실제로 어떻게 되는지 살펴보겠습니다.
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
뭐? 매번 새 목록을 만드는 대신 foo()
가 호출될 때마다 기존 목록에 기본값 "baz"
를 계속 추가하는 이유는 무엇입니까?
더 발전된 Python 프로그래밍 대답은 함수 인수의 기본값이 함수가 정의될 때 한 번만 평가된다는 것입니다. 따라서 bar
인수는 foo()
가 처음 정의된 경우에만 기본값(즉, 빈 목록)으로 초기화되지만 foo()
에 대한 호출(즉, bar
인수가 지정되지 않은 경우)은 동일한 목록을 계속 사용하여 다음을 수행합니다. 원래 초기화된 bar
입니다.
참고로 이에 대한 일반적인 해결 방법은 다음과 같습니다.
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... 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 Ax, Bx, Cx 1 1 1
말이된다.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
네, 역시나 예상대로입니다.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
$%#!& ?? Ax
만 변경했습니다. Cx
도 변경된 이유는 무엇입니까?
Python에서 클래스 변수는 내부적으로 사전으로 처리되며 흔히 MRO(Method Resolution Order)라고 하는 것을 따릅니다. 따라서 위의 코드에서 속성 x
는 클래스 C
에서 찾을 수 없기 때문에 기본 클래스에서 조회됩니다(위의 예에서는 A
만, Python은 다중 상속을 지원함). 즉, C
에는 A
와 독립적인 자체 x
속성이 없습니다. 따라서 Cx
에 대한 참조는 실제로 Ax
에 대한 참조입니다. 제대로 처리되지 않으면 Python 문제가 발생합니다. Python의 클래스 속성에 대해 자세히 알아보세요.
일반적인 실수 #3: 예외 블록에 대해 매개변수를 잘못 지정
다음 코드가 있다고 가정합니다.
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
여기서 문제는 except
문이 이러한 방식으로 지정된 예외 목록을 사용하지 않는다는 것입니다. 오히려 Python 2.x에서 except Exception, e
구문은 추가 검사에 사용할 수 있도록 지정된 선택적 두 번째 매개변수(이 경우 e
)에 예외를 바인딩하는 데 사용됩니다. 결과적으로 위의 코드에서 IndexError
예외는 except
문에 의해 catch 되지 않습니다 . 오히려 예외는 IndexError
라는 매개변수에 바인딩됩니다.
except
문에서 여러 예외를 포착하는 적절한 방법은 첫 번째 매개변수를 포착할 모든 예외를 포함하는 튜플로 지정하는 것입니다. 또한 최대 이식성을 위해 as
키워드를 사용하십시오. 해당 구문은 Python 2와 Python 3에서 모두 지원되기 때문입니다.
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
일반적인 실수 #4: Python 범위 규칙에 대한 오해
Python 범위 확인은 L ocal, E nclosing, G lobal, B uilt-in의 약어인 LEGB 규칙을 기반으로 합니다. 충분히 간단해 보이죠? 사실, 파이썬에서 이것이 작동하는 방식에는 약간의 미묘함이 있습니다. 이는 우리를 아래의 일반적인 고급 파이썬 프로그래밍 문제로 안내합니다. 다음을 고려하세요:
>>> 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) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> 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
뭐? foo1
이 잘 실행되는 동안 foo2
가 폭탄을 터뜨린 이유는 무엇입니까?
대답은 이전 예제 문제와 동일하지만 확실히 더 미묘합니다. foo1
은 lst
에 할당 하지 않는 반면 foo2
는 할당합니다. lst += [5]
가 실제로는 lst = lst + [5]
의 줄임말임을 기억하면 lst
에 값을 할당 하려고 한다는 것을 알 수 있습니다(따라서 Python은 로컬 범위에 있다고 가정함). 그러나 lst
에 할당하려는 값은 아직 정의되지 않은 lst
자체(이제 다시 로컬 범위에 있는 것으로 가정됨)를 기반으로 합니다. 팔.
일반적인 실수 #5: 목록을 반복하면서 목록 수정하기
다음 코드의 문제는 상당히 명확해야 합니다.
>>> 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
반복하는 동안 목록이나 배열에서 항목을 삭제하는 것은 숙련된 소프트웨어 개발자에게 잘 알려진 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)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
일반적인 실수 #6: 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 개발자라면 어떤 경우에도 이해하는 것이 중요합니다.
일반적인 실수 #7: 순환 모듈 종속성 생성
다음과 b.py
a.py
이 있다고 가정해 보겠습니다.
a.py
에서 :
import b def f(): return bx print f()
그리고 b.py
에서 :
import a x = 1 def g(): print af()
먼저 a.py
가져오기를 시도해 보겠습니다.
>>> import a 1
잘 작동했습니다. 아마도 그것은 당신을 놀라게 할 것입니다. 결국, 여기에서 아마도 문제가 되어야 하는 순환 가져오기가 있습니다. 그렇지 않습니까?
대답은 단순히 순환 가져오기의 존재 자체가 Python에서 문제가 아니라는 것입니다. 모듈을 이미 가져온 경우 Python은 모듈을 다시 가져오지 않을 만큼 충분히 똑똑합니다. 그러나 각 모듈이 다른 모듈에 정의된 함수나 변수에 액세스하려고 시도하는 지점에 따라 실제로 문제가 발생할 수 있습니다.
따라서 예제로 돌아가서 b.py
를 가져올 때 a.py
를 가져오는 b.py
문제가 a.py
습니다 . b.py
에서 a
에 대한 유일한 참조는 af()
에 대한 호출입니다. 그러나 그 호출은 g()
에 있고 a.py
또는 b.py
의 어떤 것도 g g()
) 를 호출하지 않습니다. 그래서 인생은 좋은 것입니다.
그러나 b.py
a.py
가져오지 않은 상태에서) 어떻게 됩니까?
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
어 오. 그 좋지 않다! 여기서 문제는 b.py 를 가져오는 과정에서 b.py
를 가져 a.py
시도하고, 이는 bx
액세스를 시도하는 f()
를 호출한다는 것입니다. 그러나 bx
는 아직 정의되지 않았습니다. 따라서 AttributeError
예외가 발생합니다.
이것에 대한 적어도 하나의 솔루션은 아주 간단합니다. b.py
를 수정하여 g()
내에서 .py를 가져 a.py
하면 됩니다.
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
아니요, 가져올 때 모든 것이 정상입니다.
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
일반적인 실수 #8: Python 표준 라이브러리 모듈과 이름 충돌
Python의 아름다움 중 하나는 "즉시 사용 가능한" 풍부한 라이브러리 모듈입니다. 그러나 결과적으로 의식적으로 피하지 않는다면 모듈 중 하나의 이름과 Python과 함께 제공되는 표준 라이브러리에 있는 동일한 이름을 가진 모듈 사이에 이름 충돌이 발생하는 것은 그리 어렵지 않습니다(예: , 코드에 email.py
라는 모듈이 있을 수 있으며, 이는 동일한 이름의 표준 라이브러리 모듈과 충돌할 수 있습니다.
이것은 모듈의 Python 표준 라이브러리 버전을 차례로 가져오려고 시도하지만 동일한 이름을 가진 모듈이 있기 때문에 다른 패키지가 실수로 내부에 있는 버전 대신 사용자의 버전을 가져오는 것과 같은 복잡한 문제로 이어질 수 있습니다. 파이썬 표준 라이브러리. 여기에서 잘못된 Python 오류가 발생합니다.
따라서 Python 표준 라이브러리 모듈에 있는 것과 동일한 이름을 사용하지 않도록 주의해야 합니다. 이름 변경 업스트림을 요청하고 승인을 받기 위해 PEP(Python Enhancement Proposal)를 제출하는 것보다 패키지 내에서 모듈 이름을 변경하는 것이 훨씬 쉽습니다.
일반적인 실수 #9: Python 2와 Python 3의 차이점을 해결하지 못함
다음 파일 foo.py
를 고려하십시오.
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
Python 2에서는 다음과 같이 잘 실행됩니다.
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
그러나 이제 Python 3에서 소용돌이를 일으키자:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
여기에서 무슨 일이 일어난 겁니까? "문제"는 Python 3에서 예외 객체가 except
블록의 범위를 넘어 접근할 수 없다는 것입니다. (그 이유는 그렇지 않으면 가비지 수집기가 실행되고 메모리에서 참조를 제거할 때까지 메모리의 스택 프레임과 함께 참조 주기를 유지하기 때문입니다. 이에 대한 자세한 기술 정보는 여기에서 확인할 수 있습니다.)
이 문제를 방지하는 한 가지 방법은 예외 개체가 액세스 가능한 상태로 유지되도록 except
블록의 범위 외부 에 있는 예외 개체에 대한 참조를 유지하는 것입니다. 다음은 이 기술을 사용하여 Python 2 및 Python 3 모두에 친숙한 코드를 생성하는 이전 예제 버전입니다.
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
이것을 Py3k에서 실행:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
야!
(참고로, Python 고용 가이드에서는 Python 2에서 Python 3으로 코드를 마이그레이션할 때 알아야 할 다른 여러 중요한 차이점에 대해 설명합니다.)
일반적인 실수 #10: __del__
메서드 오용
mod.py
라는 파일에 이것을 가지고 있다고 가정해 보겠습니다.
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
그런 다음 another_mod.py
에서 이것을 시도했습니다.
import mod mybar = mod.Bar()
못생긴 AttributeError
예외가 발생합니다.
왜요? 여기에 보고된 대로 인터프리터가 종료되면 모듈의 전역 변수가 모두 None
으로 설정되기 때문입니다. 결과적으로 위의 예에서 __del__
이 호출되는 시점에서 foo
라는 이름은 이미 None
으로 설정되어 있습니다.
이 다소 고급 Python 프로그래밍 문제에 대한 솔루션은 대신 atexit.register()
를 사용하는 것입니다. 그렇게 하면 프로그램 실행이 완료되면(정상적으로 종료될 때) 인터프리터가 종료 되기 전에 등록된 핸들러가 시작됩니다.
이러한 이해를 바탕으로 위의 mod.py
코드에 대한 수정 사항은 다음과 같을 수 있습니다.
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
이 구현은 정상적인 프로그램 종료 시 필요한 정리 기능을 호출하는 깨끗하고 안정적인 방법을 제공합니다. 분명히, 이름 self.myhandle
에 바인딩된 객체로 무엇을 할지 결정하는 것은 foo.cleanup
에 달려 있지만 아이디어는 알 수 있습니다.
마무리
Python은 생산성을 크게 향상시킬 수 있는 많은 메커니즘과 패러다임을 가진 강력하고 유연한 언어입니다. 그러나 다른 소프트웨어 도구나 언어와 마찬가지로 그 기능을 제한적으로 이해하거나 이해하는 것은 때때로 이점보다 더 큰 장애물이 될 수 있으며, "위험할 만큼 충분히 안다"는 속담 상태가 됩니다.
이 기사에서 제기된 중간 정도의 고급 프로그래밍 문제와 같은 Python의 주요 뉘앙스를 숙지하면 더 일반적인 오류를 피하면서 언어 사용을 최적화하는 데 도움이 됩니다.
Python 전문가를 식별하는 데 도움이 될 수 있는 인터뷰 질문에 대한 제안은 Python 인터뷰에 대한 내부자 가이드를 확인할 수도 있습니다.
이 문서의 포인터가 도움이 되었기를 바라며 피드백을 환영합니다.