How to treat a Python object as the value of an attribute of that object?

Question:

I’m currently writing a Python project in which I need to pass around a custom class representation of an 8-bit byte. I can access an individual byte with __getitem__() (for example, the first byte inmy_byte = Byte("00000001") would be accessed with my_byte[0], returning 1, since I’m starting at the right), but I would also want to be able to access the value of the byte as a whole. I can access it with a .value property, however that means that if I want to index a list with my_byte, then I would need to put my_list[my_byte.value], when I would much rather be able to do my_list[my_byte] and have it so that the conversion occurs implicitly there. Additionally, I can set the value of my_byte with a setter, but I also need to set that as my_byte.value, which is again clunkier than I would like, when I would prefer to be able to set the value of my_byte with just my_byte = 42.

There are a variety of reasons for it, but I would rather use this custom representation of a Byte for this project. If I have to use my_byte.value so be it, but I’m really hoping there’s a solution I’m just missing.

I would have assumed it would have been some kind of dunder method to define this kind of behavior, but I don’t think any of them do what I’m intending to do. This is a current sample of a more basic version of my current Byte representation, missing non-relevant parts:

class Byte:
  def __init__(self):
    self.bits = [0 for _ in range(8)]

  @property
  def value(self):
    return sum((2**i * self.bits[7-i] for i in range(7, -1, -1)))

  @value.setter
  def value(self, value):
    for i in range(7, -1, -1):
      if value >= 2**i:
        value -= 2**i
        self.bits[7-i] = 1
      else:
        self.bits[7-i] = 0

#Current behavior:
>> my_byte = Byte()
>> my_byte.value = 42
>> my_byte.value
42

#Intended behavior:
>> my_byte = Byte()
>> my_byte = 42
>> my_byte
42
Asked By: HobbitJack

||

Answers:

It won’t work in general, but for indexing (and any more explicit conversions to integer), the __index__ special method will do what you want. Just define it, taking only self as an argument, and have it return an int converted from the value stored, e.g.:

class Byte:
  def __init__(self):
    self.bits = [0 for _ in range(8)]

  @property
  def value(self):
    return sum((2**i * self.bits[7-i] for i in range(7, -1, -1)))

  @value.setter
  def value(self, value):
    for i in range(7, -1, -1):
      if value >= 2**i:
        value -= 2**i
        self.bits[7-i] = 1
      else:
        self.bits[7-i] = 0

  def __index__(self):
    return self.value

Having done that, my_list[my_byte] will just work. On modern Python, you can also do int(my_byte) to explicitly convert to int (on older Python, you can do __int__ = __index__ within the class definition to support int) and it will work with the operator.index function (which is how you explicitly ask to convert to an int-like thing to a true int, without the lossy coercion calling int itself will do for floats and str and the like).

Now, as noted, this won’t make it seamlessly behave like an int in every circumstance. If you want that, and don’t want to inherit from int (given you’re writing a mutable type, that won’t work), you’re going to have to write your own subclass of numbers.Integral. It’s more work, and I strongly recommend looking at the source code for fractions.Fraction before you begin. The module was written, at least in part, to be a guide to developing your own custom numeric types in a way that doesn’t involve hand-writing every single mathematical operator overload entirely from scratch. It’s still a lot, but the system of defining a single method to do each overload, then using the _operator_fallbacks helper to convert it to both the forward and reversed version of each operator halves the work you’d need to do writing each function from scratch.

Answered By: ShadowRanger
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.