10

I would like to define a class so that its instances can be casted to both tuple and dict. An example:

class Point3:
    ...

p = Point(12, 34, 56)

tuple(p)  # gives (12, 34, 56)
dict(p)   # gives { 'x': 12, 'y': 34, 'z': 56 }

I have found that if I define __iter__ as an iterator that yields single values then the instance can be casted to tuple and if it instead yields double values then it can be casted to dict:

class Point3:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    # This way makes instance castable to tuple
    def __iter__(self):
        yield self.x
        yield self.y
        yield self.z

    # This way makes instance castable to dict
    def __iter__(self):
        yield 'x', self.x
        yield 'y', self.y
        yield 'z', self.z

Is there any way to make instances castable to both tuple and dict in Python 2.7?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
md2perpe
  • 3,372
  • 2
  • 18
  • 22
  • 2
    Why not just define methods on your class called `.to_tuple()` and `.to_dict()` that convert to tuple and dict in whatever ways you want? – BrenBarn Jul 23 '18 at 18:13
  • 1
    there is no "type casting" in Python. why would you want to convert your objects to plain dicts or lists, anyway? just implement the access methods you want and use them directly. –  Jul 23 '18 at 18:14
  • @BrenBarn. That's kind of the way I've solved it now. I have made the class "castable" to `tuple` and used `.to_dict()` for creating dictionaries. – md2perpe Jul 23 '18 at 18:17
  • @md2perpe This is a good question, but you need a switch somewhere. When do you want to cast it as a dictionary and when do you want to cast it as a tuple (becasue both calls `__iter__`). – scharette Jul 23 '18 at 18:18
  • @hop. Call it whatever you want. It looks like casting, so I called it that. – md2perpe Jul 23 '18 at 18:19
  • @hop. The reason I want to convert my objects to `tuple` is that I'm scripting in [VisIt](https://wci.llnl.gov/codes/visit/) and that only accepts tuples for points and vectors. But I want to have objects that I can manipulate using operators (e.g. `focusPoint + directionVector`). Then, when using it in VisIt, it's nice to just write for example `view_attributes.focus = tuple(focusPoint)`. – md2perpe Jul 23 '18 at 18:24
  • @scharette. I was hoping that there would be some argument sent to `__iter__()` that could be used to distinguish the two cases (similar to `operator ++ ()` and `operator ++ (int)` in C++), but unfortunately I didn't find any such differences. – md2perpe Jul 23 '18 at 18:28
  • 1
    @md2perpe: if that is so, then that tool is severely broken. subclass `Point` from `dict` (or `namedtuple`) and use `tuple(p.items())` wherever necessary. –  Jul 23 '18 at 18:28
  • @hop. I'll try that. – md2perpe Jul 23 '18 at 18:31
  • see here, for example: https://stackoverflow.com/questions/44320382/subclassing-python-namedtuple –  Jul 23 '18 at 18:32
  • @hop. It even seems to work subclassing `dict` and then implement `__iter__()` to make the class "castable" to `tuple`. – md2perpe Jul 23 '18 at 18:46
  • @md2perpe do _not_ do it this way. –  Jul 23 '18 at 18:51
  • sorry, typo in my above comment. it should have said `tuple(p.values())`, of course. –  Jul 23 '18 at 19:04
  • @hop. Can we be sure that the values come in the right order when we run `tuple(p.values())`? A dict often presents the key-value pairs in disorder, but there it doesn't really matter, it just looks a bit ugly. But if they come in the wrong order in a tuple, it's really bad. – md2perpe Jul 23 '18 at 19:07
  • @md2perpe: not in Python 2, no. i missed that you mentioned that at the end of your question. please use the appropriate tag next time! –  Jul 23 '18 at 19:15
  • @hop. I didn't know there was a tag for Python 2.7. But now I know. – md2perpe Jul 23 '18 at 19:19
  • And Python 2.7 isn't my choice. I just have to live with what is in [VisIt](https://wci.llnl.gov/codes/visit/) and [ANSA](https://www.beta-cae.com/ansa.htm). – md2perpe Jul 23 '18 at 19:21

2 Answers2

7

You could just subclass NamedTuple (Other types are available, ask your doctor.):

from typing import NamedTuple


class Point(NamedTuple):
    x: float
    y: float
    z: float

    def __add__(self, p):
        return Point(self.x+p.x, self.y+p.y, self.z+p.z)


p = Point(1, 2, 3)
q = Point(5, 5, 5)

print(p.x, p.y, p.z)
print(p+q)
print(tuple(p))

.

$ python pointless.py
1 2 3
Point(x=6, y=7, z=8)
(1, 2, 3)

If the tool you are using has any regard for idiomatic Python, the named tuple should be acceptable as is, anyway. I would try it!

If you were to use dictionaries, I would recommend using the explicit tuple(p.values()) (when subclassing) or maybe p.coordinates or p.xyz as property (when wrapping), rather than relying on some magic behing the scenes.


Legacy version, no warranty.

from collections import namedtuple


_Point = namedtuple('Point', 'x y z')


class Point(_Point):
    __slots__ = ()

    def __add__(self, p):
        return Point(self.x+p.x, self.y+p.y, self.z+p.z)
  • 2
    Syntax error on `x : float`. Reported Python version: 2.7.11. – md2perpe Jul 23 '18 at 19:09
  • 4
    Note: As of 3.7, if you need a mutable class, [the `dataclasses` module](https://docs.python.org/3/library/dataclasses.html) is basically a souped up version of `typing.NamedTuple`, but with mutability by default, and much greater configurability. – ShadowRanger Jul 23 '18 at 20:26
5

You can't do what you originally wanted to do (ie, have two different __iter__ methods) because it doesn't make any sense. But you can fake it using the mapping protocol (see below).

However before going into that, if you really want to cast to a dict, have a look at this answer for some better options.


My suggestion is to either:

  1. Do what was suggested in the other answer and utilize namedtuple, which gives you the _asdict() method "for free".
  2. Implement the class utilizing the mapping protocol (as explained in the previous link above). If you do that, you can circumvent the __iter__ method entirely when casting to dict.

You might do that like this; however this is a little bit odd:

class Point3:
    _fields = tuple("xyz")
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
    def __iter__(self):
        for f in self._fields:
            yield getattr(self, f)
    def keys(self):
        return self._fields
    def __getitem__(self, i):
        if i in self._fields:
            return getattr(self, i)
        raise KeyError("{!r} is not a valid field".format(i))

With the above, the dict is created using the keys() and __getitem__() rather than __iter__:

>>> dict(Point3(1, 2, 3))
{'x': 1, 'y': 2, 'z': 3}

Using the mapping protocol can also come in handy because you can "cast"- that is, unpack your object in the form of keyword arguments- to any other type that accepts the same field names as keyword arguments, e.g.:

point= XYZer(**point3_instance)

For other people (not the OP) who are able to benefit from the latest version of Python 3 (3.7): I highly recommend using the dataclasses module:

from dataclasses import dataclass, asdict, astuple

@dataclass
class Point:
    x: float
    y: float
    z: float

Use it like so:

>>> p = Point(1,2,3)
>>> asdict(p)
{'x': 1, 'y': 2, 'z': 3}
>>> astuple(p)
(1, 2, 3)
Rick
  • 43,029
  • 15
  • 76
  • 119
  • 1
    To point #2, you don't need to use unpacking syntax with the `dict` constructor; you can just do `dict(point)`; `dict(**point)` is somewhat redundant (it converts to `dict` to unpack, then reads the result to initialize the new `dict`). – ShadowRanger Jul 23 '18 at 20:21
  • 1
    @ShadowRanger that's a good point i had forgotten that the mapping protocol short-circuits `__iter__`. – Rick Jul 23 '18 at 20:22
  • @ShadowRanger fixed. – Rick Jul 23 '18 at 20:34