Scribbling

Python: Sequence Protocol 본문

Computer Science/Python

Python: Sequence Protocol

focalpoint 2022. 4. 6. 11:41

 

To learn sequence protocol in Python, we create a custom vector class.

from array import array
import math
import reprlib

class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)

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

    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + bytes(self._components))

    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, octests):
        typecode = chr(octests[0])
        memv = memoryview(octests[1:]).cast(typecode)
        return cls(memv)


 

In Python, to create an object that works as a sequence, we don't need to inherit any classes. Instead, all we have to do is implement magic methods. This mechanism is called as "Duck Typing".

 

So, we only need to implement '__getitem__' magic method for our vector class to operate as a sequence. 

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

def __getitem__(self, index):
    cls = type(self)
    if isinstance(index, slice):
        return cls(self._components[index])
    elif isinstance(index, numbers.Integral):
        return self._components[index]
    else:
        msg = f'{cls.__name__} indices must be integers'
        raise TypeError(msg)

 

Next, '__getattr__' magic method. How does Python Interpreter look for attributes? It first checks if the attribute is defined in the object. If there isn't, it searches for a class attribute. Again if there isn't, the interpreter searches super classes. If the interpreter fails to find the attribute in the above process, it then calls __getattr__ method.

shortcut_names = 'xyzt'
def __getattr__(self, name):
    cls = type(self)
    if len(name) == 1:
        pos = cls.shortcut_names.find(name)
        if 0 <= pos < len(self._components):
            return self._components[pos]
    msg = f'{cls.__name__!r} object has no attribute {name:!r}'
    raise AttributeError(msg)
v1 = Vector(range(1, 10))
print(v1.y)
v1.y = 3
print(v1.y)

The result shows that v1.y works fine. However, a code like "v1.y = 3" can be troublesome as our vector class is supposed to be immutable. Plus, it didn't even change the value of the vector v1! What happend?

It happens because of how python interpreter looks for an attribute. Python calls '__getattr__' method as the final step. In this example, however, "v1.y = 3" adds a 'y' attribute to the v1 object. And as v1 has a 'y' attribute, "v1.y" now does not work as intended.

 

To prevent such problem, we must add '__setattr__' magic method to our class. Remeber to implment '__setattr__' when you implement '__getattr__' to avoid such inconsistency.

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():
            error = 'can`t set attributes `a` to `z` in {cls_name!r}'
        else:
            error = ''
        if error:
            msg = error.format(cls_name=cls.__name__, attr_name=name)
            raise AttributeError(msg)
    super().__setattr__(name, value)

 

Full code:

from array import array
import math
import reprlib
import numbers
import functools
import operator
import itertools

class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)

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

    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + bytes(self._components))

    def __eq__(self, other):
        return len(self) == len(other) and all((a == b for a, b in zip(self, other)))

    def __hash__(self):
        # hashes = (hash(x) for x in self)
        hashes = map(hash, self)
        return functools.reduce(operator.xor, hashes, 0)

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    def __bool__(self):
        return bool(abs(self))

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

    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = f'{cls.__name__!r} indices must be integers'
            raise TypeError(msg)

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)
        print(cls, name)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self._components):
                return self._components[pos]
        msg = f'{cls.__name__!r} object has no attribute {name!r}'
        raise AttributeError(msg)

    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():
                error = 'can`t set attributes `a` to `z` in {cls_name!r}'
            else:
                error = ''
            if error:
                msg = error.format(cls_name=cls.__name__, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)

    def angle(self, n):
        r = math.sqrt(sum(x * x for x in self[n:]))
        a = math.atan2(r, self[n-1])
        if (n == len(self) - 1) and (self[-1] < 0):
            return math.pi * 2 - a
        else:
            return a

    @property
    def angles(self):
        return [self.angle(n) for n in range(1, len(self))]

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('h'):
            fmt_spec = fmt_spec[:-1]
            coords = itertools.chain([abs(self)], self.angles)
            outer_fmt = '<{}>'
        else:
            coords = self
            outer_fmt = '({})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(','.join(components))

    @classmethod
    def frombytes(cls, octests):
        typecode = chr(octests[0])
        memv = memoryview(octests[1:]).cast(typecode)
        return cls(memv)

 

'Computer Science > Python' 카테고리의 다른 글

Python: Interfaces  (0) 2022.04.18
Python: ABC Class  (0) 2022.04.07
Python: Pythonic Object  (0) 2022.04.05
Python: Object References  (0) 2022.04.04
Python: Decorator & Closure  (0) 2022.03.29