Python 이름의 단일 및 이중 밑줄 II

2024. 1. 29. 20:01python/intermediate

클래스의 이중 선행 밑줄: Python의 이름 맹글링

Python 클래스의 맥락에서 속성 및 메서드 이름에 두 개의 밑줄을 사용하는 또 다른 명명 규칙을 찾을 수 있습니다. Python 응답을 생성하지 않는 단일 선행 밑줄과 달리 이중 선행 밑줄은 Python 용어로 이름 맹글링(name mangling) 으로 알려진 동작을 트리거합니다.

다음 섹션에서는 이름 맹글링이 무엇인지, 어떻게 작동하는지, 파이썬에서 무엇을 목표로 하는지 배우게 됩니다.

이름 맹글링 이해

앞에 두 개의 밑줄을 사용하여 속성이나 메서드의 이름을 지정하면 Python은 이름 앞에 클래스 이름과 하나의 밑줄을 붙여 자동으로 이름을 바꿉니다. 이러한 이름 변경 프로세스를 이름 맹글링(name mangling) 이라고 합니다 .

다음 샘플 클래스는 이것이 어떻게 발생하는지 보여줍니다.

In [ ]:
class SampleClass:
    def __init__(self, attribute):
        self.__attribute = attribute
    def __method(self):
        print(self.__attribute)

sample_instance = SampleClass("Hello!")
vars(sample_instance)

이 클래스에서는 .__attribute와 .__method()는 이름 앞에 두 개의 밑줄이 있습니다. Python은 해당 이름을 각각 ._SampleClass__attribute 및 ._SampleClass__method()로 변경합니다. Python은 자동으로 두 이름 모두에 _SampleClass 접두사를 추가했습니다.

이러한 내부 이름 변경으로 인해 원래 이름을 사용하여 클래스 외부에서 특성이나 메서드에 액세스할 수 없습니다.

In [ ]:
sample_instance.__attribute
In [ ]:
sample_instance.__method()

원래 이름을 사용하려고 .__attribute와 .__method()를 액세스하려고 하면 .AttributeError가 나타납니다. 이는 내부적으로 이러한 속성의 이름을 변경하는 이름 맹글링(name mangling) 때문입니다

Python이 두 개의 밑줄로 시작하는 이름을 변조하더라도 해당 이름에 대한 액세스를 완전히 제한하지는 않습니다. 언제든지 맹글링된 이름에 액세스할 수 있습니다.

In [ ]:
sample_instance._SampleClass__attribute
In [ ]:
sample_instance._SampleClass__method()

잘못된 이름을 사용하여 이름이 손상된 속성이나 메서드에 계속 액세스할 수 있지만 이는 나쁜 습관이므로 코드에서 이를 피해야 합니다. 누군가의 코드에서 이 규칙을 사용하는 이름을 발견하면 코드가 포함 클래스 외부의 이름을 사용하도록 강제하지 마십시오.

상속에서 이름 맹글링 사용

이름 맹글링이 개인 속성을 생성하기 위한 것이라고 주장하는 많은 리소스를 찾을 수 있지만 이 명명 변환은 다른 목표, 즉 상속에서 이름 충돌을 방지하는 것을 추구합니다 . 비공개 멤버를 정의하려면 이전 섹션에서와 마찬가지로 단일 선행 밑줄을 사용해야 합니다.

이름 맹글링은 특정 속성이나 메서드가 하위 클래스에서 실수로 재정의되지 않도록 하려는 경우 특히 유용합니다.

In [ ]:
class A:
    def __init__(self):
        self.__attr = 0

    def __method(self):
        print("A.__attr", self.__attr)

class B(A):
    def __init__(self):
        super().__init__()
        self.__attr = 1  # Doesn't override A.__attr

    def __method(self):  # Doesn't override A.__method()
        print("B.__attr", self.__attr)

속성 및 메서드 이름에 두 개의 밑줄을 사용하면 하위 클래스가 이를 재정의하는 것을 방지할 수 있습니다. 클래스가 하위 클래스로 분류되고 하위 클래스에서 사용하지 않으려는 속성이 있는 경우 이중 선행 밑줄을 사용하여 이름을 지정하는 것이 좋습니다. 비공개 또는 비공개 속성을 생성하기 위해 이름 맹글링을 사용하지 마세요.

Python 이름의 후행 밑줄

Python 코드에서 찾을 수 있는 또 다른 명명 규칙은 단일 후행 밑줄을 사용하는 것입니다. 이 규칙은 Python 키워드 또는 내장 이름과 충돌하는 이름을 사용하려는 경우 유용합니다. 이 상황에서는 뒤에 밑줄을 추가하면 문제를 방지하는 데 도움이 됩니다.

예를 들어, 내장 list데이터 유형을 사용하려고 할 때 다음과 같은 작업을 수행하고 싶은 유혹을 받을 수 있습니다.

In [ ]:
list = [1, 2, 3, 4]
list

이 코드는 작동하지만 list내장 유형용으로 예약된 이름과 충돌합니다. 동일한 대화형 세션을 계속 사용하는 경우 다음과 같은 오류가 발생하면 놀랄 것입니다.

In [ ]:
list("Pythonista")

방금 무슨 일이 일어났나요? 왜 list()생성자를 호출할 수 없나요 ? 이전 코드는 기본 제공 이름을 재정의했으며 이제 이름은 list클래스가 아닌 l ist 목록 개체를 가리킵니다.

충돌하는 이름에 밑줄을 추가하면 이 문제를 해결할 수 있습니다. 아래 코드가 작동하려면 대화형 세션을 다시 시작하거나 del 명령문을 사용하여 현재 네임스페이스에서 사용자 정의 이름을 제거해야 합니다.

In [ ]:
list_ = [1, 2, 3, 4]
list_
In [ ]:
del(list)  # 혹은 대화형 세션을 다시 시작하십시요?

list("Pythonista")
In [ ]:
class Passenger:
    def __init__(self, name, class_, seat):
        self.name = name
        self.class_ = class_
        self.seat = seat

이 코드 조각에서는 후행 밑줄을 사용하여 승객의 클래스를 정의합니다. 이렇게 하지 않으면 classPython은 키워드이므로 오류가 발생하고 코드에서 다른 목적으로 사용할 수 없습니다.

참고: 기계 학습 라이브러리 scikit-learn은 후행 밑줄이 속성이 데이터에서 추정되거나 맞춰졌음을 나타내는 규칙을 사용합니다 . 예를 들어 추정된 계수는 .coef 대신하여 .coef_에 저장됩니다.

후행 밑줄 트릭이 작동하더라도 동의어나 여러 단어로 된 이름을 사용하는 것이 가장 좋습니다. 위의 예에서 인수 및 해당 속성의 이름을 부여할 때 reserved_class 같은 것을 사용하면 코드가 더 좋아 보일 것입니다 .

Python의 던더 이름

Python에서 앞뒤에 이중 밑줄(__)이 있는 이름은 언어 자체에 특별한 의미를 갖습니다. 이러한 이름은 던더 이름 으로 알려져 있으며 , 던더는 더블 언더 스코어 의 약자입니다 . 대부분의 경우 Python은 언어의 데이터 모델 에서 특정 기능을 지원하는 메서드에 대해 던더 이름을 사용합니다 .

Dunder 메소드는 특수 메소드 라고도 하며 일부 비공식적 서클에서는 매직 메소드 라고 합니다 . 왜 마술인가 ? Python은 특정 작업에 대한 응답으로 자동으로 호출하기 때문입니다. 예를 들어, list 객체를 인수로 사용하여 내장 len()함수를 호출하면 Python은 목록의 길이를 검색하기 위해 내부적으로 list.len()을 호출합니다.

일반적으로 dunder 이름은 내부 Python 동작을 지원하기 위해 예약되어 있습니다. 따라서 그러한 이름을 만들어내는 것을 피해야 합니다. 대신 문서화된 던더 이름만 사용해야 합니다. 결국 Python은 언어가 정의하는 특수 메서드만 호출하기 때문에 사용자 정의 dunder 이름을 만드는 것은 실질적인 효과가 없습니다.

일반적으로 사용되는 던더 방법의 몇 가지 예는 다음과 같습니다.

Special MethodDescription
.init() Provides an initializer in Python classes
.call() Makes the instances of a class callable
.str() and .repr() Provide string representations for objects
.iter() and .next() Support the iterator protocol
.len() Supports the len() function

이는 Python이 정의하는 모든 특수 메서드의 샘플일 뿐입니다. 설명에서 결론을 내릴 수 있듯이 이러한 모든 메서드는 특정 Python 기능을 지원합니다. 관련 기능을 지원하도록 사용자 정의 클래스에서 이러한 메소드에 대한 구현을 제공할 수 있습니다.

ShoppingCart설명을 위해 온라인 상점에서 카트를 관리하는 클래스를 만들고 싶다고 가정해 보겠습니다 . len()장바구니에 있는 항목 수를 반환하는 함수를 지원하려면 이 클래스가 필요합니다 . 이러한 지원을 제공할 수 있는 방법은 다음과 같습니다.

In [ ]:
# cart.py

class ShoppingCart:
    def __init__(self):
        self.products = []

    def add_product(self, product):
        self.products.append(product)

    def get_products(self):
        return self.products

    def __len__(self):
        return len(self.products)

수업에서는 list 개체를 사용하여 추가된 제품을 추적합니다. 그런 다음 장바구니에 새 제품을 추가하고 현재 제품 목록을 검색하는 메서드를 구현합니다. .len() 특수 메소드는 len() 내장 함수를 사용하여 .products의 항목 수를 반환합니다.

.len()을 구현하면 ShoppingCart 클래스가 len() 함수도 지원하는지 확인할 수 있습니다.

In [ ]:
from cart import ShoppingCart

cart = ShoppingCart()
cart.add_product("keyboard")
cart.add_product("mouse")
cart.add_product("monitor")

len(cart)

이 코드 조각에서는 장바구니를 만들고 세 가지 항목을 추가합니다. cart인스턴스를 인수 len()로 호출하면 현재 장바구니에 있는 항목 수를 가져옵니다.

마지막으로, Python에는 특별한 메소드가 아닌 특별한 속성과 변수를 가리키는 던더 이름도 있습니다. 가장 일반적으로 사용되는 몇 가지는 다음과 같습니다.

  • __name__가져오기 시스템에서 모듈을 고유하게 식별합니다.
  • __file__모듈이 로드된 파일의 경로를 나타냅니다. 실행 가능한 스크립트와 밀접하게 관련된 일반적인 Python 관용어에서 __name__를 자주 찾을 수 있습니다 . 따라서 실행 가능한 많은 Python 파일에는 다음과 같은 코드 조각이 표시됩니다.
In [ ]:
# script.py : 실행문이 없어 에러가 남

# ...

def main():
    # Implemention...

if __name__ == "__main__":
    main()

name -main 관용구를 사용하면 포함된 파일을 스크립트로 실행할 때 코드를 실행할 수 있지만 모듈로 가져올 때는 실행할 수 없습니다.

Python에서 밑줄의 다른 사용법

지금까지 선행 또는 후행 밑줄과 관련된 일부 Python 명명 규칙에 대해 배웠습니다. 공개 이름과 비공개 이름, 이름 변경 및 던더 이름에 대해 배웠습니다.

이 섹션에서는 Python 이름에 밑줄을 사용하는 다른 사용 사례를 빠르게 살펴보겠습니다. 이러한 사용 사례 중 일부는 다음과 같습니다.

  • REPL 세션의 자리표시자 변수
  • 루프 및 기타 구성의 일회용 변수
  • 구조적 패턴 일치의 와일드카드
  • 명된 튜플 메서드

REPL 세션의 맥락에서 밑줄 문자는 암시적인 역할을 갖습니다. 마지막으로 평가된 표현식 의 결과를 포함하는 특수 변수 로 작동합니다 .

In [ ]:
12 + 30
In [ ]:
_
In [ ]:
pow(4, 2)
In [ ]:
_

이 예에서는 두 가지 다른 표현식을 평가합니다. 표현식은 항상 구체적인 값으로 평가되며, Python은 평가 후 자동으로 _ 변수에 할당합니다. 다른 변수를 사용하는 것처럼 _ 변수에 액세스하고 사용할 수 있습니다 .

In [ ]:
numbers = [1, 2, 3, 4]

len(numbers)
In [ ]:
sum(numbers) / _

이 예에서는 먼저 숫자 목록을 만듭니다. 그런 다음 len()을 호출하여 목록에 있는 값의 개수를 가져옵니다. Python은 이 값을 _변수에 자동으로 저장합니다. 마지막으로 값 목록의 평균을 계산하는 데 _ 을 사용됩니다 .

Throwaway 변수는 Python 이름에 밑줄을 사용하는 또 다른 일반적인 사용 사례입니다. for계산에서 루프 변수를 사용할 필요가 없는 루프 및 컴프리헨션 에서 자주 볼 수 있습니다 .

설명을 위해 5x5 행렬을 나타내는 목록 목록을 작성한다고 가정해 보겠습니다. 모든 행에는 에서 0까지 의 정수가 포함됩니다 4. 이 상황에서는 다음 목록 이해를 사용할 수 있습니다 .

In [ ]:
matrix = [[number for number in range(5)] for _ in range(5)]

matrix

이 예에서 외부 목록 이해는, [... for _ in range(5)], 5개의 목록을 생성합니다. 각 목록은 결과 행렬의 행을 나타냅니다. 루프 변수로 밑줄(_)을 어떻게 사용했는지 확인하세요. 각 행을 값으로 채우는 내부 목록 이해,[number for number in range(5)], 에는 이 변수가 필요하지 않기 때문에 이렇게 합니다 .

구조적 패턴 일치는 Python 버전 3.10 에 도입되었습니다 . 객체를 여러 다른 경우와 비교하기 위해 match...case 구문을 사용합니다. 이러한 명령문은 데이터 구조를 해체하고 개별 요소를 선택하는 데 효과적입니다. Python은 case 명령문 에서 단일 밑줄을 와일드카드로 처리하므로 무엇이든 일치합니다. 객체가 예상한 구조를 가지고 있지 않다는 것을 사용자에게 경고하기 위해 종종 이를 사용합니다.

다음 예에서는 패턴 일치를 사용하여 숫자 목록을 합산하는 재귀 함수를 보여줍니다.

In [ ]:
def sum_list(numbers):
    match numbers:
        case []:
            return 0
        case [int(first) | float(first), *rest]:
            return first + sum_list(rest)
        case _:
            raise ValueError(f"can only sum lists of numbers")

마지막 case문은 _ 와일드카드를 사용하며 numbers는 정수 및 부동 소수점 숫자가 포함된 목록이 아닌 경우 일치합니다. 실제로 시도해 볼 수 있습니다.

In [ ]:
sum_list([1, 2, 3])
In [ ]:
sum_list(["x", "y"])

문자열 목록을 합산하려고 하면 최종 case 트리거되면 ValueError가 발생합니다.

마지막으로, 이전에 네임드 튜플을 사용한 적이 있나요 ? 점 표기법을 사용하여 해당 항목에 액세스할 수 있는 명명된 필드를 제공함으로써 코드의 가독성을 향상시키는 데 도움이 될 수 있습니다. 네임드 튜플의 이상한 특징 중 하나는 공개 인터페이스의 일부 속성과 메소드의 이름 앞에 밑줄이 있다는 것입니다.

In [ ]:
from collections import namedtuple

Point = namedtuple("Point", "x y")

point = Point(2, 4)
point
In [ ]:
dir(point)

명명된 튜플은 tuple에서 상속하는 .count()및 .index() 메서드 외에도, 명명된 튜플은 ._asdict(), ._make() 및 ._replace()의 세 가지 추가 메서드를 제공합니다 . 또한 ._field_defaults과 ._fields의 두 가지 추가 속성아 있습니다 .

강조 표시된 줄에서 볼 수 있듯이 이러한 모든 추가 속성과 메서드에는 이름 앞에 밑줄이 있습니다. 왜? 선행 밑줄은 사용자 정의 필드와의 이름 충돌을 방지합니다. 이 경우에는 실용성이 순수함을 이긴다 는 Python 원칙 에 부합하는 확립된 관례를 깨야 할 강력한 이유가 있습니다 .

설명을 위해 다음과 같은 명명된 튜플이 있다고 가정해 보겠습니다.

In [ ]:
 
In [ ]:
from collections import namedtuple

Car = namedtuple("Car", ["make", "model", "color", "year"])

mustang = Car(make="Ford", model="Mustang", color="Red", year=2022)
mustang.make

이 예에서 문자열 "make"를 필드 이름으로 사용 하고 명명된 튜플에 변형 ._make() 변수 대신 .make() 메서드가 있는 경우 해당 메서드를 사용자 정의 필드로 재정의했을 것입니다.

참고:._field_defaults 및 ._fields와 같은 명명된 튜플의 일부 공개 속성은 이름 충돌을 일으킬 가능성이 덜해 보일 수 있습니다. 그러나 일관성을 위해 동일한 명명 규칙을 사용합니다. 그러나 사용자가 코드로 무엇을 할지 알 수 있는 방법은 없습니다.

namedtuple() 함수는 필드 이름에 밑줄을 붙이려고 하면 예외가 발생한다는 점에 유의하세요 .

In [ ]:
Car = namedtuple("Car", ["make", "_model", "color", "year"])

명명된 튜플의 이러한 동작은 명명된 튜플 API의 일부로 사용 가능한 메서드를 재정의하지 않도록 보장합니다.