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?

17
  • 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? Commented Jul 23, 2018 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. Commented Jul 23, 2018 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. Commented Jul 23, 2018 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__). Commented Jul 23, 2018 at 18:18
  • 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. Commented Jul 23, 2018 at 18:28

2 Answers 2

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) 
Sign up to request clarification or add additional context in comments.

2 Comments

Syntax error on x : float. Reported Python version: 2.7.11.
Note: As of 3.7, if you need a mutable class, the dataclasses module is basically a souped up version of typing.NamedTuple, but with mutability by default, and much greater configurability.
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) 

3 Comments

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 that's a good point i had forgotten that the mapping protocol short-circuits __iter__.
@ShadowRanger fixed.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.