느리거나 복잡한 작업의 경우(특히 i/O를 수행하는 등의 부수 효과가 있는 경우)에는 @property 대신 일반적인 메서드를 사용하라.
파이썬에서는 명시적인 세터나 게터 메서드를 구현할 필요가 전혀 없다.
항상 단순한 공개 attribute로 구현을 시작하라.class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
@property
def voltage(self):
return self._voltage
@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms
r2 = VoltageResistance(1e3)
print(f'이전: {r2.current:.2f} 암페어')
r2.voltage = 10
print(f'이후: {r2.current:.2f} 암페어')
property를 사용하여 특정 속성을 읽기 전용으로 설정할 수 있습니다. 즉, 사용자가 해당 속성의 값을 변경할 수 없도록 할 때 유용합니다.
class BoundedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if ohms <= 0:
raise ValueError(f'저항 > 0이어야 합니다. 실제 값: {ohms}')
self._ohms = ohms
class FixedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if hasattr(self, '_ohms'):
raise AttributeError("Ohms는 불변객체입니다")
self._ohms = ohms
WeakKeyDictionary
를 사용하라.__getattribute__
가 descriptor protocol을 사용해, attribute 값을 읽거나 설정하는 방식을 정확히 이해하라.class Exam:
def __init__(self):
self._writing_grade = 0
self._math_grade = 0
@staticmethod
def _check_grade(value):
if not (0 <= value <= 100):
raise ValueError(
'점수는 0과 100 사이입니다')
@property
def writing_grade(self):
return self._writing_grade
@writing_grade.setter
def writing_grade(self, value):
self._check_grade(value)
self._writing_grade = value
@property
def math_grade(self):
return self._math_grade
@math_grade.setter
def math_grade(self, value):
self._check_grade(value)
self._math_grade = value
__get__
과 __set___
메서드를 제공하고, 이 두 메서드를 사용하면, 별다른 준비 코드 없이도 _checking_grade
와 같은 동작을 재사용할 수 있다.class Grade: # descriptor protocol을 구현함
def __init__(self):
self._value = 0
def __get__(self, instance, instance_type):
return self._value
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(
'점수는 0과 100 사이입니다')
self._value = value
class Exam:
# 클래스 애트리뷰트
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.science_grade = 99
first_exam.writing_grade = 82
>>> [중요, 다음과 같이 작동함] Exam.__dict__['writing_grade'].__set__(exam, 40)
first_exam.writing_grade
>>> [중요, 다음과 같이 작동함] Exam.__dict__['writing_grade'].__get__(exam, Exam)
아래 4줄을 보면, Exam 인스턴스에 writing_grade 라는 이름의 attribute가 없으면, python은 Exam 클래스의 attribute를 대신 사용한다.
이 클래스 attribute가 __get__
과 __set__
메서드가 정의된 객체라면, 파이썬은 descriptor protocol을 따라야 한다고 결정한다.
하지만, 위 코드의 문제점은 Exam 클래스가 처음 정의될 때 1번만 이 attribute에 대한 Grade 인스턴스가 단 한번만 생성된다는 점이다. Exam 인스턴스가 생성될 때마다, 매번 Grade 인스턴스가 생성되지는 않는다.
그래서 아래와 같은 2번째 Exam 인스턴스를 만들면, 문제가 발생한다.
second_exam = Exam()
second_exam.writing_grade = 75
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 틀림; '
f'82점이어야 함')
class Grade:
def __init__(self):
self._values = {}
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(
'점수는 0과 100 사이입니다')
self._values[instance] = value
_values
딕셔너리는 프로그램이 실행되는 동안 __set__
호출에 전달된 모든 Exam 인스턴스에 대한 참조를 저장하고 있다.weakref
내장 모듈의 WeakKeyDictionary
라는 특별한 클래스를 사용하자. from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(
'점수는 0과 100 사이입니다')
self._values[instance] = value
__getattr__
, __getattribute__
, __setattr__
을 사용하라.__getattr__
과 __setattr__
을 사용해, 객체의 attribute를 지연해 가져오거나 저장할 수 있다.__getattr__
은 attribute가 존재하지 않을 때만 호출되지만, __getattribute__
는 attribute를 읽을 때마다 항상 호출된다는 점을 이해하라._getattribute__
와 __setattr__
에서 무한 재귀를 피하려면, super()
에 있는(즉, object 클래스에 있는) 메서드를 사용해 instance attribute에 접근하라.__getattr__
/ __getattribute__
__getattr__
메서드 정의가 있으면, 이 객체의 Instance dictionary에서 찾을 수 없는 attribute에 접근할 때마다 __getattr__
이 호출된다.class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f'{name}의 값'
setattr(self, name, value)
return value
data = LazyRecord()
print('이전:', data.__dict__)
print('foo:', data.foo)
print('이후:', data.__dict__)
>>>
이전: {'exists': 5}
foo: foo를 위한 값
이후: {'exists": 5, 'foo': 'foo를 위한 값'}
__getattribute__
는 attribute를 읽을 때마다 항상 호출된다.class ValidatingRecord:
def __init__(self):
self.exists = 5
def __getattribute__(self, name):
print(f'* 호출: __getattr__({name!r})')
try:
value = super().__getattribute__(name)
print(f'* {name!r} 찾음, {value!r} 반환')
return value
except AttributeError:
value = f'{name}을 위한 값'
print(f'* {name!r}를 {value!r}로 설정')
setattr(self, name, value)
return value
data = ValidatingRecord()
print('exists: ', data.exists)
print('첫 번째 foo: ', data.foo)
print('두 번째 foo: ', data.foo)
존재하지 않는 프로퍼티에 동적으로 접근하는 경우에는 AttributeError 예외가 발생한다.
(__getattr__
과 __getattribute__
에서 존재하지 않는 property를 사용할 때 발생하는 표준적인 예외가 AttributeError)
__setattr__
은 인스턴스의 attribute에(직접 대입하든 setattr 내장 함수를 통해서든) 대입이 이루어질 때마다 항상 호출된다.
__getattr__
이나 __getattribute__
로 값을 읽을 떄와는 달리, 메서드가 2개 있을 필요가 없다.class SavingRecord:
def __setattr__(self, name, value):
# 데이터를 데이터베이스 레코드에 저장한다
super().__setattr__(name, value)
class LoggingSavingRecord(SavingRecord):
def __setattr__(self, name, value):
print(f'* 호출: __setattr__({name!r}, {value!r})')
super().__setattr__(name, value)
data = LoggingSavingRecord()
print('이전: ', data.__dict__)
>>> 이전: {}
data.foo = 5
print('이후: ', data.__dict__)
>>> * 호출: __setattr__('foo', 5)
>>> 이후: {'foo': 5}
data.foo = 7
print('최후:', data.__dict__)
>>> * 호출: __setattr__('foo', 7)
>>> 이후: {'foo': 7}
__getattribute__
/ __setattr__
에서 무한 재귀를 피하려면, super()에 있는 메서드를 사용해 instance attribute에 접근하라)class BrokenDictionaryRecord:
def __init__(self, data):
self._data = {}
def __getattribute__(self, name):
print(f'* 호출: __getattribute__({name!r})')
return self._data[name]
data = Brokedata = BrokenDictionaryRecord({'foo': 3})
data.foo
>>>
* 호출: __getattribute__('foo')
* 호출: __getattribute__('_data')
* 호출: __getattribute__('_data')
* 호출: __getattribute__('_data')
class DictionaryRecord:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f'* 호출: __getattribute__({name!r})')
data_dict = super().__getattribute__('_data')
return data_dict[name]
data = DictionaryRecord({'foo': 3})
print('foo: ', data.foo)