[전문가를 위한 파이썬] 10장 시퀀스 해킹, 해시, 슬라이스
10.1. Vector: 사용자 정의 시퀀스형
- 상속이 아니라 구성을 이용해서 벡터를 구현하고자 함.
- 요소들을 실수형 배열에 저장하고, 벡터가 불변 균일 시퀀스처럼 작동하게 만들기 위한 메서드들을 구현할 것임.
10.2. Vector 버전 #1: Vector2d 호환
첫 번째 버전
from array import array
import reprlib
import math
class Vector:
typecode = 'd'
def __init__(self, components):
self._combpnents = array(self.typecode, components) # '보호된' 객체 속성인 self._components는 벡터 요소를 배열로 저장한다.
def __iter__(self):
return iter(self._components) # 반복할 수 있도록 self._components에 대한 반복자를 반환한다.
def __repr__(self):
components = reprlib.repr(self._components) # 제한된 길이로 표현하기 위해 reprlib.repr()를 사용
components = components[components.find('['):-1] # 문자열을 Vector 생성자에 전달할 수 있도록 앞에 나오는 문자열 'array ('d', '와 마지막에 나오는 괄호를 제거)
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self))
def __bytes(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components)) # self._components에서 바로 bytes 객체를 생성한다.
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.sqrt(sum(x * x for x in self)) # 각 요소의 제곱을 합한 뒤 제곱근을 구한다.
def __bool__(self):
return bool(abs(self))
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv)
10.3. 프로토콜과 덕 타이핑
- 객체지향 프로그래밍에서 프로토콜은 문서에만 정의되어 있고, 실제 코드에서는 정의되지 않는 비공식 인터페이스다. 예를 들어 파이썬의 시퀀스 프로토콜은 __len__()과 __getitem__() 메서드를 동반할 뿐이다.
- 해당 클래스의 슈퍼클래스가 무엇인지는 중요하지 않고, 단지 필요한 메서드만 제공하면 된다.
- 시퀀스 프로토콜을 구현한 클래스는 시퀀스처럼 동작하기 때문에 시퀀스인 것이다.
- 이러한 매커니즘을 덕 타이핑이라고 부른다.
10.4. Vector 버전 #2: 슬라이스 가능한 시퀀스
- 객체 안에 들어있는 시퀀스 속성에 위임하면 시퀀스 프로토콜을 구현하기 위한 __len__()과 __getitem__() 메서드를 다음과 같이 구현할 수 있다.
class Vector:
# 중략
def __len__(self):
return len(self._components)
def __getitem__(self, index):
return self._components[index]
10.4.1. 슬라이싱의 작동 방식
class MySeq:
def __getitem__(self, index):
return index # 전달받은 인수를 그대로 반환.
s = MySeq()
print(s[1]) # 인덱스는 하나이며, 새로운 것은 없다.
# 1
print(s[1:4]) #
# slice(1, 4, None)
print(s[1:4:2])
# slice(1, 4, 2)
print(s[1:4:2, 9])
# (slice(1, 4, 2), 9)
print(s[1:4:2, 7:9])
# (slice(1, 4, 2), slice(7, 9, None))
10.4.2. 슬라이스를 인식하는 __getitem__()
- Vector가 시퀀스로 동작하기 위해 필요한 __len__()과 __getitem__() 메서드 구현. __getitem__()이 슬라이싱도 제대로 처리하도록 구현.
def __len__(self):
return len(self._components)
def __getitem__(self, index):
cls = type(self) # 나중에 사용하기 위해 객체의 클래스(즉, Vector)를 가져온다.
if isinstance(index, slice): # index 인수가 슬라이스이면
return cls(self._components[index]) # _components 배열의 슬라이스로부터 Vector 클래스 생성자를 이용해서 Vector 객체를 생성한다.
elif isinstance(index, numbers.Integral): # index 인수가 int 등의 정수형이면
return self._components[index] # _components에서 해당 항복을 가져와 반환한다.
else:
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls)) # 예외 발생
10.5. Vector 버전 #3: 동적 속성 접근
- Vector에 @property 데커레이터를 이용해 읽기 전용 접근을 제공할 수도 있지만, __getattr__() 특별메서드를 이용하면 더욱 깔끔하게 구현할 수 있다.
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self) # 나중에 사용하기 위해 Vector 클래스를 가져온다.
if len(name) == 1: # name이 한 글자이면 shortcut_names 중 하나일 수 있다.
pos = cls.shortcut_names.find(name) # 한 글자 name의 위치를 찾는다.
if 0 <= pos < len(self._components): # position이 범위 안에 있으면 배열 항목을 반환한다.
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}' # 두 개의 검사 과정에 실패하면 AttributeError를 발생시킨다.
raise AttributeError(msg.format(cls, name))
- 그러나 위 구현의 __getattr__()은 shortcut_names에 나열된 '가상 속성'의 값을 가져오기 위해 self._componets 이외의 다른 속성에는 주의를 기울이지 않는다.
- __setattr__() 추가
def __setattr(self, name, value):
cls = type(self)
if len(name) == 1:
if name in cls.shortcut_names:
error = 'readonly attribute {attr_name!r}'
elif name.islower(): # name이 소문자이면 단일 문자 속성명에 대한 일반적인 메시지를 설정한다.
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = '' # 그렇지 않으면 error를 빈 문자열로 설정한다.
if error: # error 안에 어떠한 문자가 들어가 있으면 AttributeError 발생
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) # 에러가 발생하지 않는다면 표준 동작을 위해 슈퍼클래스의 __setattr__() 메서드를 호출한다.
- 주의할 점은, 모든 속성의 설정을 막는 것이 아니라 지원되는 읽기 전용 속성 x, y, z, t와의 혼동을 피하기 위해 단일 소문자로 되어 있는 속성의 설정만 막고 있다는 것이다.
- 객체 동작의 불일치를 피하려면 __getattr__()을 구현할 때 __setattr__()도 함께 구현해야 한다.
- 벡터 요소의 변경을 허용하고 싶은 경우, __setitem__() 메서드를 구현하면 v[0] = 1.1 의 형태로, __setattr__() 메서드를 구현하면 v.x = 1.1로 작성할 수 있다.
10.6. Vector 버전 #4: 해싱 및 더 빠른 ==
- __eq__() 메서드와 함께 __hash__() 메서드를 구현하면 Vector 객체를 해시할 수 있게 된다.
def __eq__(self, other):
# return tuple(self) == tuple(other)
return len(self) == len(other) and all(a==b for a, b in zip(self, other))
def __hash__(self):
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)