Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Declaring length of tuples in Python typing

I'm wondering whether or not to submit to Tuple[float, ...] even though I know the length of the tuple.

I have a Point and a Rect class, and a property aspoints in the Rect class that is to return a tuple of the top-left and bottom-right corners, as two-tuples.

The iterator has type Iterator[float], which I know will give me two floats. I want the return value of the property to be Tuple[Tuple[float, float], Tuple[float, float]] because I know that the iterator will give me two floats for each point.

Should I submit, and just say that it will return a Tuple[Tuple[float, ...], Tuple[float, ...]], leaving a comment in the documentation of the length of them, or is there a better solution?

Here's the code.

from dataclasses import dataclass
from typing import Iterator, Tuple

@dataclass
class Point:
    x: float
    y: float

    def __iter__(self) -> Iterator[float]:
        return iter((self.x, self.y))

@dataclass
class Rect:
    x: float
    y: float
    width: float
    height: float

    @property
    def tl(self) -> Point:
        return Point(self.x, self.y)

    @property
    def br(self) -> Point:
        return Point(self.x + self.width, self.y + self.height)

    @property
    def aspoints(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
        return tuple(self.tl), tuple(self.br)

The problem occurs in Rect.aspoints. From MyPy I get the following error:

error: Incompatible return value type (got "Tuple[Tuple[float, ...], Tuple[float, ...]]", expected "Tuple[Tuple[float, float], Tuple[float, float]]")

like image 707
Andreas Avatar asked Mar 25 '19 12:03

Andreas


People also ask

How do you find the length of a tuple in Python?

Python tuple method len() returns the number of elements in the tuple.

Can you take the length of a tuple?

To find the size of a tuple in Python, We use the len() function in Python. The length function is used to find the length of a tuple. It's calculating the size of a tuple; It takes one parameter, it's a tuple in our case and returns a total length of a tuple by counting the tuple's number of elements.

What is the function to define the length of a tuple?

The len() method returns the number of elements in the tuple.

What is typing tuple in Python?

typing. Tuple is special here in that it lets you specify a specific number of elements expected and the type of each position. Use ellipsis if the length is not set and the type should be repeated: Tuple[float, ...] describes a variable-length tuple with float s. For typing.


3 Answers

You can extend your aspoints function to cast the field types correctly:

def aspoints(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
    left = cast(Tuple[float, float], tuple(self.tl))
    right = cast(Tuple[float, float], tuple(self.br))
    return left, right

You can also fit everything in one line, but the readability suffers a lot from it. The cast function doesn't do anything during runtime, it just serves as an explicit way of telling mypy (or some other static type checker) that you know more about your types than can be expressed in the basic typing tools.

You should absolutely not change the return type of __iter__ to be anything that is not an iterator, that would be very strange and confusing.

like image 100
Arne Avatar answered Sep 30 '22 11:09

Arne


The problem seems to be that your knowledge of how many elements the iterator will return can't be encoded into a static type.

I'm not sure that overloading __iter__ to iterate over a fixed number of elements is the best approach here, since you're basically using it as a trick to allow casting an object to a tuple.

Maybe it would make sense to add something like a "to_tuple()" method to the Point data class instead? You could declare the type there...

Edit: alternately, I guess you could destructure the output of the iterator, but you're still not saving a lot of code:

a, b = self.tl
c, d = self.br
return (a, b), (c, d)
like image 42
Christoph Burschka Avatar answered Sep 30 '22 11:09

Christoph Burschka


Another approach: You don't really want to iterate over a Point; you just want a way to get a tuple of floats and you are only making your point iterable so that you can pass it to tuple directly. (Consider this: would you ever write code that looks like for coord in Point(3, 5): ...?)

Instead of defining __iter__, define a function that does what you really want: returns a pair of floats.

from typing import Tuple


PairOfFloats = Tuple[float,float]


@dataclass
class Point:
    x: float
    y: float

    def as_cartesian_pair(self) -> PairOfFloats:
        return (self.x, self.y)


@dataclass
class Rect:
    x: float
    y: float
    width: float
    height: float

    @property
    def tl(self) -> Point:
        return Point(self.x, self.y)

    @property
    def br(self) -> Point:
        return Point(self.x + self.width, self.y + self.height)

    @property
    def aspoints(self) -> Tuple[PairOfFloats, PairOfFloats]:
        return self.tl.as_cartesian_pair(), self.br.as_cartesian_pair()

In support of this approach, it also lets you write an additional method that also returns a pair of floats, but with different semantics:

def as_polar_pair(self) -> PairOfFloats:
    return cmath.polar(complex(self.x, self.y))

Regarding unpacking, defining Point.__getitem__ rather than Point.__iter__ is sufficient:

def __getitem__(self, i) -> float:
    if i == 0:
        return self.x
    elif i == 1:
        return self.y
    else:
        raise IndexError


>>> p = Point(3,5)
>>> p[0], p[1]
(3, 5)
>>> x, y = p
>>> x
3
>>> y
5
like image 24
chepner Avatar answered Oct 01 '22 11:10

chepner