파이썬에서 흐름 제어 모범 사례는 예외입니까?


15

"Learning Python"을 읽고 다음을 발견했습니다.

사용자 정의 예외는 비 오류 조건을 나타낼 수도 있습니다. 예를 들어, 검색 루틴은 호출자가 해석 할 상태 플래그를 리턴하는 대신 일치하는 것이 발견되면 예외를 발생 시키도록 코딩 될 수 있습니다. 다음에서 try / except / else 예외 핸들러는 if / else 리턴 값 테스터의 작업을 수행합니다.

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

파이썬은 동적으로 유형이 지정되고 핵심에 다형성이 있기 때문에 센티넬 반환 값보다는 예외가 일반적으로 이러한 조건을 알리는 선호되는 방법입니다.

나는 다양한 종류의 포럼에서 StopIteration을 사용하여 루프를 끝내기 위해 파이썬에 대한 언급을 여러 번 논의했지만, 공식 스타일 가이드에서 많이 찾을 수는 없습니다 (PEP 8에는 흐름 제어에 대한 예외에 대한 언급이 하나 있습니다) 또는 개발자의 진술. 이것이 파이썬에 대한 모범 사례라고 말하는 공식적인 것이 있습니까?

이것은 ( 제어 흐름으로서의 예외는 심각한 반 패턴으로 간주됩니까? 그렇다면 그렇다면 왜입니까? ) 또한이 의견이이 스타일이 파이썬이라는 의견을 제시했습니다. 이것은 무엇을 기반으로합니까?

티아

답변:


25

일반적인 합의는 "예외를 사용하지 마십시오!" 대부분 다른 언어에서 왔으며 때로는 구식입니다.

  • C ++에서 던지는 예외 매우 비용이 많이 드는 "스택 해제"로 인해이. 모든 로컬 변수 선언은 withPython 의 명령문 과 유사하며 해당 변수의 객체가 소멸자를 실행할 수 있습니다. 이 소멸자는 예외가 발생했을 때뿐만 아니라 함수에서 돌아올 때 실행됩니다. 이 "RAII 관용구"는 필수 언어 기능이며 강력하고 올바른 코드를 작성하는 것이 매우 중요합니다. 따라서 저렴한 예외를 제외하고 RAII는 C ++이 RAII에 대해 결정한 상충 관계였습니다.

  • 초기 C ++에서는 많은 코드가 예외 안전 방식으로 작성되지 않았습니다. 실제로 RAII를 사용하지 않으면 메모리 및 기타 리소스가 유출되기 쉽습니다. 따라서 예외를 던지면 해당 코드가 잘못됩니다. C ++ 표준 라이브러리에서도 예외를 사용하기 때문에 더 이상 합리적이지 않습니다. 예외가 존재하지 않는 척할 수는 없습니다. 그러나 C 코드와 C ++를 결합 할 때 여전히 예외가 발생합니다.

  • Java에서 모든 예외에는 연관된 스택 추적이 있습니다. 스택 추적은 오류를 디버깅 할 때 매우 유용하지만 예외가 인쇄되지 않으면 제어 흐름에만 사용되므로 예외가 발생하지 않습니다.

따라서 이러한 언어에서는 예외가 너무 비싸서 제어 흐름으로 사용되지 않습니다. 파이썬에서는 이것이 문제가되지 않으며 예외가 훨씬 저렴합니다. 또한, 파이썬 언어는 이미 다른 제어 흐름 구조에 비해 눈에 띄지 않는 예외의 비용을하게 오버 헤드 앓고 : 예 검사를 사전인가 항목이 명시 적 회원 테스트로 존재하는 경우 if key in the_dict: ...입니다 일반적으로 정확하게 빨리 단순히 항목에 접근 the_dict[key]; ...하고 있는지 확인 KeyError를 얻습니다. 일부 필수 언어 기능 (예 : 생성기)은 예외 측면에서 설계되었습니다.

따라서 파이썬에서 예외를 피할 기술적 인 이유는 없지만 반환 값 대신 예외를 사용해야하는지에 대한 의문이 여전히 있습니다. 예외가있는 설계 수준 문제는 다음과 같습니다.

  • 그들은 전혀 분명하지 않습니다. 함수를 쉽게보고 어떤 예외가 발생할 수 있는지 확인할 수 없으므로 항상 무엇을 포착해야하는지 알 수 없습니다. 반환 값은보다 명확한 경향이 있습니다.

  • 예외는 코드를 복잡하게 만드는 비 로컬 제어 흐름입니다. 예외가 발생하면 제어 흐름이 재개 될 위치를 알 수 없습니다. 즉시 처리 할 수없는 오류의 경우 이는 호출자에게 상태를 알리면 전적으로 불필요합니다.

파이썬 문화는 일반적으로 예외에 찬성하여 기울어 져 있지만 쉽게 넘어갈 수 있습니다. list_contains(the_list, item)목록에 해당 항목과 동일한 항목이 포함되어 있는지 확인 하는 함수를 상상해보십시오 . 결과가 절대적으로 성가신 예외를 통해 전달되면 다음과 같이 호출해야하기 때문입니다.

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

부울을 반환하는 것이 훨씬 명확합니다.

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

함수가 이미 값을 반환한다고 가정하면 사람들 이이 값을 확인하는 것을 잊어 버릴 수 있기 때문에 특수 조건에 대한 특수 값을 반환하는 것은 다소 오류가 발생하기 쉽습니다 (C의 1/3 문제의 원인 일 수 있음). 일반적으로 예외가 더 정확합니다.

좋은 예는 `haystack 문자열에서 문자열 pos = find_string(haystack, needle)의 첫 항목을 검색 needle하고 시작 위치를 반환하는 함수입니다. 그러나 그들이 건초 줄에 바늘 줄이 없다면 어떻게 될까요?

C에 의한 솔루션과 파이썬에 의해 흉내 낸 솔루션은 특별한 값을 반환하는 것입니다. C에서 이것은 널 포인터이고, 파이썬에서 이것은입니다 -1. 이것은 특히 -1파이썬에서 유효한 인덱스 처럼 위치를 검사하지 않고 문자열 인덱스로 사용할 때 놀라운 결과를 초래할 것 입니다. C에서 NULL 포인터는 적어도 segfault를 제공합니다.

PHP에서는 다른 유형의 특수 값인 FALSE정수 대신 부울이 반환 됩니다. 결과적으로 언어의 암시 적 변환 규칙으로 인해 실제로 더 나아지는 것은 아닙니다 (그러나 파이썬에서는 부울을 정수로 사용할 수 있습니다!). 일관된 유형을 반환하지 않는 함수는 일반적으로 매우 혼란스러운 것으로 간주됩니다.

보다 강력한 변형은 문자열을 찾을 수 없을 때 예외를 throw하는 것이 었으므로 정상적인 제어 흐름 중에 실수로 일반 값 대신 특수 값을 사용할 수 없습니다.

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

또는 항상 직접 사용할 수 없지만 먼저 래핑 해제해야하는 유형을 반환 할 수 있습니다 (예 : 부울이 예외 발생 여부 또는 결과 사용 가능 여부를 나타내는 결과 부울 튜플). 그때:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

이렇게하면 문제를 즉시 처리 할 수 ​​있지만 매우 성가시다. 또한 체인 기능을 쉽게 할 수 없습니다. 모든 함수 호출에는 이제 세 줄의 코드가 필요합니다. 골랑은이 귀찮은 것이 가치가 있다고 생각하는 언어입니다.

요약하자면, 예외는 문제가없는 것은 아니며, 특히 "정상"반환 값을 대체 할 때 확실히 과용 될 수 있습니다. 그러나 오류가 아닌 특수 조건을 신호하는 데 사용되는 예외를 사용하면 깨끗하고 직관적이며 사용하기 쉽고 오용하기 어려운 API를 개발할 수 있습니다.


1
심층적 인 답변에 감사드립니다. 저는 "예외가 예외적 인 상황에서만 발생하는"Java와 같은 다른 언어의 배경에서 왔습니다. EAFP, EIBTI 등과 같은 다른 파이썬 가이드 라인 (메일 링리스트, 블로그 포스트 등)에서 이와 같은 방식으로 언급 한 파이썬 핵심 개발자가 있습니까? 다른 개발자 / 보스가 묻습니다. 감사!
JB

사용자 정의 예외 클래스의 fillInStackTrace 메소드를 오버로드하여 즉시 리턴하면 스택 워킹이 비싸고 이것이 완료되는 위치이므로 인상하기에 매우 저렴하게 만들 수 있습니다.
존 코완

인트로의 첫 번째 글 머리 기호는 처음에는 혼란 스러웠습니다. 어쩌면 "현대 C ++에서 예외 처리 코드는 제로 비용이지만 예외를 던지는 것은 비싸다" 와 같은 것으로 바꿀 수 있을까요? (
잠복 자

를 사용 collections.defaultdict하거나 사전을 사용하는 사전 조회 작업의 my_dict.get(key, default)경우 코드가보다 명확 해집니다.try: my_dict[key] except: return default
Steve Barnes

11

아니! - 하지 일반적으로 - 예외 코드의 단일 클래스를 제외하고 좋은 흐름 제어 연습 간주되지 않습니다. 예외를 조건을 알리는 합리적 또는 더 나은 방법으로 간주하는 곳은 생성기 또는 반복기 작업입니다. 이러한 작업은 가능한 모든 값을 유효한 결과로 반환 할 수 있으므로 마무리 신호를 전달하는 메커니즘이 필요합니다.

한 번에 한 바이트 씩 스트림의 이진 파일을 읽는 것을 고려하십시오. 절대적으로 모든 값은 잠재적으로 유효한 결과이지만 파일의 끝을 신호해야합니다. 그래서 우리는 매번 두 가지 값 (바이트 값과 유효한 플래그)을 반환 하거나 더 이상 할 일이 없으면 예외를 발생시킵니다. 두 가지 경우에 소비 코드는 다음과 같습니다.

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

대안 적으로 :

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

그러나 이것은 PEP 343 이 구현되고 백 포트 된 이후 로, 모두 with성명서 에 깔끔하게 정리되어 있습니다. 위의 내용은 매우 pythonic입니다.

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

python3에서 이것은 다음이되었습니다.

for val in open(source, 'rb').read():
     processbyte(val)

배경, 이론적 근거, 예 등을 제공하는 PEP 343 을 읽어 보시기 바랍니다 .

또한 제너레이터 기능을 사용하여 마무리 신호를 보낼 때 처리 종료 신호를 보내기 위해 예외를 사용하는 것이 일반적입니다.

검색기 예제가 거의 확실하게 뒤떨어져 있다고 가정하고 싶습니다. 그러한 함수는 생성기이어야하며 첫 번째 호출에서 첫 번째 일치를 반환 한 다음 대체 호출이 다음 일치를 반환하고 NotFound이상 일치 하지 않으면 예외를 발생 시킵니다.


"아니오!-일반적이 아닙니다-예외는 단일 코드 클래스를 제외하고는 양호한 흐름 제어 관행으로 간주되지 않습니다." 이 강조 진술의 근거는 어디에 있습니까? 이 답변은 반복 사례에 좁게 초점을 맞추는 것 같습니다. 사람들이 예외를 사용한다고 생각할 수있는 다른 많은 경우가 있습니다. 다른 경우에는 그렇게하는 데 어떤 문제가 있습니까?
Daniel Waltrip

@DanielWaltrip 나는 단순한 if것이 더 좋을 때 예외가 자주 사용되는 다양한 프로그래밍 언어에서 너무 많은 코드를 본다 . 디버깅 및 테스트 또는 코드 동작에 대한 추론과 관련하여 예외에 의존하지 않는 것이 좋습니다. 예외는 예외입니다. 생산 코드에서 간단한 반환이 아닌 throw / catch를 통해 반환되는 계산은 계산이 0으로 나누기 오류에 도달하면 오류가 발생한 곳에서 충돌하지 않고 임의의 값을 반환한다는 것을 의미했습니다. 디버깅.
Steve Barnes

Hey @SteveBarnes, 0으로 나누기 오류 캐치 예제는 프로그래밍이 좋지 않은 것처럼 들립니다 (예외를 다시 발생시키지 않는 너무 일반적인 캐치).
wTyeRogers

당신의 대답은 다음과 같이 요약됩니다 : A) "NO!" B) 예외를 통한 제어 흐름이 대안보다 더 잘 작동하는 유효한 예가있다. C) 생성기를 사용하도록 질문 코드를 수정 (예외를 제어 흐름으로 사용하고 잘 수행). 귀하의 답변은 일반적으로 유익하지만, 항목 A를 지원하는 주장은 존재하지 않는 것 같습니다. 항목 A는 굵은 글씨로 표시되어 있지만, 약간의 지원이 필요합니다. 그것이 지원된다면, 나는 당신의 대답을 무시하지 않을 것입니다. 아아 ...
wTyeRogers
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.