Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 765 – finally 블록을 빠져나가는 return/break/continue 금지

Author:
Irit Katriel <irit at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
15-Nov-2024
Python-Version:
3.14
Post-History:
09-Nov-2024, 16-Nov-2024
Replaces:
601
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at finally clause.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 finally 블록을 빠져나가는 return, break, continue 문에 대한 지원을 철회할 것을 제안합니다. 이는 과거에 PEP 601에서 제안된 바 있습니다. 이번 PEP는 이 변경의 비용과 편익에 관한 경험적 증거에 기반합니다. 이 증거는 PEP 601이 거부될 당시에는 존재하지 않았습니다. 또한 PEP 601에서 제안한 것과는 약간 다른 해결책을 제안합니다.

동기

finally 블록에서 return, break, continue의 의미는 많은 개발자에게 뜻밖입니다. 문서에는 다음과 같이 언급되어 있습니다:

  • finally 절이 break, continue, return 문을 실행하면 예외는 다시 발생하지 않습니다.
  • finally 절에 return 문이 포함되어 있으면, 반환되는 값은 try 절의 return 문의 값이 아니라 finally 절의 return 문의 값이 됩니다.

두 동작 모두 혼란을 일으키지만, 삼켜진 예외는 잘못된 반환값보다 테스트에서 발견되지 않고 넘어갈 가능성이 더 높으므로 첫 번째 동작이 특히 위험합니다.

2019년에 PEP 601은 몇 릴리스 동안 SyntaxWarning을 내보내다가 이후 SyntaxError로 바꾸도록 Python을 변경할 것을 제안했습니다. 이 제안은 이를 린터와 PEP 8이 다룰 프로그래밍 스타일 문제로 보는 쪽이 선호되어 거부되었습니다. 실제로 PEP 8은 이제 finally 블록에서 제어 흐름 문을 사용하지 말 것을 권장하며, Pylint, Ruff, flake8-bugbear 같은 린터는 이를 문제로 보고 플래그를 표시합니다.

근거

최근의 실제 코드 분석은 다음을 보여 줍니다:

  • 이러한 기능은 드뭅니다(상위 8,000개 PyPI 패키지에서 백만 LOC당 2건, 무작위로 선택한 패키지에서 백만 LOC당 4건). 이는 이 패턴에 플래그를 표시하는 린터 덕분일 수 있습니다.
  • 사용 사례 대부분은 잘못되었으며, 의도하지 않은 예외 삼킴 버그를 만들어 냅니다.
  • 코드 소유자는 대개 버그 수정에 호의적이며, 수정하기도 쉽다고 생각합니다.

자세한 내용은 부록을 참조하십시오.

이 새로운 데이터는 Python 자체가 사용자들이 이 해로운 기능을 쓰지 않도록 이끈다면 Python 사용자에게 이로울 것임을 보여 줍니다.

PEP 601 논의에서 제기된 인자 중 하나는 언어 기능이 직교적이어야 하며, 문맥에 따른 제한 없이 결합되어야 한다는 것이었습니다. 그러나 그 사이에 PEP 654가 구현되었는데, 이 PEP는 except* 절에서 return, break, continue를 금지합니다. 그렇게 할 경우의 의미가 except* 절이 병렬로 동작한다는 속성, 즉 한 절의 코드가 다른 절의 호출을 억제해서는 안 된다는 속성을 위반하기 때문입니다. 그 경우 우리는 기능의 조합이 금지하는 것이 타당할 만큼 해로울 수 있다는 점을 받아들였습니다.

명세

이 변경은 return, break, continue가 finally 블록 안에서 그 바깥 위치로 제어 흐름을 옮기게 될 때 Python 컴파일러가 SyntaxWarning이나 SyntaxError를 내보낼 수 있다는 것을 언어 명세의 일부로 규정하는 것입니다.

다음 예제는 SyntaxWarning 또는 SyntaxError를 발생시킬 수 있습니다:

 def f():
     try:
         ...
     finally:
         return 42

 for x in o:
     try:
         ...
     finally:
         break  # (or continue)

다음 예제는 경고나 오류를 발생시키지 않습니다:

 try:
     ...
 finally:
     def f():
         return 42

 try:
     ...
 finally:
     for x in o:
         break  # (or continue)

CPython은 버전 3.14에서 SyntaxWarning을 발생시킬 것이며, 이것이 SyntaxError가 될지, 된다면 언제가 될지는 열어 둡니다. 그러나 다른 Python 구현이 이를 구현하기로 선택할 수 있도록, 여기서는 언어 명세가 SyntaxError를 허용한다는 것을 규정합니다.

CPython 구현은 AST 생성 중에 SyntaxWarning을 발생시킬 것입니다. 이는 경고가 정적 분석과 컴파일 중에는 나타나되, 미리 컴파일된 코드를 실행할 때는 나타나지 않도록 하기 위함입니다. 우리는 프로젝트 유지 관리자가 (정적 분석을 실행하거나, 미리 컴파일된 파일이 없는 CI를 실행할 때) 이 경고를 보게 될 것으로 예상합니다. 그러나 프로젝트의 최종 사용자는 설치 시 사전 컴파일을 건너뛰거나, 설치 시점의 경고를 확인하거나, 의존성에 대해 정적 분석을 실행하는 경우에만 경고를 보게 됩니다.

하위 호환성

하위 호환성을 이유로, CPython이 SyntaxWarning만 발생시키도록 제안하며 이를 오류로 격상할 구체적인 계획은 없습니다. 이 변경이 도입되면 -We 옵션으로 실행되는 코드는 동작하지 않을 수 있습니다.

보안 영향

이 경고/오류는 프로그래머가 찾기 어려운 일부 버그를 피하는 데 도움이 되므로 보안상 이점이 있습니다. 새로운 SyntaxWarning 또는 SyntaxError를 발생시키는 것과 관련된 보안 문제는 알려진 바가 없습니다.

교육 방법

이 변경 사항은 언어 명세와 What’s New 문서에 기록될 것입니다. SyntaxWarning은 사용자에게 코드를 변경해야 한다는 사실을 알려 줄 것입니다. 경험적 증거에 따르면 필요한 변경은 대체로 매우 간단합니다.

기각된 아이디어

CPython에서 SyntaxError 발생시키기

PEP 601은 CPython이 몇 번의 릴리스 동안 SyntaxWarning을 발생시키고 그 이후에는 SyntaxError를 발생시키도록 제안했습니다. SyntaxWarning만으로도 더 적은 위험으로 대부분의 이점을 얻을 수 있다고 보기 때문에, CPython에서 이것이 SyntaxError가 될지, 된다면 언제가 될지는 열어 둡니다.

의미 변경

진행 중인 예외가 finally 안의 제어 흐름 명령어보다 우선하도록 이 명령어들의 의미를 변경하자는 제안이 있었습니다. 다시 말해, return, break, continue가 허용되어 finally 블록을 빠져나가지만, 예외는 여전히 발생하게 됩니다.

이 제안은 두 가지 이유로 기각되었습니다. 첫째, 이 제안은 정상적으로 동작하는 코드의 의미를 디버깅하기 어려운 방식으로 바꿉니다. 모든 예외를 삼키려는 의도로 (문서화된 의미를 올바르게 사용하여) 작성된 finally가 이제는 예외를 계속 전파하게 됩니다. 이는 런타임에 드문 경계 사례에서만 발생할 수 있으며, 테스트에서 반드시 감지된다는 보장이 없습니다. 코드가 잘못되어 예외를 삼키는 버그가 있더라도, 3.13에서는 예외를 발생시키지 않던 프로그램이 왜 3.14에서 예외를 발생시키기 시작했는지 사용자가 이해하기 어려울 수 있습니다. 반면 SyntaxWarning은 테스트 중에 드러날 가능성이 높고, 코드에서 문제가 있는 정확한 위치를 가리키며, 프로그램 실행을 막지도 않습니다.

두 번째 반대 의견은 제안된 의미 자체에 관한 것이었습니다. 제어 흐름 문을 허용하려는 동기는 그것이 유용해서가 아니라 기능의 직교성을 바라기 때문입니다(서론에서 언급했듯이, 이 직교성은 except* 절의 경우 이미 깨져 있습니다). 그러나 제안된 의미는 복잡합니다. return, break, continue가 진행 중인 예외 없이 finally가 실행될 때는 평소처럼 동작하지만, 진행 중인 예외가 있을 때는 인자 없는 raise와 비슷한 것으로 바뀐다는 뜻이기 때문입니다. 한 기능의 존재가 다른 기능의 의미를 바꾼다면 두 기능이 직교한다고 주장하기 어렵습니다.

부록

해로운 것으로 여겨지는 finally 안의 return

다음은 Irit Katriel이 2024년 11월 9일에 게시한 연구 보고서를 요약한 것입니다. 이 보고서는 실제 코드에서 finally 절 안의 return, break, continue 사용 실태를 조사한 내용을 다루며, 다음 질문에 답합니다: 사람들이 이것을 사용하고 있습니까? 얼마나 자주 잘못 사용하고 있습니까? 제안된 변경으로 인해 얼마나 많은 코드 수정이 필요하게 되겠습니까?

방법

이 분석은 최근 30일간의 다운로드 수를 기준으로 가장 인기 있는 PyPI 패키지 8,000개를 대상으로 합니다. 패키지들은 10월 17~18일에 Guido van Rossum이 작성한 스크립트를 사용하여 내려받았으며, 이 스크립트는 가장 인기 있는 패키지 목록을 만드는 Hugo van Kemenade의 도구에 의존합니다.

내려받은 후에는 두 번째 스크립트를 사용하여 각 파일의 AST를 구성하고, 이를 순회하여 finally 블록 바로 안에 있는 break, continue, return 문을 찾아냈습니다.

그런 다음 각 사례의 현재 소스 코드를 찾아 분류했습니다. 코드가 잘못된 것으로 보이는 경우에는 해당 프로젝트의 버그 트래커에 이슈를 등록했습니다. 이 이슈들에 대한 응답 또한 이번 조사에서 수집한 데이터의 일부입니다.

결과

이 보고서가 망신 주기처럼 보일 수 있다는 우려 때문에 잘못된 사용 사례의 목록은 포함하지 않기로 했습니다. 대신 결과를 일반적인 용어로 설명하되, 제가 발견한 문제 중 일부가 클라우드 보안 애플리케이션을 비롯한 매우 인기 있는 라이브러리에서 나타난다는 점은 언급해 두겠습니다. 제가 사용한 스크립트의 링크를 방법 절에 제공했으므로, 관심 있는 분이라면 제 분석을 재현하는 것이 어렵지 않을 것입니다.

조사한 프로젝트에는 총 120,964,221줄의 Python 코드가 포함되어 있었으며, 스크립트는 그중에서 finally 블록 안에 있는 제어 흐름 명령어 203건을 찾아냈습니다. 대부분은 return이었고, 몇 건은 break였으며, continue는 하나도 없었습니다. 그 내역은 다음과 같습니다:

  • 46건은 올바른 사용이며, 이 패턴을 기능으로서 다루는 테스트(예: 이 패턴을 탐지하는 린터의 테스트)에 나타납니다.
  • 8건은 올바를 수도 있는 것으로 보입니다. 의도적으로 예외를 삼키거나, 활성 예외가 발생할 수 없는 곳에 나타나는 경우입니다. 올바르기는 하지만, 나쁜 패턴을 피하도록 다시 작성하는 것이 어렵지 않으며 그렇게 하면 코드가 더 명확해집니다: 의도적으로 예외를 삼키는 것은 except BaseException:으로 더 명시적으로 할 수 있고, 예외를 삼키지 않는 return은 finally 블록 뒤로 옮길 수 있습니다.
  • 149건은 명백히 잘못되었으며, 의도하지 않게 예외를 삼키는 결과로 이어질 수 있습니다. 이들은 다음 절에서 분석합니다.

오류 사례

오류 사례 중 다수는 다음 패턴을 따랐습니다:

try:
    ...
except SomeSpecificError:
    ...
except Exception:
    logger.log(...)
finally:
    return some_value

이와 같은 코드는 Exception 서브클래스는 의도적으로 로그를 남기고 삼키면서 BaseExceptions는 조용히 삼키기 때문에 명백히 잘못되었습니다. 여기서의 의도는 BaseExceptions가 계속 전파되도록 하거나, (작성자가 BaseException 문제를 알지 못하는 경우) 모든 예외에 대해 로그를 남기고 삼키는 것입니다. 그러나 except Exception을 except BaseException으로 바꾸더라도, 이 코드에는 여전히 finally 블록이 except 블록 안에서 발생한 모든 예외를 삼킨다는 문제가 남으며, 이는 아마도 의도한 바가 아닐 것입니다(의도한 것이라면 또 다른 try-except BaseException으로 명시할 수 있습니다).

실제 코드에서 발견된 이 문제의 또 다른 변형은 다음과 같습니다:

    try:
        ...
    except:
        return NotImplemented
    finally:
        return some_value

여기서의 의도는 예외가 발생했을 때 NotImplemented를 반환하는 것으로 보이지만, finally 블록 안의 return이 except 블록 안의 return을 덮어쓰게 됩니다.

Note

다음에 이어 논의에 이어, 평균적인 프로그래머가 작성한 코드를 분석하기 위해 무작위로 고른 PyPI 패키지를 대상으로 같은 분석을 다시 수행했습니다. 표본은 총 77,398,892줄의 코드로 이루어져 있었으며, finally 안에서 return/break/continue를 사용한 사례가 316건 있었습니다. 즉, 코드 100만 줄당 약 4건입니다.

작성자들의 반응

finally 절 안에서 return 또는 break를 잘못 사용한 149건 중 27건은 이미 오래된 사례였습니다. 코드가 그사이 삭제되었거나 수정되어 라이브러리의 main/master 브랜치에는 더 이상 나타나지 않았기 때문입니다. 나머지 122건은 서로 다른 73개 패키지에 있었으며, 작성자에게 문제를 알리기 위해 패키지마다 이슈를 등록했습니다. 2주 안에 73개 이슈 중 40개가 코드 유지 관리자로부터 반응을 받았습니다:

  • 15개 이슈에는 문제를 수정하는 PR이 열렸습니다.
  • 20개 이슈는 살펴볼 가치가 있는 문제라고 인정하는 반응을 받았습니다.
  • 3건은 코드가 더 이상 유지 보수되지 않으므로 수정하지 않을 것이라고 답했습니다.
  • 2건은 “의도한 대로 동작함”이라며 이슈를 닫았습니다. 그중 한 곳은 모든 예외를 삼키려는 의도라고 밝혔지만, 다른 한 곳은 Exception과 BaseException의 차이를 모르는 것으로 보였습니다.

한 이슈는 연관성이 있을 것이라는 추측과 함께 Ctrl-C에 반응하지 않는 문제를 다룬 기존의 열린 이슈에 연결되었습니다.

이슈 중 두 개에는 “good first issue” 레이블이 붙었습니다.

올바른 사용 사례

(테스트 코드가 아닌 곳에서) 이 기능을 올바르게 사용한 것으로 보이는 8건의 사례도 주목할 필요가 있습니다. 이 기능을 막으면 바로 이런 곳에서 잘 동작하는 코드를 바꿔야 하므로, 이 사례들은 기능 차단이 일으킬 “변경 부담”을 보여 줍니다. 이 사례들에서는 작성자에게 연락하지 않았으므로, 이러한 변경이 얼마나 어려운지는 우리가 직접 평가해야 합니다. 전체 보고서에서 볼 수 있듯이, 각 사례에 필요한 변경은 작습니다.

논의

가장 먼저 주목할 점은 finally 블록 안의 return/break/continue가 자주 보이지 않는다는 것입니다. 1억 2천만 줄이 넘는 코드에서 203건에 불과했습니다. 이는 아마도 이런 사용을 경고하는 린터 덕분일 것입니다.

두 번째로 관찰한 점은 대부분의 사용이 잘못되었다는 것입니다. 우리 표본에서는 73%(203건 중 149건)가 잘못된 사용이었습니다.

마지막으로, 작성자들의 반응은 압도적으로 긍정적이었습니다. 2주 안에 받은 40건의 반응 중 35건이 문제를 인정했으며, 그중 15건은 문제를 수정하는 PR까지 만들었습니다. 코드가 지금 그대로 괜찮다고 본 경우는 두 건뿐이었으며, 세 건은 코드가 더 이상 유지 보수되지 않으므로 살펴보지 않겠다고 밝혔습니다.

코드가 의도한 대로 동작하는 것으로 보이는 8건의 사례는 다시 작성하기 어렵지 않습니다.