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 ...
결과적으로 우리는 둘 다 틀렸습니다. 진정한 답은 파이썬 클래스 속성과 파이썬 인스턴스 속성의 차이점을 이해하는 데 있습니다.
참고: 클래스 속성에 대한 전문가 핸들이 있는 경우 사용 사례로 건너뛸 수 있습니다.
파이썬 클래스 속성
위의 코드 가 구문적으로 유효하다는 점에서 면접관이 틀렸습니다.
저 역시 인스턴스 속성에 '기본값'을 설정하지 않았다는 점에서 틀렸습니다. 대신 값이 []
인 클래스 속성으로 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
를 재정의하게 되므로 빈 목록을 기본값으로 사용하면 쉽게 간과되는 작은 버그가 발생합니다. 위 대신 다음 중 하나를 수행할 수 있습니다.
- 소개에서 설명한 것처럼 인스턴스 속성에 완전히 고정됩니다.
빈 목록(변경 가능한 값)을 "기본값"으로 사용하지 않습니다.
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
물론
None
경우를 적절하게 처리해야 하지만 지불해야 하는 비용은 적습니다.
그렇다면 언제 Python 클래스 속성을 사용해야 할까요?
클래스 속성은 까다롭지만 유용할 때 몇 가지 경우를 살펴보겠습니다.
상수 저장 . 클래스 속성은 클래스 자체의 속성으로 액세스할 수 있으므로 클래스 전체의 클래스별 상수를 저장하는 데 사용하는 것이 좋습니다. 예를 들어:
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
기본값 정의 . 간단한 예로, 제한된 목록(즉, 특정 수 이하의 요소만 포함할 수 있는 목록)을 만들고 기본 상한선을 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
은 인스턴스 변수여야 합니다. (단, 변경 가능한 값을 기본값으로 사용할 때 주의하십시오.)주어진 클래스의 모든 인스턴스에서 모든 데이터를 추적합니다 . 이것은 일종의 구체적이지만 주어진 클래스의 모든 기존 인스턴스와 관련된 데이터에 액세스하려는 시나리오를 볼 수 있습니다.
시나리오를 보다 구체적으로 만들기 위해
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>]
성능 (일종의... 아래 참조).
후드
참고: 이 수준에서 성능이 걱정된다면 처음부터 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을 사용하여 각 시도에서 설정 코드를 다시 실행할 수 있는 방법이 없으므로 시도에서 변수를 다시 초기화해야 합니다. 두 번째 줄은 이전에 계산된 초기화 시간을 뺀 위의 시간을 나타냅니다.
위에서 보면 할당을 처리하는 데 Foo
가 Bar
의 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에 있는 것처럼 가정된 공개-개인 구분을 사용하여 코드를 작성하라는 초대로 간주되어서는 안 됩니다.