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

Python 개선 제안 한국어 번역

PEP 502 – 문자열 보간 - 확장 논의

Author:
Mike G. Miller
Status:
Rejected
Type:
Informational
Created:
10-Aug-2015
Python-Version:
3.6

Table of Contents

번역·라이선스 안내

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

초록

PEP 498: 리터럴 문자열 보간은 “형식화된 문자열”을 제안했으며 2015년 9월 9일에 수락되었습니다. 설계 단계에서 제시된 추가 배경과 근거는 아래에 자세히 설명되어 있습니다.

해당 PEP를 요약하면, 문자열을 렌더링할 템플릿으로 표시하는 문자열 접두사가 도입되었습니다. 이러한 포맷 문자열은 str.format()의 기존 문법(the existing syntax)을 기반으로 하는 하나 이상의 표현식을 포함할 수 있습니다. [10] [11] 포맷 문자열은 컴파일 시점에 일반적인 문자열 포맷 연산으로 확장되며, 텍스트에 있는 표현식은 추출되어 대신 위치 인자로 전달됩니다.

런타임에 결과 표현식이 평가되어, 주어진 명세에 따라 문자열을 렌더링합니다.:

>>> location = 'World'
>>> f'Hello, {location} !'      # new prefix: f''
'Hello, World !'                # interpolated result

포맷 문자열은 기존의 str.format() 호출을 단순화하는 문법적 설탕에 불과하다고 생각할 수 있습니다.

PEP 상태

이 PEP는 사실에 기반한 어조가 아닌 의견에 기반한 어조를 사용했다는 이유로 거부되었습니다. 또한 PEP 498이 이미 작성되어 있었고 설계 결정의 세부 사항은 그곳에 담겨야 하므로, 이 PEP는 중요하지 않다고 판단되었습니다.

동기

Python에는 문자열 포매팅 및 조작 기능이 풍부하지만, 부족한 영역이 하나 있는데 바로 편리한 문자열 보간 문법이 없다는 점입니다. 비슷한 사용 사례를 가진 다른 동적 스크립트 언어와 비교하면, 비슷한 문자열을 만드는 데 필요한 코드의 양이 상당히 많으며, 때로는 장황함, 빽빽한 문법, 식별자 중복 때문에 가독성도 떨어집니다.

이러한 어려움은 (PEP 498이 된) 눈덩이를 굴리기 시작한 원래의 python-ideas 게시물(post to python-ideas)에 어느 정도 자세히 설명되어 있습니다. [1]

게다가 print 문이 더 일관된 Python 3의 print 함수(PEP 3105)로 대체되면서, 입력하고 읽어야 할 괄호 한 쌍이 더 늘어나는 사소한 부담이 하나 추가되었습니다. 현재 문자열 포매팅 방식의 장황함과 합쳐져, 이는 그렇지 않았다면 단순했을 언어를 동류의 언어들에 비해 안타깝게도 불리한 위치에 놓이게 합니다:

echo "Hello, user: $user, id: $id, on host: $hostname"              # bash
say  "Hello, user: $user, id: $id, on host: $hostname";             # perl
puts "Hello, user: #{user}, id: #{id}, on host: #{hostname}\n"      # ruby
                                                                    # 80 ch -->|
# Python 3, str.format with named parameters
print('Hello, user: {user}, id: {id}, on host: {hostname}'.format(**locals()))

# Python 3, worst case
print('Hello, user: {user}, id: {id}, on host: {hostname}'.format(user=user,
                                                                  id=id,
                                                                  hostname=
                                                                    hostname))

Python에서는 여러 변수를 포함한 문자열을 표준 너비의 한 줄 코드로 포매팅하고 출력하는 것이 눈에 띄게 더 어렵고 장황하며, 들여쓰기가 이 문제를 더욱 악화시킵니다.

메시지 형식화의 복잡성이 아직 캡슐화되지 않은 소규모 프로젝트, 시스템 프로그래밍, 셸 스크립트 대체, 심지어 한 줄짜리 코드와 같은 사용 사례에서는, 이러한 장황함 때문에 지난 수년간 상당수의 개발자와 관리자가 다른 언어를 선택했을 가능성이 높습니다.

근거

목표

형식 문자열의 설계 목표는 다음과 같습니다.

  1. 변수를 수동으로 전달할 필요를 없앱니다.
  2. 식별자의 반복과 불필요한 괄호를 없앱니다.
  3. 어색한 문법, 구두점 문자, 시각적 잡음을 줄입니다.
  4. 위치 인자보다 이름 있는 매개변수를 선호함으로써 가독성을 높이고 불일치 오류를 없앱니다.
  5. locals()와 globals()를 사용할 필요 없이, 주어진 문자열을 구문 분석하여 이름 있는 매개변수를 찾은 다음 자동으로 전달합니다. [2] [3]

제약 사항

Unix와 그 셸에서 설계 단서를 얻은 다른 언어들과 달리, 그리고 Javascript와 마찬가지로, Python은 문자열을 감싸는 데 작은따옴표(')와 큰따옴표(") ASCII 인용 문자를 모두 지정했습니다. 이제 와서 둘 중 하나를 보간용으로 선택하고 나머지 하나를 보간되지 않는 문자열용으로 남겨 두는 것은 합리적이지 않습니다. “백틱”(또는 억음 부호 `) 같은 다른 문자들도 repr()의 단축 표기로 쓰였던 역사적 제약(constrained by history)을 받습니다.

따라서 이러한 기능의 설계에는 몇 가지 선택지만 남습니다:

  • %를 사용하는 printf 스타일 문자열 포매팅처럼 연산자를 사용하는 방법.
  • string.Template()과 같은 클래스를 사용하는 방법.
  • str.format()과 같은 메서드나 함수를 사용하는 방법.
  • 새로운 문법을 도입하는 방법, 또는
  • 잘 알려진 r''이나 u''와 같은 새로운 문자열 접두사 표시를 도입하는 방법.

위의 처음 세 가지 방법은 이미 성숙한 방법입니다. 각각 고유한 사용 사례와 단점이 있지만, 앞서 언급한 장황함과 시각적 잡음 문제도 함께 안고 있습니다. 모든 방법은 다음 절들에서 다룹니다.

배경

포맷 문자열은 기존의 여러 기법과 제안, 그리고 그로부터 우리가 함께 배운 것들을 바탕으로 합니다. 따라서 가독성과 오류 방지라는 설계 목표에 맞추어, 다음 예제들은 위치 인자가 아닌 이름 있는 인자를 사용합니다.

다음과 같은 딕셔너리가 있고, 그 항목들을 최종 사용자에게 유익한 정보를 주는 문자열로 출력하려 한다고 가정해 봅시다:

>>> params = {'user': 'nobody', 'id': 9, 'hostname': 'darkstar'}

연산자를 사용하는 printf 스타일 포매팅

이 venerable technique은 바이트 기반 프로토콜에 쓰인다는 점, 간단한 경우에 단순하다는 점, 많은 프로그래머에게 익숙하다는 점 등에서 여전히 쓸모가 있습니다:

>>> 'Hello, user: %(user)s, id: %(id)s, on host: %(hostname)s' % params
'Hello, user: nobody, id: 9, on host: darkstar'

이 형태에서는 딕셔너리를 먼저 만들어야 한다는 점까지 고려하면, 이 기법은 장황하고 약간 어수선하지만 비교적 읽기 쉽습니다. 또 다른 문제는 연산자가 원래 문자열 외에 인자를 하나만 받을 수 있다는 것입니다. 즉, 여러 매개변수를 튜플이나 딕셔너리로 전달해야 합니다. 또한 전달하는 인자의 개수나 기대하는 타입을 잘못 지정하거나, 키를 누락하거나, 끝에 붙는 타입(예: s 또는 d)을 빠뜨리는 실수를 비교적 쉽게 저지를 수 있습니다.

string.Template 클래스

PEP 292(더 간단한 문자열 치환)의 string.Template class from은 의도적으로 단순화된 설계로, 익숙한 셸 보간 문법을 사용하고 safe-substitution feature를 갖추고 있으며, 주된 사용 사례는 셸 및 국제화 도구에 있습니다:

Template('Hello, user: $user, id: ${id}, on host: $hostname').substitute(params)

이 방법 역시 장황하지만, 문자열 자체는 읽기 쉽습니다. 기능은 제한적이지만, 요구 사항은 잘 충족합니다. 많은 경우에 쓰기에는 충분히 강력하지 않지만, 그 덕분에 경험이 적은 사용자가 곤란에 빠지지 않으며, 제3자가 제공하는 어느 정도만 신뢰할 수 있는 입력(i18n)으로 인한 문제도 피할 수 있습니다. 안타깝게도 flufl.i18n과 같은 convenience library로 감싸지 않는 한, 즉석 문자열 보간에 사용하기를 꺼리게 될 만큼 많은 코드가 필요합니다.

PEP 215 - 문자열 보간

PEP 215는 이 제안과 공통점이 많은 이전 제안이었습니다. 당시에는 세상이 이를 받아들일 준비가 되어 있지 않았던 것으로 보이지만, 최근 여러 다른 언어에서 지원하고 있는 점을 고려하면 이제 그때가 왔는지도 모릅니다.

여기에 포함된 수많은 달러 기호($) 문자 때문에 Python의 숙적인 Perl과 비슷해 보였을 수 있으며, 이것이 해당 PEP가 수락되지 못한 원인 중 하나였을 가능성이 높습니다. 그 제안은 다음 제안으로 대체되었습니다.

str.format() 메서드

PEP 3101의 str.format() syntax of은 기존 방법 중 가장 최근에 나온 현대적인 방법입니다. 또한 다른 방법들보다 더 강력하며 대체로 더 읽기 쉽습니다. 이전 기법들의 많은 단점과 제약을 피할 수 있습니다.

그러나 필요한 함수 호출과 매개변수 전달 때문에, 문자열 리터럴을 다루는 여러 상황에서 장황한 수준부터 매우 장황한 수준까지 이릅니다:

>>> 'Hello, user: {user}, id: {id}, on host: {hostname}'.format(**params)
'Hello, user: nobody, id: 9, on host: darkstar'

# when using keyword args, var name shortening sometimes needed to fit :/
>>> 'Hello, user: {user}, id: {id}, on host: {host}'.format(user=user,
                                                            id=id,
                                                            host=hostname)
'Hello, user: nobody, id: 9, on host: darkstar'

메서드 기반 접근 방식이 얼마나 장황한지는 여기에서 볼 수 있습니다.

PEP 498 – 리터럴 문자열 포매팅

PEP 498은 위의 Abstract에서도 설명했듯이 형식 문자열을 정의하고 논의합니다.

또한 처음 접하는 사람들에게는 다소 논란이 될 수 있지만, 포맷 문자열이 임의의 표현식을 지원하도록 확장되어야 한다는 개념을 도입합니다. 이는 Rejected Ideas 아래의 문법 제한(Restricting Syntax) 절에서 더 자세히 다룹니다.

PEP 501 – 번역에 대비한 문자열 보간

이를 보완하는 PEP 501은 i-접두사, ES6(Javascript)와 호환되는 string.Template 문법 통합, 지연 렌더링, 객체 반환값을 제안하면서 국제화를 일급 관심사로 논의에 끌어들입니다.

다른 언어의 구현

문자열 보간은 이제 여러 산업 분야에서 사용되는 다양한 프로그래밍 언어에서 잘 지원되며, 일종의 표준으로 수렴하고 있습니다. 이는 str.format() 스타일 문법을 약간씩 변형한 형태를 중심으로 하며, 유용성을 넓히기 위해 임의의 표현식을 추가합니다.

Motivation 절에서는 Bash, Perl, Ruby에 편리한 보간 문법이 어떻게 존재하는지 보였습니다. 이들의 표현식 지원을 살펴봅시다.

Bash

Bash는 문자열 안에서 여러 가지 임의의, 심지어 재귀적인 구문까지 지원합니다:

> echo "user: $USER, id: $((id + 6)) on host: $(echo is $(hostname))"
user: nobody, id: 15 on host: is darkstar
  • 큰따옴표 안에서 명시적 보간을 지원합니다.
  • 환경 변수에 직접 접근할 수 있습니다.
  • 임의의 표현식을 지원합니다. [4]
  • 외부 프로세스 실행과 출력 캡처를 지원합니다. [5]
  • 재귀적 표현식을 지원합니다.

Perl

Perl에도 임의의 표현식 구문이 있지만, 그다지 잘 알려져 있지는 않을 것입니다:

say "I have @{[$id + 6]} guanacos.";                # lists
say "I have ${\($id + 6)} guanacos.";               # scalars
say "Hello { @names.join(', ') } how are you?";     # Perl 6 version
  • 큰따옴표 안에서 명시적 보간을 지원합니다.
  • 임의의 표현식을 지원합니다. [6] [7]

Ruby

Ruby는 보간 문자열 안에서 임의의 표현식을 허용합니다:

puts "One plus one is two: #{1 + 1}\n"
  • 큰따옴표 안에서 명시적 보간을 지원합니다.
  • 임의의 표현식을 지원합니다. [8] [9]
  • %로 구분 문자를 바꿀 수 있습니다.
  • Python 구현은 참조 구현 절을 참고하십시오.

기타

최근 문자열 보간을 구현한, 덜 비슷한 현대 언어 몇 가지를 살펴봅시다.

Scala

Scala interpolation은 문자열 접두사를 통해 이루어집니다. 각 접두사는 서로 다른 결과를 냅니다.:

s"Hello, $name ${1 + 1}"                    # arbitrary
f"$name%s is $height%2.2f meters tall"      # printf-style
raw"a\nb"                                   # raw, like r''

사용자는 Scala의 StringContext 클래스를 확장하여 이러한 접두사를 직접 구현할 수도 있습니다.

  • 리터럴 접두사를 붙인 큰따옴표 안에서 명시적으로 보간합니다.
  • 사용자가 구현한 접두사를 지원합니다.
  • 임의의 표현식을 지원합니다.

ES6 (Javascript)

Template strings의 설계자들도 작은따옴표와 큰따옴표가 이미 쓰이고 있다는, Python과 같은 문제에 부딪혔습니다. 그러나 Python과 달리 “백틱”은 아직 쓰이지 않았습니다. their issues에도 불구하고, 백틱은 ECMAScript 2015 (ES6) 표준의 일부로 채택되었습니다.:

console.log(`Fifteen is ${a + b} and\nnot ${2 * a + b}.`);

태그와 같은 이름의 함수를 구현하면 사용자 정의 접두사도 지원됩니다.:

function tag(strings, ...values) {
    console.log(strings.raw[0]);    // raw string is also available
    return "Bazinga!";
}
tag`Hello ${ a + b } world ${ a * b}`;
  • 백틱 안에서 명시적으로 보간합니다.
  • 사용자가 구현한 접두사를 지원합니다.
  • 임의의 표현식을 지원합니다.

C#, 버전 6

C#에도 유용한 새 interpolation feature가 있으며, IFormattable 인터페이스를 통해 어느 정도 customize interpolation할 수 있습니다.:

$"{person.Name, 20} is {person.Age:D3} year{(p.Age == 1 ? "" : "s")} old.";
  • 큰따옴표와 $ 접두사로 명시적으로 보간합니다.
  • 사용자 정의 보간을 사용할 수 있습니다.
  • 임의의 표현식을 지원합니다.

Apple의 Swift

모든 문자열에서 임의의 interpolation under Swift를 사용할 수 있습니다.:

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
  • 큰따옴표 안에서 암묵적으로 보간합니다.
  • 임의의 표현식을 지원합니다.
  • CR/LF를 포함할 수 없습니다.

추가 예제

문자열 보간의 여러 추가 예제는 found at Wikipedia에서 볼 수 있습니다.

배경과 역사를 살펴보았으니, 이제 해결책을 찾아 계속 나아갑시다.

새 문법

새 문법 기능은 저마다 그것이 자리 잡는 머릿속 공간이라는 비용을 치르게 하므로, 이는 최후의 수단이어야 합니다. 하지만 가능한 선택지 목록에 대안이 하나 더 남아 있으며, 이어서 다룹니다.

새 문자열 접두사

Python의 문자열 형식화 역사와 하위 호환성, 다른 언어의 구현, 꼭 필요하지 않은 한 새로운 문법을 피한다는 원칙을 고려하면, 수용 가능한 설계는 독창적인 통찰보다는 소거법을 통해 도출됩니다. 따라서 보간되는 문자열 리터럴을 문자열 접두사로 표시하는 방식을 선택했습니다.

또한 기능이 더 중복되지 않도록, 기존 선택지 중 가장 강력한 str.format()을 재사용하고 이를 기반으로 확장하는 표현식 문법을 선택했습니다:

>>> location = 'World'
>>> f'Hello, {location} !'      # new prefix: f''
'Hello, World !'                # interpolated result

PEP 498 – 리터럴 문자열 형식화(Literal String Formatting)는 이 설계의 메커니즘과 구현을 자세히 다룹니다.

추가 주제

안전성

이 절에서는 포맷 문자열을 지원하면서 고려한 안전성 상황과 예방 조치를 설명합니다.

  1. 포맷 문자열에는 입력으로 받거나 여기저기 전달되는 변수가 아닌 문자열 리터럴만을 고려했으므로, 외부 공격이 성공하기 어렵습니다.

    str.format() 및 그 대안들이 이미 이 사용 사례를 처리합니다(already handle).

  2. 변환 과정에서는 locals()도 globals()도 필요하지 않고 사용되지도 않으므로, 정보 유출이 방지됩니다.
  3. 복잡성과 재귀 깊이로 인한 RuntimeError를 없애기 위해, 재귀적 보간은 지원하지 않습니다.

그러나 문자열 리터럴 안의 실수나 악성 코드는 간과될 수 있습니다. 이는 코드 전반에 대해서도 할 수 있는 말이지만, 이러한 표현식이 문자열 안에 있다는 점 때문에 눈에 띄지 않을 가능성이 조금 더 높습니다.

도구를 통한 완화

pyflakes, pylint, Pycharm 같은 도구나 린터가 표현식이 포함된 문자열 내부를 검사하고 적절히 표시할 수 있다는 것이 기본 발상입니다. 이는 오늘날 프로그래밍 언어에서 흔한 작업이므로, 다중 언어 도구가 Python만을 위해 이 기능을 구현할 필요가 없어 구현에 드는 시간이 크게 단축됩니다.

더 먼 미래에는 프로젝트의 안전 정책을 벗어나는 구문이 있는지 문자열을 검사하게 될 수도 있습니다.

스타일 가이드/예방 조치

임의의 표현식은 Python 표현식이 할 수 있는 모든 일을 수행할 수 있으므로, 형식 문자열 안에서 부작용을 일으킬 수 있는 구문은 피할 것을 강력히 권장합니다.

사용 패턴과 실제 문제가 파악되면 추가 지침을 작성할 수 있습니다.

참조 구현

say module on PyPI는 여기에서 설명한 문자열 보간을 호출 가능 객체 인터페이스라는 작은 부담만으로 구현합니다:

> pip install say

from say import say
nums = list(range(4))
say("Nums has {len(nums)} items: {nums}")

Ruby 보간의 Python 구현도 있습니다(is also available). 이 구현은 codecs 모듈을 사용하여 작업을 수행합니다:

> pip install interpy

# coding: interpy
location = 'World'
print("Hello #{location}.")

하위 호환성

형식 문자열은 기존 문법을 사용하고 현재 또는 과거의 기능을 피함으로써 기존 코드에 간섭하지 않도록 설계되었으며, 어떤 문제도 일으키지 않을 것으로 예상됩니다.

보류된 아이디어

국제화

국제화 지원을 통합하는 것이 매우 바람직했지만(PEP 501 참조), 세부 사항이 거의 모든 지점에서 갈라지므로 공통 해법이 나오기 어렵습니다: [15]

  • 사용 사례의 차이
  • 컴파일 시점 작업과 실행 시점 작업
  • 보간 문법 요구 사항
  • 대상 독자
  • 보안 정책

기각된 아이디어

문법을 str.format()으로만 제한하기

임의 표현식 지원에 대한 일반적인 arguments against는 다음과 같았습니다:

  1. YAGNI, “그건 필요 없을 겁니다(You aren’t gonna need it).”
  2. 이 기능은 Python의 전통적인 보수주의와 맞지 않습니다.
  3. 연기 - 필요성이 입증되면 향후 버전에서 구현할 수 있습니다.

그러나 str.format() 문법만 지원하는 것은 문제에 대한 충분한 해결책이 되지 못한다고 판단되었습니다. 예를 들어, 출력하기 전에 객체의 길이를 구하거나 값을 증가시키는 것처럼 간단한 연산이 필요한 경우가 많습니다.

Implementations in Other Languages 절을 보면 개발자 커뮤니티 전반이 대체로 이에 동의한다는 것을 알 수 있습니다. 임의 표현식을 사용하는 문자열 보간은 그 유용성 덕분에 현대 언어에서 업계 표준이 되어 가고 있습니다.

추가/사용자 정의 문자열 접두사

Implementations in Other Languages 절에서 보았듯이, 많은 현대 언어는 공통 인터페이스를 갖춘 확장 가능한 문자열 접두사를 제공합니다. 이는 일반적인 상황에서 코드를 일반화하고 코드 줄 수를 줄이는 방법이 될 수 있습니다. 그 예는 ES6(Javascript), Scala, Nim, 그리고 (좀 더 제한적으로) C#에서 찾아볼 수 있습니다. 이는 BDFL이 거부했습니다. [14]

입력 변수의 자동 이스케이프

일부 경우에는 도움이 되지만, 문자열 표현식을 언제 어디서 안전하게 사용할 수 있는지에 대해 지나친 불확실성을 만든다고 여겨졌습니다. 또한 이 개념은 다른 사람에게 설명하기도 어려웠습니다. [12]

개발자가 명시적으로 이스케이프하지 않은 한, 형식 문자열 변수는 항상 이스케이프되지 않은 것으로 간주하십시오.

환경 접근과 명령 치환

시스템 프로그래밍과 셸 스크립트 대체 용도로는, 표현식 문자열 안에서 환경 변수를 다루고 명령의 출력을 직접 캡처할 수 있으면 유용할 것입니다. 이는 충분히 중요하지 않고, bash/perl과 너무 비슷해 보여 나쁜 습관을 부추길 수 있다는 이유로 거부되었습니다. [13]

감사의 글

  • PEP 498을 작성하고 구현해 주신 Eric V. Smith께 감사드립니다.
  • 제기된 여러 엉뚱한 아이디어를 거부하여 최종 설계가 초점을 잃지 않도록 도와주신 python-ideas 메일링 리스트의 모든 분께 감사드립니다.

참고 문헌