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

Python 개선 제안 한국어 번역

PEP 329 – 표준 라이브러리에서 내장 객체를 상수로 취급하기

Author:
Raymond Hettinger <python at rcn.com>
Status:
Rejected
Type:
Standards Track
Created:
18-Apr-2004
Python-Version:
2.4
Post-History:
18-Apr-2004

Table of Contents

번역·라이선스 안내

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

초록

이 제안은 내장 객체 참조를 상수로 취급하는 함수를 추가하고, 그 함수를 표준 라이브러리 전체에 적용하자는 것입니다.

상태

이 PEP는 작성자가 스스로 거부했습니다. ASPN 레시피는 좋은 반응을 얻었지만, 이를 핵심 배포판에 포함하는 것을 고려하려는 의지는 그보다 적었습니다.

Jython 구현은 바이트코드를 사용하지 않으므로, 현재의 _len=len 최적화를 제거하면 성능이 저하될 것입니다.

또한 바이트코드를 변경하는 것은 성능을 개선하고 더 깔끔한 코딩을 가능하게 하는 방법 가운데 가장 깔끔하지 못한 방법 중 하나입니다. 더 견고한 해결책은 아마도 무엇을 최적화할 수 있는지 나타내는 컴파일러 pragma 지시문이나 메타변수(const/volatile 선언과 유사한 것)를 사용하는 방식이 될 것입니다.

동기

라이브러리에는 느린 전역 조회 대신 빠른 지역 참조를 만들기 위한 _len=len 같은 코드가 들어 있습니다. 이러한 구문은 성능을 위해 필요하기는 하지만 코드를 어지럽히며, 대개 불완전합니다(많은 기회를 놓치고 있습니다).

이 제안이 채택되면 그러한 구문을 코드 베이스에서 제거할 수 있으며, 동시에 성능 면에서도 그 결과를 개선할 수 있습니다.

현재 라이브러리에는 while 1이 백 개가 넘게 있습니다. 이것들은 성능상의 이유로 더 읽기 쉬운 while True로 바뀌지 않았습니다(True가 항상 상수라는 것을 알 수 없으므로 컴파일러가 검사를 제거할 수 없습니다). True를 상수로 변환하면 성능을 유지하면서도 코드가 명확해질 것입니다.

다른 많은 기본 Python 연산도 전역 조회 때문에 훨씬 느리게 실행됩니다. try/except 문에서는 포착할 예외가 일치하는지 검사하기 전에 동적으로 조회됩니다. 마찬가지로 while x is not None 같은 단순한 동일성 검사도 매번 반복할 때마다 None 변수를 다시 조회해야 합니다. 내장 객체 조회는 둘러싼 전역 스코프를 먼저 검사해야 하므로 특히 심각합니다. 이러한 조회 체인은 다른 곳에 쓰는 편이 더 나은 캐시 공간을 잡아먹습니다.

요컨대, 이 제안이 채택되면 코드가 더 깔끔해지고 전반적으로 성능이 향상될 것입니다.

제안

bind_constants()와 bind_all() 두 함수를 포함하는 codetweaks.py라는 모듈을 추가하십시오. 첫 번째 함수는 상수 바인딩을 수행하고, 두 번째 함수는 대상 모듈의 모든 함수와 클래스에 이를 재귀적으로 적용합니다.

표준 라이브러리의 대부분의 모듈에서는 스크립트 끝부분에 다음 두 줄을 추가하십시오:

import codetweaks, sys
codetweaks.bind_all(sys.modules[__name__])

내장 객체를 바인딩하는 것 외에도, (sre_compile 같은) 일부 모듈에서는 내장 객체뿐 아니라 모듈 변수도 상수로 바인딩하는 것이 타당합니다.

질문과 답변

  1. 이것 때문에 모든 사람이 최적화 문제에 주의를 돌리게 되지 않겠습니까?

    자동으로 이루어지므로, 오히려 최적화에 대해 생각할 필요가 줄어듭니다.

  2. 간단히 말해, 어떻게 동작합니까?

    모든 함수에는 자신의 바이트코드(Python 가상 머신의 언어)와 상수 테이블을 담은 속성이 있습니다. bind 함수는 바이트코드에서 LOAD_GLOBAL 명령어를 찾아 그 값이 이미 알려져 있는지 확인합니다. 알려져 있다면 그 값을 상수 테이블에 추가하고 옵코드를 LOAD_CONSTANT로 바꿉니다.

  3. 언제 동작합니까?

    모듈이 처음 임포트될 때, Python은 바이트코드를 컴파일하고 바인딩 최적화를 실행합니다. 이후의 임포트는 이전 작업을 재사용할 뿐입니다. 세션마다 이 과정을 반복합니다(결과는 pyc 파일에 저장되지 않습니다).

  4. 이것이 동작한다는 것을 어떻게 압니까?

    직접 구현하여 라이브러리의 모든 모듈에 적용했고, 테스트 스위트가 예외 없이 실행되었습니다.

  5. 모듈이 내장 이름을 가리는 변수를 정의하면 어떻게 됩니까?

    실제로 그런 일이 있습니다. 예를 들어, True는 모듈 수준에서 True = (1==1)처럼 재정의될 수 있습니다. 아래의 예시 구현은 이렇게 가려지는 경우를 감지하고 전역 조회를 변경하지 않은 채로 둡니다.

  6. 대부분의 전역 조회가 절대 바뀌지 않는 값에 대한 것임을 알아차린 사람이 당신이 처음입니까?

    아닙니다. 이는 오래전부터 알려져 있었습니다. Skip Montanaro가 PEP 266에서 훌륭하게 설명하고 있습니다.

  7. 내장 모듈을 교체하고 나만의 구현을 제공하고 싶다면 어떻게 해야 합니까?

    모듈을 임포트하기 전에 교체하거나, 모듈을 다시 로드하거나, codetweaks.py를 비활성화하십시오(비활성화 플래그가 제공될 것입니다).

  8. 이 모듈은 Python 바이트코드 방식의 변경에 얼마나 영향을 받습니까?

    번호 재배정에 대비하기 위해 opcode.py를 임포트합니다. 또한, 기본적이면서 아주 오래전부터 존재해 온 LOAD_CONST와 LOAD_GLOBAL을 사용합니다. 그렇기는 하지만 코딩 방식은 바뀔 수 있으며, 그 경우 마찬가지로 현재 코딩 방식에 의존하는 dis 같은 모듈과 함께 이 구현도 바뀌어야 할 것입니다.

  9. 시작 시간에는 어떤 영향이 있습니까?

    차이를 측정할 수 없었습니다. warnings.py를 제외하면 시작 모듈 중 어느 것도 바인딩되지 않습니다. 또한, 바인딩 함수는 LOAD_GLOBAL 옵코드를 찾기 위해 코드 문자열을 한 번만 훑으므로 매우 빠릅니다.

샘플 구현

다음은 codetweaks.py의 예시 구현입니다.:

from types import ClassType, FunctionType
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG
LOAD_GLOBAL, LOAD_CONST = opmap['LOAD_GLOBAL'], opmap['LOAD_CONST']
ABORT_CODES = (EXTENDED_ARG, opmap['STORE_GLOBAL'])

def bind_constants(f, builtin_only=False, stoplist=[], verbose=False):
    """ Return a new function with optimized global references.

    Replaces global references with their currently defined values.
    If not defined, the dynamic (runtime) global lookup is left undisturbed.
    If builtin_only is True, then only builtins are optimized.
    Variable names in the stoplist are also left undisturbed.
    If verbose is True, prints each substitution as is occurs.

    """
    import __builtin__
    env = vars(__builtin__).copy()
    stoplist = dict.fromkeys(stoplist)
    if builtin_only:
        stoplist.update(f.func_globals)
    else:
        env.update(f.func_globals)

    co = f.func_code
    newcode = map(ord, co.co_code)
    newconsts = list(co.co_consts)
    codelen = len(newcode)

    i = 0
    while i < codelen:
        opcode = newcode[i]
        if opcode in ABORT_CODES:
            return f    # for simplicity, only optimize common cases
        if opcode == LOAD_GLOBAL:
            oparg = newcode[i+1] + (newcode[i+2] << 8)
            name = co.co_names[oparg]
            if name in env and name not in stoplist:
                value = env[name]
                try:
                    pos = newconsts.index(value)
                except ValueError:
                    pos = len(newconsts)
                    newconsts.append(value)
                newcode[i] = LOAD_CONST
                newcode[i+1] = pos & 0xFF
                newcode[i+2] = pos >> 8
                if verbose:
                    print name, '-->', value
        i += 1
        if opcode >= HAVE_ARGUMENT:
            i += 2

    codestr = ''.join(map(chr, newcode))
    codeobj = type(co)(co.co_argcount, co.co_nlocals, co.co_stacksize,
                    co.co_flags, codestr, tuple(newconsts), co.co_names,
                    co.co_varnames, co.co_filename, co.co_name,
                    co.co_firstlineno, co.co_lnotab, co.co_freevars,
                    co.co_cellvars)
    return type(f)(codeobj, f.func_globals, f.func_name, f.func_defaults,
                    f.func_closure)


def bind_all(mc, builtin_only=False, stoplist=[], verbose=False):
    """Recursively apply bind_constants() to functions in a module or class.

    Use as the last line of the module (after everything is defined, but
    before test code).

    In modules that need modifiable globals, set builtin_only to True.

    """
    for k, v in vars(mc).items():
        if type(v) is FunctionType:
            newv = bind_constants(v, builtin_only, stoplist, verbose)
            setattr(mc, k, newv)
        elif type(v) in (type, ClassType):
            bind_all(v, builtin_only, stoplist, verbose)


def f(): pass
try:
    f.func_code.code
except AttributeError:                  # detect non-CPython environments
    bind_all = lambda *args, **kwds: 0
del f

import sys
bind_all(sys.modules[__name__])         # Optimizer, optimize thyself!

바이트코드가 없는 비 CPython 환경을 자동으로 감지한다는 점에 유의하십시오 [2]. 그런 상황에서는 bind 함수가 원래 함수를 변경 없이 그대로 반환합니다. 이로써 라이브러리 모듈에 두 줄을 추가하더라도 다른 구현에는 영향을 주지 않음이 보장됩니다.

최종 코드에는 바인딩을 쉽게 비활성화할 수 있는 플래그를 추가해야 합니다.

참고 문헌

[1] 비공개가 아닌 구현을 위한 ASPN 레시피 https://code.activestate.com/recipes/277940/