Python 클래스 속성: 지나치게 철저한 가이드

게시 됨: 2022-03-11

나는 최근에 공동 작업 텍스트 편집기를 사용하는 전화 화면에서 프로그래밍 인터뷰를 했습니다.

특정 API를 구현하라는 요청을 받았고 Python에서 구현하기로 결정했습니다. 문제 설명을 추상화하여 인스턴스에 일부 data 와 일부 other_data 를 저장한 클래스가 필요하다고 가정해 보겠습니다.

나는 심호흡을 하고 타이핑을 시작했다. 몇 줄 후에 나는 다음과 같은 것을 얻었습니다.

 class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...

면접관이 저를 제지했습니다.

  • 면담자: “저 줄: data = [] . 나는 그것이 유효한 파이썬이라고 생각하지 않습니까?”
  • 나: “확신해요. 인스턴스 속성의 기본값을 설정하는 것뿐입니다.”
  • 면접관: "그 코드는 언제 실행되나요?"
  • 나: “잘 모르겠습니다. 혼란을 피하기 위해 수정하겠습니다.”

참고로 제가 의도한 바를 알려드리기 위해 코드를 수정한 방법은 다음과 같습니다.

 class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...

결과적으로 우리는 둘 다 틀렸습니다. 진정한 답은 파이썬 클래스 속성과 파이썬 인스턴스 속성의 차이점을 이해하는 데 있습니다.

Python 클래스 속성 대 Python 인스턴스 속성

참고: 클래스 속성에 대한 전문가 핸들이 있는 경우 사용 사례로 건너뛸 수 있습니다.

파이썬 클래스 속성

위의 코드 구문적으로 유효하다는 점에서 면접관이 틀렸습니다.

저 역시 인스턴스 속성에 '기본값'을 설정하지 않았다는 점에서 틀렸습니다. 대신 값이 []클래스 속성으로 data 를 정의하고 있습니다.

내 경험상 Python 클래스 속성은 많은 사람들 알고 있지만 완전히 이해하는 사람은 거의 없는 주제입니다.

Python 클래스 변수 대 인스턴스 변수: 차이점은 무엇입니까?

Python 클래스 속성은 클래스 인스턴스 의 속성이 아니라 클래스의 속성(원형, 알고 있음)입니다.

차이점을 설명하기 위해 Python 클래스 예제를 사용하겠습니다. 여기서 class_var 는 클래스 속성이고 i_var 는 인스턴스 속성입니다.

 class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var

클래스의 모든 인스턴스는 class_var 에 액세스할 수 있으며 클래스 자체 의 속성으로 액세스할 수도 있습니다.

 foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1

Java 또는 C++ 프로그래머의 경우 클래스 속성은 정적 멤버와 유사하지만 동일하지는 않습니다. 우리는 그들이 어떻게 다른지 나중에 보게 될 것입니다.

클래스 대 인스턴스 네임스페이스

여기서 무슨 일이 일어나고 있는지 이해하기 위해 Python 네임스페이스 에 대해 간단히 이야기해 보겠습니다.

네임스페이스는 이름에서 객체로의 매핑으로, 다른 네임스페이스의 이름 간에는 관계가 0이라는 속성이 있습니다. 추상화되어 있지만 일반적으로 Python 사전으로 구현됩니다.

컨텍스트에 따라 점 구문(예: object.name_from_objects_namespace ) 또는 지역 변수(예: object_from_namespace )를 사용하여 네임스페이스에 액세스해야 할 수도 있습니다. 구체적인 예:

 class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1

Python 클래스 클래스의 인스턴스는 각각 미리 정의된 속성 MyClass.__dict__instance_of_MyClass.__dict__ 로 표시되는 고유한 고유한 네임스페이스를 가집니다.

클래스의 인스턴스에서 속성에 액세스하려고 하면 먼저 해당 인스턴스 네임스페이스를 확인합니다. 속성을 찾으면 연결된 값을 반환합니다. 그렇지 않은 경우 클래스 네임스페이스 찾고 속성을 반환합니다(있는 경우, 그렇지 않으면 오류 발생). 예를 들어:

 foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1

인스턴스 네임스페이스는 클래스 네임스페이스보다 우위를 차지합니다. 둘 다에 동일한 이름을 가진 속성이 있는 경우 인스턴스 네임스페이스가 먼저 확인되고 해당 값이 반환됩니다. 다음은 속성 조회를 위한 코드(소스)의 단순화된 버전입니다.

 def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]

그리고 시각적 형태로:

시각적 형태의 속성 조회

클래스 속성이 할당을 처리하는 방법

이를 염두에 두고 Python 클래스 속성이 할당을 처리하는 방법을 이해할 수 있습니다.

  • 클래스에 액세스하여 클래스 속성을 설정하면 모든 인스턴스의 값을 재정의합니다. 예를 들어:

     foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2

    네임스페이스 수준에서... 우리는 MyClass.__dict__['class_var'] = 2 를 설정하고 있습니다. (참고: 이것은 정확한 코드가 아닙니다( setattr(MyClass, 'class_var', 2) ) __dict__ 는 직접 할당을 방지하는 변경할 수 없는 래퍼인 dictproxy를 반환하지만 데모를 위해 도움이 됩니다). 그런 다음 foo.class_var 에 액세스할 때 class_var 는 클래스 네임스페이스에 새 값을 가지므로 2가 반환됩니다.

  • Paython 클래스 변수가 인스턴스에 액세스하여 설정되면 해당 인스턴스에 대한 값만 재정의합니다. 이것은 본질적으로 클래스 변수를 재정의하고 직관적으로 해당 인스턴스에만 사용할 수 있는 인스턴스 변수로 바꿉니다. 예를 들어:

     foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1

    네임스페이스 수준에서... 우리는 foo.__dict__class_var 속성을 추가하고 있으므로 foo.class_var 를 조회할 때 2를 반환합니다. 한편, MyClass 의 다른 인스턴스는 인스턴스 네임스페이스에 class_var없으므로 계속해서 class_var 를 찾습니다. MyClass.__dict__ 에 있으므로 1을 반환합니다.

가변성

퀴즈 질문: 클래스 속성에 변경 가능한 유형 이 있으면 어떻게 됩니까? 특정 인스턴스를 통해 클래스 속성에 액세스하여 클래스 속성을 조작(절단?)할 수 있으며 결국 모든 인스턴스가 액세스하는 참조 객체를 조작하게 됩니다(Timothy Wiseman이 지적한 대로).

이것은 예를 통해 가장 잘 설명됩니다. 앞서 정의한 Service 로 돌아가서 클래스 변수를 사용하여 향후 문제가 발생할 수 있는 방법을 살펴보겠습니다.

 class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...

내 목표는 빈 목록( [] )을 data 의 기본값으로 사용하고 Service 의 각 인스턴스가 시간이 지남에 따라 인스턴스별로 변경되는 자체 데이터 를 갖도록 하는 것이었습니다. 그러나 이 경우 다음과 같은 동작이 발생합니다( Service 는 이 예에서 임의적인 other_data 인수를 취한다는 점을 기억하십시오).

 s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]

이것은 좋지 않습니다. 한 인스턴스를 통해 클래스 변수를 변경하면 다른 모든 인스턴스도 변경됩니다!

네임스페이스 수준에서... Service 의 모든 인스턴스는 인스턴스 네임스페이스에 자체 data 속성을 만들지 않고 Service.__dict__ 에 있는 동일한 목록에 액세스하고 수정합니다.

할당을 사용하여 이 문제를 해결할 수 있습니다. 즉, 목록의 변경 가능성을 이용하는 대신 다음과 같이 자체 목록을 갖도록 Service 개체를 할당할 수 있습니다.

 s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]

이 경우 s1.__dict__['data'] = [1] 을 추가하므로 원래 Service.__dict__['data'] 는 변경되지 않은 상태로 유지됩니다.

불행히도 이를 위해서는 Service 사용자가 변수에 대해 잘 알고 있어야 하며 확실히 실수하기 쉽습니다. 어떤 의미에서 우리는 원인보다는 증상을 다룰 것입니다. 우리는 건설에 의해 올바른 것을 선호합니다.

내 개인 솔루션: 클래스 변수를 사용하여 Python 인스턴스 변수에 기본값을 할당하는 경우 변경 가능한 값을 사용하지 마십시오 . 이 경우 Service 의 모든 인스턴스는 결국 자체 인스턴스 속성으로 Service.data 를 재정의하게 되므로 빈 목록을 기본값으로 사용하면 쉽게 간과되는 작은 버그가 발생합니다. 위 대신 다음 중 하나를 수행할 수 있습니다.

  1. 소개에서 설명한 것처럼 인스턴스 속성에 완전히 고정됩니다.
  2. 빈 목록(변경 가능한 값)을 "기본값"으로 사용하지 않습니다.

     class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...

    물론 None 경우를 적절하게 처리해야 하지만 지불해야 하는 비용은 적습니다.

그렇다면 언제 Python 클래스 속성을 사용해야 할까요?

클래스 속성은 까다롭지만 유용할 때 몇 가지 경우를 살펴보겠습니다.

  1. 상수 저장 . 클래스 속성은 클래스 자체의 속성으로 액세스할 수 있으므로 클래스 전체의 클래스별 상수를 저장하는 데 사용하는 것이 좋습니다. 예를 들어:

     class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
  2. 기본값 정의 . 간단한 예로, 제한된 목록(즉, 특정 수 이하의 요소만 포함할 수 있는 목록)을 만들고 기본 상한선을 10개 항목으로 선택할 수 있습니다.

     class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception("Too many elements") self.data.append(e) MyClass.limit ## 10

    그런 다음 인스턴스의 limit 속성에 할당하여 고유한 제한이 있는 인스턴스를 생성할 수도 있습니다.

     foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10

    이것은 MyClass 의 일반적인 인스턴스가 10개 이하의 요소를 보유하기를 원하는 경우에만 의미가 있습니다. 모든 인스턴스에 다른 제한을 부여하는 경우 limit 은 인스턴스 변수여야 합니다. (단, 변경 가능한 값을 기본값으로 사용할 때 주의하십시오.)

  3. 주어진 클래스의 모든 인스턴스에서 모든 데이터를 추적합니다 . 이것은 일종의 구체적이지만 주어진 클래스의 모든 기존 인스턴스와 관련된 데이터에 액세스하려는 시나리오를 볼 수 있습니다.

    시나리오를 보다 구체적으로 만들기 위해 Person 클래스가 있고 모든 사람이 name 을 갖고 있다고 가정해 보겠습니다. 우리는 사용된 모든 이름을 추적하고 싶습니다. 한 가지 접근 방식은 가비지 수집기의 개체 목록을 반복하는 것이지만 클래스 변수를 사용하는 것이 더 간단합니다.

    이 경우 names 은 클래스 변수로만 액세스할 수 있으므로 변경 가능한 기본값이 허용됩니다.

     class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']

    이 디자인 패턴을 사용하여 일부 관련 데이터가 아니라 주어진 클래스의 모든 기존 인스턴스를 추적할 수도 있습니다.

     class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]
  4. 성능 (일종의... 아래 참조).

관련: Toptal 개발자의 Python 모범 사례 및 팁

후드

참고: 이 수준에서 성능이 걱정된다면 처음부터 Python을 사용하고 싶지 않을 수도 있습니다. 차이가 10분의 10분의 1밀리초 정도이기 때문입니다. 삽화를 위해 도움이 됩니다.

클래스의 이름 공간은 클래스 정의 시 생성되고 채워진다는 것을 상기하십시오. 즉, 주어진 클래스 변수에 대해 한 번만 할당하는 반면 인스턴스 변수는 새 인스턴스가 생성될 때마다 할당되어야 합니다. 예를 들어 보겠습니다.

 def called_class(): print "Class assignment" return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## "Class assignment" def called_instance(): print "Instance assignment" return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"

Bar.y 에 한 번만 할당하지만 instance_of_Foo.y__init__ 에 대한 호출마다 할당합니다.

추가 증거로 Python 디스어셈블러를 사용하겠습니다.

 import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE

바이트 코드를 보면 Foo.__init__ 이 두 개의 할당을 수행해야 하는 반면 Bar.__init__ 은 하나만 수행해야 한다는 것이 다시 분명합니다.

실제로 이 이득은 실제로 어떻게 생겼습니까? 나는 타이밍 테스트가 종종 통제할 수 없는 요인에 크게 의존하고 이들 사이의 차이점을 정확하게 설명하기 어려운 경우가 많다는 것을 처음으로 인정할 것입니다.

그러나 이 작은 조각(Python timeit 모듈과 함께 실행)이 클래스 변수와 인스턴스 변수의 차이점을 설명하는 데 도움이 된다고 생각하므로 어쨌든 포함했습니다.

참고: 저는 OS X 10.8.5 및 Python 2.7.2가 설치된 MacBook Pro를 사용하고 있습니다.

초기화

 10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s

Bar 의 초기화는 1초 이상 더 빠르기 때문에 여기에서의 차이는 통계적으로 유의미한 것으로 보입니다.

왜 이런 경우가 있습니까? 하나의 추측 적인 설명: Foo.__init__ 에서 두 개의 할당을 수행하지만 Bar.__init__ 에서 하나만 할당합니다.

과제

 10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s

참고: timeit을 사용하여 각 시도에서 설정 코드를 다시 실행할 수 있는 방법이 없으므로 시도에서 변수를 다시 초기화해야 합니다. 두 번째 줄은 이전에 계산된 초기화 시간을 뺀 위의 시간을 나타냅니다.

위에서 보면 할당을 처리하는 데 FooBar 의 60% 정도밖에 걸리지 않는 것처럼 보입니다.

왜 이런 일이 발생합니까? 하나의 추측 적인 설명: Bar(2).y 에 할당할 때 먼저 인스턴스 네임스페이스( Bar(2).__dict__[y] )를 찾고 y 를 찾지 못한 다음 클래스 네임스페이스( Bar.__dict__[y] ) 그런 다음 적절한 할당을 수행합니다. Foo(2).y 에 할당할 때 인스턴스 네임스페이스( Foo(2).__dict__[y] )에 즉시 할당하는 것보다 절반의 조회를 수행합니다.

요약하면 이러한 성능 향상은 실제로는 중요하지 않지만 이러한 테스트는 개념적 수준에서 흥미롭습니다. 그렇다면 이러한 차이점이 클래스 변수와 인스턴스 변수 간의 기계적 차이를 설명하는 데 도움이 되기를 바랍니다.

결론적으로

클래스 속성은 Python에서 충분히 사용되지 않는 것 같습니다. 많은 프로그래머는 작동 방식과 도움이 되는 이유에 대해 서로 다른 인상을 가지고 있습니다.

내 의견: Python 클래스 변수는 좋은 코드 학교 내에서 제자리를 가집니다. 주의해서 사용하면 사물을 단순화하고 가독성을 높일 수 있습니다. 그러나 주어진 수업에 부주의하게 던져지면 그들은 당신을 걸려 넘어지게 할 것입니다.

부록 : 프라이빗 인스턴스 변수

포함하고 싶었지만 자연스러운 진입점이 없었습니다...

Python에는 말하자면 개인 변수가 없지만 클래스와 인스턴스 이름 지정 간의 또 다른 흥미로운 관계는 이름 맹글링과 함께 제공됩니다.

Python 스타일 가이드에서는 의사 개인 변수 앞에 이중 밑줄 '__'을 붙여야 한다고 합니다. 이것은 다른 사람들에게 당신의 변수가 개인적으로 다루어져야 한다는 신호일 뿐만 아니라 일종의 접근을 방지하는 방법이기도 합니다. 내가 의미하는 바는 다음과 같습니다.

 class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File "<stdin>", line 1, in <module> ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1

보세요: 인스턴스 속성 __zap_Bar__zap 을 생성하기 위해 자동으로 클래스 이름이 접두사로 붙습니다.

a._Bar__zap 을 사용하여 여전히 설정 가능하고 얻을 수 있지만 이 이름 맹글링은 '비공개' 변수를 생성하는 수단입니다. 이는 사용자 다른 사람들이 우연히 또는 무지를 통해 변수에 액세스하는 것을 방지하기 때문입니다.

편집: Pedro Werneck이 친절하게 지적했듯이 이 동작은 주로 하위 분류를 돕기 위한 것입니다. PEP 8 스타일 가이드에서는 (1) 하위 클래스가 특정 속성에 액세스하는 것을 방지하고 (2) 이러한 하위 클래스에서 네임스페이스 충돌을 방지하는 두 가지 목적을 수행하는 것으로 보고 있습니다. 유용하기는 하지만 변수 맹글링은 Java에 있는 것처럼 가정된 공개-개인 구분을 사용하여 코드를 작성하라는 초대로 간주되어서는 안 됩니다.

관련 항목: 고급화: Python 프로그래머가 저지르는 가장 일반적인 실수 10가지 방지