스코핑 규칙과 트릭

Beginner

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

소개

이 랩에서는 Python 의 스코핑 규칙에 대해 배우고 스코프를 다루는 고급 기술을 탐구합니다. Python 에서 스코프를 이해하는 것은 깔끔하고 유지보수가 용이한 코드를 작성하는 데 매우 중요하며, 예상치 못한 동작을 방지하는 데 도움이 됩니다.

이 랩의 목표는 Python 의 스코핑 규칙을 자세히 이해하고, 클래스 초기화를 위한 실용적인 스코핑 기술을 배우고, 유연한 객체 초기화 시스템을 구현하며, 코드 단순화를 위해 프레임 검사 기술을 적용하는 것입니다. structure.pystock.py 파일을 사용하게 됩니다.

클래스 초기화 문제 이해

프로그래밍 세계에서 클래스는 사용자 정의 데이터 타입을 생성할 수 있게 해주는 기본적인 개념입니다. 이전 연습에서 Structure 클래스를 생성했을 수 있습니다. 이 클래스는 데이터 구조를 쉽게 정의하는 데 유용한 도구 역할을 합니다. 데이터 구조는 데이터를 효율적으로 접근하고 사용할 수 있도록 데이터를 구성하고 저장하는 방법입니다. 기본 클래스인 Structure 클래스는 미리 정의된 필드 이름 목록을 기반으로 속성을 초기화하는 것을 처리합니다. 속성은 객체에 속하는 변수이며, 필드 이름은 이러한 속성에 부여하는 이름입니다.

Structure 클래스의 현재 구현을 자세히 살펴보겠습니다. 이를 위해 코드 편집기에서 structure.py 파일을 열어야 합니다. 이 파일에는 Structure 클래스에 대한 코드가 포함되어 있습니다. 프로젝트 디렉토리로 이동하여 파일을 여는 명령은 다음과 같습니다.

cd ~/project
code structure.py

Structure 클래스는 간단한 데이터 구조를 정의하기 위한 기본 프레임워크를 제공합니다. Stock 클래스와 같은 서브클래스를 생성할 때 해당 서브클래스에 원하는 특정 필드를 정의할 수 있습니다. 서브클래스는 기본 클래스 (이 경우 Structure 클래스) 의 속성과 메서드를 상속합니다. 예를 들어, Stock 클래스에서 name, shares, price 필드를 정의합니다.

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

이제 stock.py 파일을 열어 전체 코드 컨텍스트에서 Stock 클래스가 어떻게 구현되는지 살펴보겠습니다. 이 파일에는 Stock 클래스를 사용하고 상호 작용하는 코드가 포함되어 있을 것입니다. 다음 명령을 사용하여 파일을 엽니다.

code stock.py

Structure 클래스와 서브클래스를 사용하는 이 접근 방식은 작동하지만 몇 가지 제한 사항이 있습니다. 이러한 문제를 식별하기 위해 Python 인터프리터를 실행하고 Stock 클래스가 어떻게 동작하는지 살펴보겠습니다. 다음 명령은 Stock 클래스를 가져와 도움말 정보를 표시합니다.

python3 -c "from stock import Stock; help(Stock)"

이 명령을 실행하면 도움말 출력에 표시된 시그니처가 그다지 유용하지 않다는 것을 알 수 있습니다. name, shares, price와 같은 실제 매개변수 이름을 표시하는 대신 *args만 표시합니다. 명확한 매개변수 이름이 없으면 사용자가 Stock 클래스의 인스턴스를 올바르게 생성하는 방법을 이해하기 어렵습니다.

키워드 인수를 사용하여 Stock 인스턴스를 생성해 보겠습니다. 키워드 인수를 사용하면 매개변수의 값을 이름으로 지정할 수 있으므로 코드를 더 읽기 쉽게 만들 수 있습니다. 다음 명령을 실행합니다.

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

다음과 같은 오류 메시지가 표시됩니다.

TypeError: __init__() got an unexpected keyword argument 'name'

이 오류는 Stock 클래스의 객체를 초기화하는 역할을 하는 현재 __init__ 메서드가 키워드 인수를 처리하지 않기 때문에 발생합니다. 위치 인수만 허용하므로 매개변수 이름을 사용하지 않고 특정 순서로 값을 제공해야 합니다. 이것이 이 랩에서 해결하고자 하는 제한 사항입니다.

이 랩에서는 Structure 클래스를 더 유연하고 사용자 친화적으로 만들기 위한 다양한 접근 방식을 탐구합니다. 그렇게 함으로써 Stock 클래스 및 Structure의 다른 서브클래스의 사용성을 향상시킬 수 있습니다.

locals() 를 사용하여 함수 인수 접근하기

Python 에서 변수 스코프를 이해하는 것은 매우 중요합니다. 변수의 스코프는 코드 내에서 해당 변수에 접근할 수 있는 위치를 결정합니다. Python 은 스코핑을 이해하는 데 매우 유용한 locals()라는 내장 함수를 제공합니다. locals() 함수는 현재 스코프의 모든 지역 변수를 포함하는 딕셔너리를 반환합니다. 이는 함수 인수를 검사하려는 경우, 코드의 특정 부분 내에서 어떤 변수를 사용할 수 있는지 명확하게 보여주므로 매우 유용할 수 있습니다.

이것이 어떻게 작동하는지 확인하기 위해 Python 인터프리터에서 간단한 실험을 해보겠습니다. 먼저, 프로젝트 디렉토리로 이동하여 Python 인터프리터를 시작해야 합니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다.

cd ~/project
python3

Python 대화형 셸에 들어가면 Stock 클래스를 정의합니다. Python 에서 클래스는 객체를 생성하기 위한 청사진과 같습니다. 이 클래스에서는 특수 __init__ 메서드를 사용합니다. __init__ 메서드는 Python 의 생성자이며, 클래스의 객체가 생성될 때 자동으로 호출됩니다. 이 __init__ 메서드 내에서 locals() 함수를 사용하여 모든 지역 변수를 출력합니다.

class Stock:
    def __init__(self, name, shares, price):
        print(locals())

이제 이 Stock 클래스의 인스턴스를 생성해 보겠습니다. 인스턴스는 클래스 청사진에서 생성된 실제 객체입니다. name, shares, price 매개변수에 대한 값을 전달합니다.

s = Stock('GOOG', 100, 490.1)

이 코드를 실행하면 다음과 유사한 출력이 표시됩니다.

{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}

이 출력은 locals()__init__ 메서드의 모든 지역 변수를 포함하는 딕셔너리를 제공한다는 것을 보여줍니다. self 참조는 클래스 자체의 인스턴스를 참조하는 Python 클래스의 특수 변수입니다. 다른 변수는 Stock 객체를 생성할 때 전달한 매개변수 값입니다.

locals() 기능을 사용하여 객체 속성을 자동으로 초기화할 수 있습니다. 속성은 객체와 관련된 변수입니다. 헬퍼 함수를 정의하고 Stock 클래스를 수정해 보겠습니다.

def _init(locs):
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

class Stock:
    def __init__(self, name, shares, price):
        _init(locals())

_init 함수는 locals()에서 얻은 지역 변수의 딕셔너리를 사용합니다. 먼저 pop 메서드를 사용하여 딕셔너리에서 self 참조를 제거합니다. 그런 다음 딕셔너리의 나머지 키 - 값 쌍을 반복하고 setattr 함수를 사용하여 각 변수를 객체의 속성으로 설정합니다.

이제 위치 인수와 키워드 인수를 모두 사용하여 이 구현을 테스트해 보겠습니다. 위치 인수는 함수 시그니처에 정의된 순서대로 전달되는 반면, 키워드 인수는 매개변수 이름과 함께 전달됩니다.

## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)

## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)

이제 두 가지 방법 모두 작동해야 합니다! _init 함수를 사용하면 위치 인수와 키워드 인수를 원활하게 처리할 수 있습니다. 또한 함수 시그니처에서 매개변수 이름을 유지하므로 help() 출력의 유용성이 향상됩니다. Python 의 help() 함수는 함수, 클래스 및 모듈에 대한 정보를 제공하며, 매개변수 이름을 그대로 유지하면 이 정보가 더 의미 있게 됩니다.

실험을 마쳤으면 다음 명령을 실행하여 Python 인터프리터를 종료할 수 있습니다.

exit()

스택 프레임 검사 탐구

우리가 사용해 온 _init(locals()) 접근 방식은 기능적이지만 단점이 있습니다. __init__ 메서드를 정의할 때마다 locals()를 명시적으로 호출해야 합니다. 이는 특히 여러 클래스를 처리할 때 다소 번거로워질 수 있습니다. 다행히 스택 프레임 검사를 사용하여 코드를 더 깔끔하고 효율적으로 만들 수 있습니다. 이 기술을 사용하면 locals()를 명시적으로 호출하지 않고도 호출자의 지역 변수에 자동으로 접근할 수 있습니다.

Python 인터프리터에서 이 기술을 탐구해 보겠습니다. 먼저 터미널을 열고 프로젝트 디렉토리로 이동합니다. 그런 다음 Python 인터프리터를 시작합니다. 다음 명령을 실행하여 이 작업을 수행할 수 있습니다.

cd ~/project
python3

이제 Python 인터프리터에 들어왔으므로 sys 모듈을 가져와야 합니다. sys 모듈은 Python 인터프리터에서 사용하거나 유지 관리하는 일부 변수에 대한 접근을 제공합니다. 스택 프레임 정보를 접근하는 데 사용합니다.

import sys

다음으로, 개선된 버전의 _init() 함수를 정의합니다. 이 새 버전은 호출자의 프레임에 직접 접근하여 locals()를 명시적으로 전달할 필요가 없도록 합니다.

def _init():
    ## Get the caller's frame (1 level up in the call stack)
    frame = sys._getframe(1)

    ## Get the local variables from that frame
    locs = frame.f_locals

    ## Extract self and set other variables as attributes
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

이 코드에서 sys._getframe(1)은 호출 함수에 해당하는 프레임 객체를 검색합니다. 인수로 1은 호출 스택에서 한 단계 위를 의미합니다. 프레임 객체가 있으면 frame.f_locals를 사용하여 해당 지역 변수에 접근할 수 있습니다. 이렇게 하면 호출자의 스코프에 있는 모든 지역 변수의 딕셔너리가 제공됩니다. 그런 다음 self 변수를 추출하고 나머지 변수를 self 객체의 속성으로 설정합니다.

이제 이 새로운 _init() 함수를 새로운 버전의 Stock 클래스로 테스트해 보겠습니다.

class Stock:
    def __init__(self, name, shares, price):
        _init()  ## No need to pass locals() anymore!

## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)

## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)

보시다시피, __init__ 메서드는 더 이상 locals()를 명시적으로 전달할 필요가 없습니다. 이렇게 하면 호출자의 관점에서 코드가 더 깔끔하고 읽기 쉬워집니다.

스택 프레임 검사 작동 방식

sys._getframe(1)을 호출하면 Python 은 호출자의 실행 프레임을 나타내는 프레임 객체를 반환합니다. 인수 1은 "현재 프레임에서 한 단계 위"(호출 함수) 를 의미합니다.

프레임 객체에는 실행 컨텍스트에 대한 중요한 정보가 포함되어 있습니다. 여기에는 현재 실행 중인 함수, 해당 함수의 지역 변수 및 현재 실행 중인 줄 번호가 포함됩니다.

frame.f_locals에 접근하면 호출자의 스코프에 있는 모든 지역 변수의 딕셔너리를 얻습니다. 이는 해당 스코프에서 직접 호출된 경우 locals()가 반환하는 것과 유사합니다.

이 기술은 매우 강력하지만 주의해서 사용해야 합니다. 일반적으로 고급 Python 기능으로 간주되며 Python 의 일반적인 스코프 경계를 벗어나기 때문에 다소 "마법적"으로 보일 수 있습니다.

스택 프레임 검사를 실험한 후 다음 명령을 실행하여 Python 인터프리터를 종료할 수 있습니다.

exit()

구조에서 고급 초기화 구현

우리는 방금 함수 인수에 접근하기 위한 두 가지 강력한 기술을 배웠습니다. 이제 이러한 기술을 사용하여 Structure 클래스를 업데이트합니다. 먼저, 왜 이렇게 하는지 이해해 보겠습니다. 이러한 기술은 특히 다양한 유형의 인수를 처리할 때 클래스를 더 유연하고 사용하기 쉽게 만듭니다.

코드 편집기에서 structure.py 파일을 엽니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다. cd 명령은 디렉토리를 프로젝트 폴더로 변경하고, code 명령은 코드 편집기에서 structure.py 파일을 엽니다.

cd ~/project
code structure.py

파일의 내용을 다음 코드로 바꿉니다. 이 코드는 여러 메서드가 있는 Structure 클래스를 정의합니다. 각 부분을 살펴보고 무엇을 하는지 이해해 보겠습니다.

import sys

class Structure:
    _fields = ()

    @staticmethod
    def _init():
        ## Get the caller's frame (the __init__ method that called this)
        frame = sys._getframe(1)

        ## Get the local variables from that frame
        locs = frame.f_locals

        ## Extract self and set other variables as attributes
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({values})'

    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')

다음은 코드에서 수행한 작업입니다.

  1. 이전 __init__() 메서드를 제거했습니다. 서브클래스가 자체 __init__ 메서드를 정의하므로 더 이상 이전 메서드가 필요하지 않습니다.
  2. 새로운 _init() 정적 메서드를 추가했습니다. 이 메서드는 프레임 검사를 사용하여 모든 매개변수를 자동으로 캡처하고 속성으로 설정합니다. 프레임 검사를 통해 호출 메서드의 지역 변수에 접근할 수 있습니다.
  3. __repr__() 메서드를 유지했습니다. 이 메서드는 객체의 멋진 문자열 표현을 제공하며, 이는 디버깅 및 인쇄에 유용합니다.
  4. __setattr__() 메서드를 추가했습니다. 이 메서드는 속성 유효성 검사를 적용하여 유효한 속성만 객체에 설정할 수 있도록 합니다.

이제 Stock 클래스를 업데이트해 보겠습니다. 다음 명령을 사용하여 stock.py 파일을 엽니다.

code stock.py

해당 내용을 다음 코드로 바꿉니다.

from structure import Structure

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

    def __init__(self, name, shares, price):
        self._init()  ## This magically captures and sets all parameters!

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

여기서 주요 변경 사항은 __init__ 메서드가 각 속성을 수동으로 설정하는 대신 이제 self._init()를 호출한다는 것입니다. _init() 메서드는 프레임 검사를 사용하여 모든 매개변수를 자동으로 캡처하고 속성으로 설정합니다. 이렇게 하면 코드가 더 간결해지고 유지 관리가 쉬워집니다.

단위 테스트를 실행하여 구현을 테스트해 보겠습니다. 단위 테스트는 코드가 예상대로 작동하는지 확인하는 데 도움이 됩니다. 터미널에서 다음 명령을 실행합니다.

cd ~/project
python3 teststock.py

이전에는 실패했던 키워드 인수에 대한 테스트를 포함하여 모든 테스트가 통과하는 것을 볼 수 있습니다. 이는 구현이 올바르게 작동하고 있음을 의미합니다.

Stock 클래스에 대한 도움말 문서를 확인해 보겠습니다. 도움말 문서는 클래스 및 해당 메서드에 대한 정보를 제공합니다. 터미널에서 다음 명령을 실행합니다.

python3 -c "from stock import Stock; help(Stock)"

이제 모든 매개변수 이름을 표시하는 __init__ 메서드에 대한 적절한 시그니처를 볼 수 있습니다. 이렇게 하면 다른 개발자가 클래스를 사용하는 방법을 더 쉽게 이해할 수 있습니다.

마지막으로, 키워드 인수가 예상대로 작동하는지 대화형으로 테스트해 보겠습니다. 터미널에서 다음 명령을 실행합니다.

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

지정된 속성으로 Stock 객체가 제대로 생성된 것을 볼 수 있습니다. 이는 클래스 초기화 시스템이 키워드 인수를 지원함을 확인합니다.

이 구현을 통해 다음과 같은 훨씬 더 유연하고 사용자 친화적인 클래스 초기화 시스템을 달성했습니다.

  1. 문서에서 적절한 함수 시그니처를 유지하여 개발자가 클래스를 사용하는 방법을 더 쉽게 이해할 수 있도록 합니다.
  2. 위치 인수와 키워드 인수를 모두 지원하여 객체를 생성할 때 더 많은 유연성을 제공합니다.
  3. 서브클래스에서 최소한의 보일러플레이트 코드를 요구하여 작성해야 하는 코드의 양을 줄입니다.

요약

이 랩에서는 Python 의 스코핑 규칙과 스코프를 처리하기 위한 몇 가지 강력한 기술에 대해 배웠습니다. 먼저, locals() 함수를 사용하여 함수 내의 모든 지역 변수에 접근하는 방법을 탐구했습니다. 둘째, sys._getframe()을 사용하여 호출자의 지역 변수에 접근하기 위해 스택 프레임을 검사하는 방법을 배웠습니다.

또한 이러한 기술을 적용하여 유연한 클래스 초기화 시스템을 만들었습니다. 이 시스템은 함수 매개변수를 객체 속성으로 자동 캡처하고 설정하며, 문서에서 적절한 함수 시그니처를 유지하고, 위치 인수와 키워드 인수를 모두 지원합니다. 이러한 기술은 Python 의 유연성과 인트로스펙션 (introspection) 기능을 보여줍니다. 프레임 검사는 주의해서 사용해야 하는 고급 기술이지만, 적절하게 사용하면 보일러플레이트 코드를 효과적으로 줄일 수 있습니다. 스코핑 규칙과 이러한 고급 기술을 이해하면 더 깔끔하고 유지 관리 가능한 Python 코드를 작성할 수 있는 더 많은 도구를 갖추게 됩니다.