본문 바로가기

Python study

시퀀스 해킹, 해시, 슬라이스

사용자 정의 시퀀스형 Vector 클래스

class Vector:
    def __init__(self, *args):
        self._values = list(args)
    
    def __len__(self):
        return len(self._values)
    
    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")
    
    def __setitem__(self, index, value):
        self._values[index] = value
    
    def __iter__(self):
        return iter(self._values)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values
    
    def __repr__(self):
        return f"Vector({', '.join(map(str, self._values))})"
    
    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])
    
    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])
    
    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])
    
    def __rmul__(self, scalar):
        return self.__mul__(scalar)
    

v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)
print(v1 + v2) # Vector(5, 7, 9)
print(v1 - v2) # Vector(-3, -3, -3)
print(v1 * 3) # Vector(3, 6, 9)
print(3 * v1) # Vector(3, 6, 9)
print(v1[1]) # 2
print(v1[1:3]) # Vector(2, 3)

클래스 설명

  1. 초기화: __init__ 메서드는 가변 인자를 받아 리스트로 저장한다.
  2. 길이: __len__ 메서드는 벡터의 길이를 반환한다.
  3. 인덱싱 및 슬라이싱: __getitem__ 메서드는 인덱스나 슬라이스를 받아 해당 요소나 서브 벡터를 반환한다.
  4. 값 설정: __setitem__ 메서드는 특정 인덱스의 값을 설정한다.
  5. 반복: __iter__ 메서드는 벡터의 요소를 순회할 수 있게 한다.
  6. 등호 비교: __eq__ 메서드는 다른 벡터와의 동등성을 비교한다.
  7. 문자열 표현: __repr__ 메서드는 벡터를 문자열로 표현한다.
  8. 벡터 덧셈: __add__ 메서드는 두 벡터를 더한다.
  9. 벡터 뺄셈: __sub__ 메서드는 두 벡터를 뺀다.
  10. 스칼라 곱셈: __mul__ 메서드는 벡터의 모든 요소에 스칼라 값을 곱한다.
  11. 반대 스칼라 곱셈: __rmul__ 메서드는 스칼라 곱셈의 반대 순서를 처리한다.

 

reprlib

reprlib은 Python 표준 라이브러리 중 하나로, 객체의 축약된 표현을 제공하여 특히 큰 데이터 구조를 처리할 때 유용하다. 이 모듈은 재귀적인 데이터 구조를 다루는 데 유용하며, 무한 반복을 방지하기 위해 출력의 깊이와 길이를 제한할 수 있다.

reprlib 모듈은 기본적으로 매우 큰 또는 깊게 중첩된 데이터 구조를 더 간단하게 표시하기 위해 고안되었다. 이 모듈의 repr() 함수는 객체의 축약된 표현을 생성한다. 특히 reprlib은 기본적인 repr보다 더 제한된 길이로 출력하여 큰 데이터 구조를 다룰 때 유용하다.

위의 Vector 클래스의 __repr__ 메서드에서 reprlib.repr을 사용하여 _values 리스트의 축약된 표현을 생성한다. 이는 매우 큰 벡터도 간결하게 표시되도록 한다. 예를 들어, 매우 큰 벡터가 있을 때, 전체 리스트 대신 일부 요소만 표시하고 나머지는 생략 부호로 나타낸다.

import reprlib

class Vector:
    def __init__(self, *args):
        self._values = list(args)
    
    def __len__(self):
        return len(self._values)
    
    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")
    
    def __setitem__(self, index, value):
        self._values[index] = value
    
    def __iter__(self):
        return iter(self._values)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values
    
    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"
    
    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])
    
    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])
    
    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])
    
    def __rmul__(self, scalar):
        return self.__mul__(scalar)

v1 = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
print(v1) # Vector([1, 2, 3, 4, 5, 6, ...])

 

슬라이싱의 작동 방식

슬라이싱은 파이썬에서 시퀀스 객체(예: 리스트, 문자열, 튜플 등)의 일부를 잘라내는 방법이다. 슬라이싱의 구문은 다음과 같다:

sequence[start:stop:step]

여기서 start, stop, step은 모두 정수 값이며 각각의 의미는 다음과 같다:

  • start: 슬라이스의 시작 인덱스이다. 이 인덱스부터 슬라이싱이 시작된다. 이 값이 생략되면 기본적으로 0이 된다.
  • stop: 슬라이스의 끝 인덱스이다. 이 인덱스는 슬라이스에 포함되지 않는다. 이 값이 생략되면 기본적으로 시퀀스의 끝까지 슬라이싱한다.
  • step: 슬라이스의 간격을 지정한다. 이 값이 생략되면 기본적으로 1이 된다. 음수 값을 사용하면 역순으로 슬라이싱할 수 있다.

슬라이싱 작동 방식의 세부 사항

  1. start 인덱스: 슬라이스가 시작되는 위치를 지정한다. 음수 값을 사용할 수 있으며, 이는 시퀀스의 끝에서부터 카운트된다.
    • 예: lst[-3:]는 리스트의 끝에서 세 번째 요소부터 끝까지를 의미한다.
  2. stop 인덱스: 슬라이스가 끝나는 위치를 지정한다. 이 위치의 요소는 포함되지 않는다. 음수 값을 사용할 수 있으며, 이는 시퀀스의 끝에서부터 카운트된다.
    • 예: lst[:-3]는 리스트의 처음부터 끝에서 세 번째 요소 전까지를 의미한다.
  3. step 인덱스: 슬라이싱할 때 요소를 건너뛰는 간격을 지정한다. 기본값은 1이다. 음수 값을 사용하면 역방향으로 슬라이싱한다.
    • 예: lst[::2]는 리스트의 처음부터 끝까지 2 간격으로 요소를 포함한다.
    • 예: lst[::-1]는 리스트를 역순으로 반환한다.

슬라이싱의 예외 처리

  • 인덱스가 시퀀스의 범위를 벗어나더라도, 슬라이싱은 IndexError를 발생시키지 않는다. 슬라이싱은 자동으로 유효한 범위 내에서 동작한다.
    • 예: lst[:100]은 리스트의 끝까지를 반환합니다.

 

슬라이스를 인식하는 getitem()

__getitem__ 메서드는 Python에서 객체의 특정 요소에 접근할 때 호출되는 메서드이다. 이 메서드는 객체가 인덱싱 또는 슬라이싱을 지원하도록 한다. 여기서 슬라이싱을 인식하고 처리하는 방법에 대해 자세히 설명하겠다.

__getitem__ 메서드의 역할

__getitem__ 메서드는 obj[key] 구문을 사용할 때 호출된다. 여기서 key는 정수 인덱스, 슬라이스 객체, 또는 사용자 정의 객체일 수 있다. 슬라이싱을 지원하기 위해서는 __getitem__ 메서드에서 slice 객체를 처리해야 한다.

슬라이스 객체

슬라이스 객체는 시퀀스에서 부분 시퀀스를 선택할 때 사용하는 내장 객체이다. 슬라이스 객체는 start, stop, step 속성을 가지며, 이를 통해 슬라이스의 범위와 간격을 지정한다.

슬라이스 객체 인식 및 처리

슬라이스 객체를 인식하고 처리하려면 __getitem__ 메서드 내에서 index가 slice 타입인지 확인하고, 적절히 처리한다. 아래는 슬라이싱을 처리하는 __getitem__ 메서드의 예이다.

import reprlib

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            # 슬라이스 객체를 처리
            return Vector(*self._values[index])
        elif isinstance(index, int):
            # 인덱스를 처리
            return self._values[index]
        else:
            # 슬라이스나 인덱스가 아닌 경우 예외 처리
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

v1 = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
print(v1)          # Vector([1, 2, 3, 4, 5, 6, ...])
print(v1[2:7])     # Vector([3, 4, 5, 6, 7])
print(v1[::-1])    # Vector([10, 9, 8, 7, 6, 5, ...])
  1. 슬라이스 객체 확인
    • isinstance(index, slice)를 사용하여 index가 slice 객체인지 확인한다.
    • 슬라이스 객체인 경우, 슬라이스의 start, stop, step 속성을 사용하여 원래 시퀀스의 부분 시퀀스를 가져온다.
  2. 슬라이스 처리
    • self._values[index]를 사용하여 슬라이스를 적용한다. 이 때 index는 slice 객체로서, self._values 리스트에 슬라이스를 적용하여 부분 리스트를 반환한다.
    • 이 부분 리스트를 새로운 Vector 객체로 반환하여, 슬라이스 결과도 Vector 타입이 되도록 한다.
  3. 인덱스 처리
    • index가 정수인 경우, self._values[index]를 사용하여 해당 인덱스의 요소를 반환한다.
  4. 예외 처리
    • index가 slice나 정수가 아닌 경우 TypeError 예외를 발생시킨다.

 

__getattr__()

__getattr__ 메서드를 Vector 클래스에 적용하여 동적으로 속성을 접근하는 예를 설명하겠다. 이 메서드는 요청된 속성이 존재하지 않을 때 호출되며, 이를 통해 동적으로 속성을 생성하거나 기본값을 반환하는 등의 동작을 정의할 수 있다.

__getattr__ 기본 동작

__getattr__ 메서드는 다음과 같은 형태로 정의된다:

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

동적 속성 접근 - __getattr__

  • __getattr__ 메서드는 객체의 속성에 접근할 때, 해당 속성이 존재하지 않으면 호출된다.
  • name 인자는 접근하려는 속성의 이름을 문자열로 받는다.
  • 예를 들어, 벡터의 크기(유클리드 거리)를 동적으로 계산하여 반환하도록 설정했다. 속성 이름이 magnitude일 때, 벡터 요소들의 제곱합의 제곱근을 계산하여 반환한다.
  • 속성이 존재하지 않으면 AttributeError를 발생시킨다.
v = Vector(3, 4)
print(v.magnitude)  # 5.0, 벡터의 크기를 동적으로 계산하여 반환

# print(v.nonexistent)  # AttributeError 발생: 'Vector' object has no attribute 'nonexistent'

주의 사항

  1. 무한 재귀 방지
    • __getattr__ 메서드 내에서 존재하지 않는 속성에 다시 접근하려 하면 무한 재귀에 빠질 수 있다. 예를 들어, self.nonexistent와 같은 접근을 하면 다시 __getattr__이 호출되어 무한 재귀 오류가 발생한다.
    • 이를 방지하려면 __getattr__ 내에서 항상 존재하는 속성에만 접근하거나, 필요한 경우 기본 클래스의 속성 접근 메서드를 명시적으로 호출해야 한다.
  2. 존재하지 않는 속성 접근
    • __getattr__은 오직 존재하지 않는 속성에 접근할 때만 호출되므로, 일반적인 속성 접근은 여전히 빠르게 처리된다.
    • 필요한 경우, __getattr__ 대신 __getattribute__를 사용하여 모든 속성 접근을 제어할 수 있지만, 이는 성능에 영향을 미칠 수 있으며 주의해서 사용해야 한다.
  3. 속성 이름의 일관성
    • 동적으로 생성되는 속성 이름은 일관되고 명확해야 한다. 잘못된 속성 이름을 반환하거나 의도하지 않은 속성을 동적으로 생성하는 경우, 코드의 가독성과 유지보수성이 떨어질 수 있다.

이러한 주의 사항을 고려하여 __getattr__을 적절히 사용하면, 유연하고 동적인 속성 접근을 구현할 수 있다.

 

__getattr__()과 __setattr__()

__getattr__과 __setattr__은 객체 지향 프로그래밍에서 객체의 동작을 동적으로 정의할 때 사용하는 메서드이다. 이 두 메서드를 적절히 구현하지 않으면 객체의 동작이 불일치하거나 예기치 않게 작동할 수 있다. 이를 피하기 위해 두 메서드의 역할과 구현 방법을 이해하는 것이 중요하다.

__getattr__와 __setattr__의 역할

  • __getattr__(self, name): 객체에 요청된 속성이 존재하지 않을 때 호출된다. 이 메서드를 사용하여 동적으로 속성의 값을 반환할 수 있다.
  • __setattr__(self, name, value): 객체의 속성이 설정될 때 호출된다. 이 메서드를 사용하여 동적으로 속성의 값을 설정할 수 있다.

__getattr__와 __setattr__의 상호작용

__getattr__은 존재하지 않는 속성에 접근할 때만 호출되므로, 모든 속성 접근에 관여하지 않는다. 반면 __setattr__은 모든 속성 설정에 관여한다. 따라서, 이 두 메서드를 함께 적절히 구현하지 않으면 객체의 동작이 불일치할 수 있다.

import reprlib

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == '_values':
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"Cannot set attribute '{name}'")

v = Vector(3, 4)
print(v.magnitude)  # 5.0, 동적으로 계산된 속성

# v.magnitude = 10  # AttributeError 발생: Cannot set attribute 'magnitude'
  1. __getattr__ 구현:
    • __getattr__ 메서드는 magnitude 속성을 요청할 때 벡터의 크기를 동적으로 계산하여 반환한다.
    • 존재하지 않는 다른 속성을 요청하면 AttributeError를 발생시킨다.
  2. __setattr__ 구현:
    • __setattr__ 메서드는 속성 설정 시 호출된다.
    • _values 속성은 super().__setattr__을 사용하여 기본 동작을 유지한다. 이는 객체 초기화와 같이 _values 속성을 설정할 때 필요하다.
    • 다른 속성 설정 시도는 AttributeError를 발생시켜 속성 설정을 제한한다.

주의 사항

  1. 무한 재귀 방지:
    • __setattr__ 메서드에서 self.name = value와 같이 직접 속성을 설정하면 다시 __setattr__이 호출되어 무한 재귀에 빠질 수 있다.
    • 이를 방지하려면 super().__setattr__(name, value)를 사용하여 기본 클래스의 __setattr__ 메서드를 호출한다.
  2. 일관된 동작 유지:
    • __getattr__와 __setattr__을 함께 사용하여 동적으로 속성을 정의하거나 제한할 때, 예상치 못한 동작을 방지하기 위해 일관성을 유지해야 한다.
    • 동적으로 생성된 속성을 설정하려는 시도에 대해 명확하게 정의된 동작을 제공해야 한다.
  3. 예외 처리:
    • 속성이 존재하지 않거나 설정이 불가능한 경우 적절한 예외(AttributeError)를 발생시켜 사용자에게 명확한 피드백을 제공한다.

이와 같이 __getattr__과 __setattr__을 적절히 구현하면 객체의 동작을 일관되게 유지하고, 동적 속성 접근과 설정을 효율적으로 관리할 수 있다.

 

hash()메소드

hash() 메서드를 구현하여 Vector 객체의 해시 값을 계산하려면 functools.reduce() 함수를 사용할 수 있다. reduce() 함수는 Python의 내장 함수로, 두 개의 인자를 받는 함수를 반복적으로 적용하여 시퀀스의 요소들을 하나의 값으로 결합한다.

reduce() 함수의 작동 방식

reduce() 함수는 다음과 같이 작동한다:

  1. 초기 값으로 시퀀스의 첫 번째와 두 번째 요소를 함수에 적용한다.
  2. 그 결과를 시퀀스의 세 번째 요소와 함수에 적용한다.
  3. 이 과정을 시퀀스의 끝까지 반복하여 하나의 값을 반환한다.

예를 들어, reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])는 다음과 같은 단계로 작동한다:

  • 1 + 2 -> 3
  • 3 + 3 -> 6
  • 6 + 4 -> 10
  • 10 + 5 -> 15

결과는 15가 된다.

Vector 클래스의 hash() 메서드 구현

이제 reduce()를 사용하여 Vector 클래스의 해시 값을 계산해 보겠다. 해시 값을 계산할 때는 벡터 요소들의 해시 값을 결합하여 하나의 정수 값을 만들어야 한다.

import reprlib
from functools import reduce

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == '_values':
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"Cannot set attribute '{name}'")

    def __hash__(self):
        # 해시 값을 계산할 때 reduce()를 사용한.
        return reduce(lambda x, y: x ^ hash(y), self._values, 0)

v1 = Vector(1, 2, 3)
v2 = Vector(1, 2, 3)
v3 = Vector(4, 5, 6)

print(hash(v1)) # 0 v1과 v2는 같은 해시 값을 가진다.
print(hash(v2)) # 0 v1과 v2는 같은 해시 값을 가진다.
print(hash(v3)) # 7 v3는 다른 해시 값을 가진다.
  1. __hash__ 메서드:
    • reduce(lambda x, y: x ^ hash(y), self._values, 0)를 사용하여 벡터 요소들의 해시 값을 결합한다.
    • lambda x, y: x ^ hash(y)는 XOR 연산을 사용하여 각 요소의 해시 값을 결합한다. XOR 연산은 순서에 관계없이 동일한 결과를 생성하므로, 순열의 순서가 바뀌어도 같은 해시 값을 가지게 된다.
    • 초기 값으로 0을 사용한다.
  2. 주의 사항:
    • __hash__를 구현할 때, __eq__ 메서드도 함께 구현해야 한다. Python에서는 같은 값을 가지는 객체는 같은 해시 값을 가져야 하기 때문이다.
    • 불변 객체에 대해서만 해시 값을 계산할 수 있다. 객체가 변경 가능하면 해시 값이 변할 수 있어, 이는 해시 기반 컬렉션(예: 집합, 딕셔너리)에서 문제를 일으킬 수 있다.

이와 같이 __getattr__과 __setattr__을 적절히 사용하고, hash 메서드를 reduce()를 통해 구현하면, 객체의 일관된 동작을 유지하면서 동적 속성 접근과 해시 계산을 효율적으로 관리할 수 있다.

 

operator 모듈

operator 모듈은 Python 표준 라이브러리의 일부로, 다양한 연산자를 함수 형태로 제공한다. 이 모듈을 사용하면 코드의 가독성을 높이고, 특히 reduce()와 같은 고차 함수와 함께 사용할 때 유용하다.

operator 모듈의 주요 기능

operator 모듈은 기본적인 산술 연산자, 비교 연산자, 비트 연산자 등을 함수로 제공한다. 예를 들어, 덧셈 연산자인 +는 operator.add로, XOR 연산자인 ^는 operator.xor로 사용할 수 있다.

주요 함수는 다음과 같다:

  • operator.add(a, b): 덧셈 (a + b)
  • operator.sub(a, b): 뺄셈 (a - b)
  • operator.mul(a, b): 곱셈 (a * b)
  • operator.truediv(a, b): 나눗셈 (a / b)
  • operator.and_(a, b): 비트 AND (a & b)
  • operator.or_(a, b): 비트 OR (a | b)
  • operator.xor(a, b): 비트 XOR (a ^ b)
  • operator.eq(a, b): 동등 비교 (a == b)
  • operator.ne(a, b): 비동등 비교 (a != b)

Vector 클래스의 hash 메서드에 operator 모듈 적용

reduce() 함수와 operator.xor을 사용하여 Vector 클래스의 해시 값을 계산해 보겠다.

import reprlib
from functools import reduce
import operator

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self._values == other._values

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == '_values':
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"Cannot set attribute '{name}'")

    def __hash__(self):
        # 해시 값을 계산할 때 reduce()와 operator.xor을 사용합니다.
        return reduce(operator.xor, map(hash, self._values), 0)

v1 = Vector(1, 2, 3)
v2 = Vector(1, 2, 3)
v3 = Vector(4, 5, 6)

print(hash(v1))  # v1과 v2는 같은 해시 값을 가집니다.
print(hash(v2))  # v1과 v2는 같은 해시 값을 가집니다.
print(hash(v3))  # v3는 다른 해시 값을 가집니다.
  1. __hash__ 메서드:
    • reduce(operator.xor, map(hash, self._values), 0)를 사용하여 벡터 요소들의 해시 값을 결합한다.
    • map(hash, self._values)는 벡터의 각 요소에 대해 해시 값을 계산한 값을 반환한다.
    • reduce(operator.xor, ...)는 각 요소의 해시 값을 XOR 연산을 통해 결합한다.
    • 초기 값으로 0을 사용한다.
  2. operator 모듈 사용의 장점:
    • 연산자를 함수로 제공하므로 코드의 가독성을 높인다.
    • lambda 표현식을 사용하지 않으므로 코드가 더 명확해진다.

이와 같이 operator 모듈을 사용하여 reduce()를 구현하면 코드의 가독성과 유지보수성을 높일 수 있다. Vector 클래스의 해시 값을 효과적으로 계산할 수 있으며, 이는 객체의 일관된 동작을 보장한다.

 

eq() 메소드

수천 개의 요소를 가진 Vector 객체의 경우, __eq__ 메서드를 최적화하여 효율성을 높이는 것이 중요하다. 현재 구현에서는 두 벡터의 모든 요소를 순차적으로 비교하고 있어, 요소의 수가 많을 경우 성능이 저하될 수 있다. 이를 개선하기 위해 다음과 같은 최적화 기법을 사용할 수 있다:

  1. 길이 비교: 두 벡터의 길이가 다르면 즉시 False를 반환하여 비교 작업을 줄인다.
  2. 해시값 비교: 두 벡터의 해시값을 먼저 비교하여 다르면 즉시 False를 반환한다. 해시값이 같으면 실제 요소를 비교한다.

Vector 클래스의 최적화된 __eq__ 메서드

아래는 위의 최적화 기법을 적용한 Vector 클래스의 __eq__ 메서드이다.

import reprlib
from functools import reduce
import operator

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == '_values':
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"Cannot set attribute '{name}'")

    def __hash__(self):
        # 해시 값을 계산할 때 reduce()와 operator.xor을 사용합니다.
        return reduce(operator.xor, map(hash, self._values), 0)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        # 길이 비교 최적화
        if len(self) != len(other):
            return False
        # 해시값 비교 최적화
        if hash(self) != hash(other):
            return False
        # 모든 요소 비교
        return self._values == other._values

v1 = Vector(1, 2, 3, 4, 5)
v2 = Vector(1, 2, 3, 4, 5)
v3 = Vector(1, 2, 3, 4, 6)

print(v1 == v2)  # True
print(v1 == v3)  # False
  1. 길이 비교:
    • if len(self) != len(other):를 사용하여 두 벡터의 길이를 먼저 비교한다. 길이가 다르면 두 벡터는 다르므로 False를 반환한다.
  2. 해시값 비교:
    • if hash(self) != hash(other):를 사용하여 두 벡터의 해시값을 비교한다. 해시값이 다르면 두 벡터는 다르므로 False를 반환한다.
    • 해시값 비교는 빠른 연산으로, 벡터의 요소가 많을 때 유용하다.
  3. 모든 요소 비교:
    • 길이와 해시값이 동일한 경우에만 실제 요소들을 비교한다. 이는 두 벡터가 동일한지 확인하는 마지막 단계이다.

이 최적화된 __eq__ 메서드를 통해 수천 개의 요소를 가진 Vector 객체의 동등성 비교가 더욱 효율적으로 수행될 수 있다.

 

format함수

Vector 클래스에 format 메서드를 추가하여 벡터를 다양한 형식으로 출력할 수 있게 하겠다. 이를 위해 Python의 __format__ 메서드를 구현하면 된다. __format__ 메서드는 객체를 포맷팅할 때 호출되며, 포맷팅 규칙을 정의할 수 있다.

Vector 클래스의 __format__ 메서드 구현

import reprlib
from functools import reduce
import operator

class Vector:
    def __init__(self, *args):
        self._values = list(args)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Vector(*self._values[index])
        elif isinstance(index, int):
            return self._values[index]
        else:
            raise TypeError("Invalid argument type.")

    def __setitem__(self, index, value):
        self._values[index] = value

    def __iter__(self):
        return iter(self._values)

    def __repr__(self):
        return f"Vector({reprlib.repr(self._values)})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a + b for a, b in zip(self, other)])

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        if len(self) != len(other):
            raise ValueError("Vectors must be the same length.")
        return Vector(*[a - b for a, b in zip(self, other)])

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*[scalar * v for v in self])

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __getattr__(self, name):
        if name == 'magnitude':
            return sum(x**2 for x in self._values) ** 0.5
        raise AttributeError(f"'Vector' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == '_values':
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"Cannot set attribute '{name}'")

    def __hash__(self):
        # 해시 값을 계산할 때 reduce()와 operator.xor을 사용합니다.
        return reduce(operator.xor, map(hash, self._values), 0)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        # 길이 비교 최적화
        if len(self) != len(other):
            return False
        # 해시값 비교 최적화
        if hash(self) != hash(other):
            return False
        # 모든 요소 비교
        return self._values == other._values

    def __format__(self, format_spec):
        if format_spec == '':
            format_spec = ' '.join(['{}'] * len(self._values))
        return format_spec.format(*self._values)

v = Vector(1, 2, 3, 4, 5)

print(format(v))                     # 기본 포맷: 1 2 3 4 5
print(format(v, '{:.2f}, {:.2f}, {:.2f}, {:.2f}, {:.2f}'))  # 포맷 지정: 1.00, 2.00, 3.00, 4.00, 5.00
  1. __format__ 메서드:
    • format_spec 인자는 포맷 지정 문자열을 받는다. 기본값은 빈 문자열이다.
    • format_spec이 빈 문자열인 경우, 기본 포맷으로 벡터의 모든 요소를 공백으로 구분하여 출력한다.
    • format_spec을 지정한 경우, 해당 포맷 문자열을 사용하여 벡터의 각 요소를 포맷한다.
    • format_spec 문자열 내에서 {}를 사용하여 벡터 요소의 포맷팅 위치를 지정한다.
  2. 포맷팅 예제:
    • format(v)는 기본 포맷으로 벡터를 출력한다.
    • format(v, '{:.2f}, {:.2f}, {:.2f}, {:.2f}, {:.2f}')는 소수점 두 자리까지 포맷팅하여 출력한다.

이제 Vector 클래스는 다양한 형식으로 벡터를 포맷팅하여 출력할 수 있다. 이를 통해 벡터의 출력 형식을 유연하게 지정할 수 있다.

 

모든 코드는 github에 저장되어 있습니다.

https://github.com/SeongUk18/python

728x90