Python

[전문가를 위한 파이썬] 10장 시퀀스 해킹, 해시, 슬라이스

나쁜도비 2024. 7. 21. 14:13

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)

 

 

728x90