exec() 로 코드 생성하기

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 의 exec() 함수에 대해 배우게 됩니다. 이 함수는 문자열로 표현된 Python 코드를 동적으로 실행할 수 있게 해줍니다. 런타임에 코드를 생성하고 실행할 수 있게 해주는 강력한 기능으로, 프로그램을 더욱 유연하고 적응력 있게 만들어줍니다.

이 랩의 목표는 exec() 함수의 기본적인 사용법을 배우고, 이를 사용하여 클래스 메서드를 동적으로 생성하며, Python 표준 라이브러리가 exec()를 내부적으로 어떻게 사용하는지 살펴보는 것입니다.

exec() 기본 이해

Python 에서 exec() 함수는 런타임에 동적으로 생성된 코드를 실행할 수 있게 해주는 강력한 도구입니다. 이는 특정 입력 또는 구성에 따라 코드를 즉석에서 생성할 수 있음을 의미하며, 이는 많은 프로그래밍 시나리오에서 매우 유용합니다.

exec() 함수의 기본적인 사용법을 살펴보겠습니다. 이를 위해 Python 셸을 열어보겠습니다. 터미널을 열고 python3를 입력합니다. 이 명령은 Python 인터프리터를 시작하여 Python 코드를 직접 실행할 수 있게 해줍니다.

python3

이제 Python 코드를 문자열로 정의한 다음 exec() 함수를 사용하여 실행해 보겠습니다. 작동 방식은 다음과 같습니다.

>>> code = '''
for i in range(n):
    print(i, end=' ')
'''
>>> n = 10
>>> exec(code)
0 1 2 3 4 5 6 7 8 9

이 예제에서:

  1. 먼저, code라는 문자열을 정의했습니다. 이 문자열에는 Python for-loop 가 포함되어 있습니다. 이 루프는 n번 반복하고 각 반복 숫자를 인쇄하도록 설계되었습니다.
  2. 그런 다음, 변수 n을 정의하고 값 10 을 할당했습니다. 이 변수는 루프에서 range() 함수의 상한으로 사용됩니다.
  3. 그 후, code 문자열을 인수로 사용하여 exec() 함수를 호출했습니다. exec() 함수는 문자열을 가져와 Python 코드로 실행합니다.
  4. 마지막으로, 루프가 실행되어 0 부터 9 까지의 숫자를 인쇄했습니다.

exec() 함수의 진정한 힘은 함수나 메서드와 같은 더 복잡한 코드 구조를 생성하는 데 사용할 때 더욱 분명해집니다. __init__() 메서드를 동적으로 생성하는 더 고급 예제를 시도해 보겠습니다.

>>> class Stock:
...     _fields = ('name', 'shares', 'price')
...
>>> argstr = ','.join(Stock._fields)
>>> code = f'def __init__(self, {argstr}):\n'
>>> for name in Stock._fields:
...     code += f'    self.{name} = {name}\n'
...
>>> print(code)
def __init__(self, name,shares,price):
    self.name = name
    self.shares = shares
    self.price = price

>>> locs = { }
>>> exec(code, locs)
>>> Stock.__init__ = locs['__init__']

>>> ## Now try the class
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s.price
490.1

이 더 복잡한 예제에서:

  1. 먼저, _fields 속성을 가진 Stock 클래스를 정의했습니다. 이 속성은 클래스의 속성 이름을 포함하는 튜플입니다.
  2. 그런 다음, __init__ 메서드에 대한 Python 코드를 나타내는 문자열을 생성했습니다. 이 메서드는 객체의 속성을 초기화하는 데 사용됩니다.
  3. 다음으로, exec() 함수를 사용하여 코드 문자열을 실행했습니다. 또한 빈 딕셔너리 locsexec()에 전달했습니다. 실행 결과로 생성된 함수는 이 딕셔너리에 저장됩니다.
  4. 그 후, 딕셔너리에 저장된 함수를 Stock 클래스의 __init__ 메서드로 할당했습니다.
  5. 마지막으로, Stock 클래스의 인스턴스를 생성하고 객체의 속성에 액세스하여 __init__ 메서드가 올바르게 작동하는지 확인했습니다.

이 예제는 exec() 함수를 사용하여 런타임에 사용 가능한 데이터를 기반으로 메서드를 동적으로 생성하는 방법을 보여줍니다.

동적 init() 메서드 생성

이제 exec() 함수에 대해 배운 내용을 실제 프로그래밍 시나리오에 적용해 보겠습니다. Python 에서 exec() 함수를 사용하면 문자열에 저장된 Python 코드를 실행할 수 있습니다. 이 단계에서는 Structure 클래스를 수정하여 __init__() 메서드를 동적으로 생성합니다. __init__() 메서드는 Python 클래스에서 객체가 인스턴스화될 때 호출되는 특수한 메서드입니다. 이 메서드의 생성은 클래스의 필드 이름 목록을 포함하는 _fields 클래스 변수를 기반으로 합니다.

먼저, 기존의 structure.py 파일을 살펴보겠습니다. 이 파일에는 Structure 클래스와 이를 상속하는 Stock 클래스의 현재 구현이 포함되어 있습니다. 파일의 내용을 보려면 다음 명령을 사용하여 WebIDE 에서 엽니다.

cat /home/labex/project/structure.py

출력에서 현재 구현이 객체의 초기화를 처리하기 위해 수동적인 접근 방식을 사용하고 있음을 알 수 있습니다. 즉, 객체의 속성을 초기화하는 코드가 동적으로 생성되는 대신 명시적으로 작성됩니다.

이제 Structure 클래스를 수정하겠습니다. __init__() 메서드를 동적으로 생성하는 create_init() 클래스 메서드를 추가합니다. 이러한 변경을 하려면 WebIDE 편집기에서 structure.py 파일을 열고 다음 단계를 따르세요.

  1. Structure 클래스에서 기존의 _init()set_fields() 메서드를 제거합니다. 이러한 메서드는 수동 초기화 접근 방식의 일부이며, 동적 접근 방식을 사용할 것이므로 더 이상 필요하지 않습니다.

  2. create_init() 클래스 메서드를 Structure 클래스에 추가합니다. 메서드에 대한 코드는 다음과 같습니다.

@classmethod
def create_init(cls):
    """Dynamically create an __init__ method based on _fields."""
    ## Create argument string from field names
    argstr = ','.join(cls._fields)

    ## Create the function code as a string
    code = f'def __init__(self, {argstr}):\n'
    for name in cls._fields:
        code += f'    self.{name} = {name}\n'

    ## Execute the code and get the generated function
    locs = {}
    exec(code, locs)

    ## Set the function as the __init__ method of the class
    setattr(cls, '__init__', locs['__init__'])

이 메서드에서 먼저 쉼표로 구분된 모든 필드 이름을 포함하는 문자열 argstr을 생성합니다. 이 문자열은 __init__() 메서드의 인수 목록으로 사용됩니다. 그런 다음, __init__() 메서드에 대한 코드를 문자열로 생성합니다. 필드 이름을 반복하고 각 인수를 해당 객체 속성에 할당하는 코드를 추가합니다. 그 후, exec() 함수를 사용하여 코드를 실행하고 생성된 함수를 locs 딕셔너리에 저장합니다. 마지막으로, setattr() 함수를 사용하여 생성된 함수를 클래스의 __init__() 메서드로 설정합니다.

  1. 이 새로운 접근 방식을 사용하도록 Stock 클래스를 수정합니다.
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

여기에서 Stock 클래스에 대한 _fields를 정의한 다음 create_init() 메서드를 호출하여 Stock 클래스에 대한 __init__() 메서드를 생성합니다.

완성된 structure.py 파일은 다음과 같이 표시됩니다.

class Structure:
    ## Restrict attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"No attribute {name}")

    ## String representation for debugging
    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f"{type(self).__name__}({args})"

    @classmethod
    def create_init(cls):
        """Dynamically create an __init__ method based on _fields."""
        ## Create argument string from field names
        argstr = ','.join(cls._fields)

        ## Create the function code as a string
        code = f'def __init__(self, {argstr}):\n'
        for name in cls._fields:
            code += f'    self.{name} = {name}\n'

        ## Execute the code and get the generated function
        locs = {}
        exec(code, locs)

        ## Set the function as the __init__ method of the class
        setattr(cls, '__init__', locs['__init__'])

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

이제 구현이 올바르게 작동하는지 테스트해 보겠습니다. 모든 테스트가 통과하는지 확인하기 위해 단위 테스트 파일을 실행합니다. 다음 명령을 사용합니다.

cd /home/labex/project
python3 -m unittest test_structure.py

구현이 올바르면 모든 테스트가 통과하는 것을 볼 수 있습니다. 이는 동적으로 생성된 __init__() 메서드가 예상대로 작동하고 있음을 의미합니다.

Python 셸에서 클래스를 수동으로 테스트할 수도 있습니다. 방법은 다음과 같습니다.

>>> from structure import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s
Stock('GOOG', 100, 490.1)
>>> s.shares = 50
>>> s.share = 50  ## This should raise an AttributeError
Traceback (most recent call last):
  ...
AttributeError: No attribute share

Python 셸에서 먼저 structure.py 파일에서 Stock 클래스를 가져옵니다. 그런 다음, Stock 클래스의 인스턴스를 생성하고 인쇄합니다. 객체의 shares 속성을 수정할 수도 있습니다. 그러나 _fields 목록에 없는 속성을 설정하려고 하면 AttributeError가 발생해야 합니다.

축하합니다! exec() 함수를 사용하여 클래스 속성을 기반으로 __init__() 메서드를 동적으로 생성하는 데 성공했습니다. 이 접근 방식은 특히 가변적인 수의 속성을 가진 클래스를 처리할 때 코드를 더 유연하고 유지 관리하기 쉽게 만들 수 있습니다.

Python 표준 라이브러리가 exec() 를 사용하는 방법 검토

Python 에서 표준 라이브러리는 다양한 유용한 함수와 모듈을 제공하는 미리 작성된 코드의 강력한 모음입니다. 이러한 함수 중 하나는 exec()로, Python 코드를 동적으로 생성하고 실행하는 데 사용할 수 있습니다. 동적으로 코드를 생성한다는 것은 하드 코딩된 코드를 갖는 대신 프로그램 실행 중에 즉석에서 코드를 생성하는 것을 의미합니다.

collections 모듈의 namedtuple 함수는 exec()를 사용하는 표준 라이브러리의 잘 알려진 예입니다. namedtuple은 속성 이름과 인덱스 모두를 사용하여 해당 요소에 액세스할 수 있는 특수한 종류의 튜플입니다. 완전한 클래스 정의를 작성하지 않고도 간단한 데이터 보관 클래스를 만드는 데 유용한 도구입니다.

namedtuple이 어떻게 작동하고 내부적으로 exec()를 사용하는지 살펴보겠습니다. 먼저, Python 셸을 엽니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다. 이 명령은 Python 코드를 직접 실행할 수 있는 Python 인터프리터를 시작합니다.

python3

이제 namedtuple 함수를 사용하는 방법을 살펴보겠습니다. 다음 코드는 namedtuple을 생성하고 해당 요소에 액세스하는 방법을 보여줍니다.

>>> from collections import namedtuple
>>> Stock = namedtuple('Stock', ['name', 'shares', 'price'])
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s[1]  ## namedtuples also support indexing
100

위의 코드에서 먼저 collections 모듈에서 namedtuple 함수를 가져옵니다. 그런 다음 name, shares, price 필드가 있는 Stock이라는 새 namedtuple 유형을 생성합니다. Stock namedtuple의 인스턴스 s를 생성하고 속성 이름 (s.name, s.shares) 과 인덱스 (s[1]) 를 모두 사용하여 해당 요소에 액세스합니다.

이제 namedtuple이 어떻게 구현되었는지 살펴보겠습니다. inspect 모듈을 사용하여 소스 코드를 볼 수 있습니다. inspect 모듈은 모듈, 클래스, 메서드 등과 같은 라이브 객체에 대한 정보를 얻기 위해 몇 가지 유용한 함수를 제공합니다.

>>> import inspect
>>> from collections import namedtuple
>>> print(inspect.getsource(namedtuple))

이 코드를 실행하면 많은 양의 코드가 출력됩니다. 자세히 살펴보면 namedtupleexec() 함수를 사용하여 클래스를 동적으로 생성한다는 것을 알 수 있습니다. 수행하는 작업은 클래스 정의에 대한 Python 코드를 포함하는 문자열을 구성하는 것입니다. 그런 다음 exec()를 사용하여 이 문자열을 Python 코드로 실행합니다.

이 접근 방식은 namedtuple이 런타임에 사용자 지정 필드 이름으로 클래스를 생성할 수 있으므로 매우 강력합니다. 필드 이름은 namedtuple 함수에 전달하는 인수에 의해 결정됩니다. 이것은 exec()를 사용하여 코드를 동적으로 생성할 수 있는 실제 예입니다.

namedtuple의 구현에 대해 주목해야 할 몇 가지 주요 사항은 다음과 같습니다.

  1. 문자열 형식을 사용하여 클래스 정의를 구성합니다. 문자열 형식은 문자열 템플릿에 값을 삽입하는 방법입니다. namedtuple의 경우 이를 사용하여 올바른 필드 이름으로 클래스 정의를 만듭니다.
  2. 필드 이름의 유효성을 처리합니다. 즉, 제공하는 필드 이름이 유효한 Python 식별자인지 확인합니다. 그렇지 않은 경우 적절한 오류가 발생합니다.
  3. docstring 및 메서드와 같은 추가 기능을 제공합니다. Docstring 은 클래스 또는 함수의 목적과 사용법을 문서화하는 문자열입니다. namedtuple은 생성하는 클래스에 유용한 docstring 및 메서드를 추가합니다.
  4. exec()를 사용하여 생성된 코드를 실행합니다. 이것은 클래스 정의를 포함하는 문자열을 실제 Python 클래스로 바꾸는 핵심 단계입니다.

이 패턴은 create_init() 메서드에서 구현한 것과 유사하지만 더 정교한 수준입니다. namedtuple 구현은 강력하고 사용자 친화적인 인터페이스를 제공하기 위해 더 복잡한 시나리오와 예외 사례를 처리해야 합니다.

요약

이 랩에서는 Python 의 exec() 함수를 사용하여 런타임에 코드를 동적으로 생성하고 실행하는 방법을 배웠습니다. 주요 내용은 문자열 기반 코드 조각을 실행하기 위한 exec()의 기본 사용법, 속성을 기반으로 클래스 메서드를 동적으로 생성하기 위한 고급 사용법, 그리고 namedtuple을 사용한 Python 표준 라이브러리에서의 실제 적용 사례를 포함합니다.

코드를 동적으로 생성하는 기능은 더 유연하고 적응 가능한 프로그램을 가능하게 하는 강력한 기능입니다. 보안 및 가독성 문제로 인해 주의해서 사용해야 하지만, API 생성, 데코레이터 구현 또는 도메인별 언어 구축과 같은 특정 시나리오에서 Python 프로그래머에게 유용한 도구입니다. 런타임 조건에 적응하는 코드를 생성하거나 구성을 기반으로 코드를 생성하는 프레임워크를 구축할 때 이러한 기술을 적용할 수 있습니다.