-
-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix return types #71
Fix return types #71
Conversation
@astronouth7303 I don't understand what's going on. Is it making vector/vector operations that involve subclasses of |
Consistency of typing without every subclass needing to wrap each operation to return the right type. Concretely, so |
Oh, I didn't realise 👍 then, as it causes the bug I mentioned in mutant-game |
The type annotations are probably not as precise as they could be, though. |
Subclasses of the thing are implied in all annotations, afaik. So, they all return a |
Yes, but what I mean is that code that does a vector operation, then calls a subclass method/property on the result, would work with your change, but fail to typecheck because the typechecker thinks we return a |
https://mypy.readthedocs.io/en/latest/kinds_of_types.html#class-types
So a type annotation of |
@astronouth7303 OK, to clarify: class V(Vector2):
def method(self):
pass
x = V(1, 0) + (0, 1) # x's inferred type will be Vector2, not V
x.method() # Should fail to typecheck The type annotation is valid (as you said, a (covariant) annotation of |
Oh, then yes. I think this is one of the limitations of the python type checker, since (afaik), you can't have a type variable that goes between different bits of a method's annotation. Like you can't say "x.foo() will return whatever type x is". |
That sounds like a thing that should be discussed on mypy. |
# Normalize for reflect | ||
a = op(V1(1, 2), V2(3, 4).normalize()) | ||
b = op(V2(1, 2), V1(3, 4).normalize()) | ||
assert isinstance(a, V2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean specifically that we're returning the subclass in all cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because V2 is a subclass of V1, it should return the more specialized V2.
ppb_vector/vector2.py
Outdated
@@ -34,6 +35,37 @@ def _mkvector(value, *, castto=_fakevector): | |||
raise ValueError(f"Cannot use {value} as a vector-like") | |||
|
|||
|
|||
@functools.lru_cache() | |||
def _find_lowest_type(left: type, right: type) -> type: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably use typing.Type here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are documented as equivalent:
Type[Any]
is equivalent toType
which in turn is equivalent totype
, which is the root of Python’s metaclass hierarchy.
But I'll do it because metaprogramming like this can be weird.
@astronouth7303 It looks like it's doable: https://gist.github.com/nbraud/a592985bb700ac74894efd8243cb08ce |
Everything mentioned so far should be taken care of. |
With annotations getting this complex, I figured we should actually run it through mypy. It was informative. |
python/mypy#5821 came up, and has some insights into typing. |
Sorry, this PR is getting kinda large. I feel like I've touched most of the code at this point. |
@astronouth7303 Yeah, there is likely no way to improve the annotations without touching all the methods. Thanks a lot for doing it. Small question about > from numbers import Real
> isinstance(1, Real)
True
> isinstance(1.0, Real)
True Is mypy in the wrong about |
Re: the PR becoming too large, I guess we can split off the actual behaviour change (in selecting the return type) and the improvement in type annotations. What do you think? |
I suppose I can. I'm not sure it helps much? |
This is because mypy doesn't pickup on runtime registered ABC subclasses. So, mypy is correct in that (And no, this is pretty impure typing. It comes from a pragmatic side because |
@astronouth7303 Sounds like a bug that should be reported against mypy, then? |
That's Real Complicated, and there's been Very Large Discussions about mypy and ABCs and The Number Tower (PEP 3141). See python/mypy#2636 and python/mypy#3186 for details on those discussions. |
ppb_vector/vector2.py
Outdated
elif isinstance(value, Vector2): | ||
return cls(value.x, value.y) | ||
# FIXME: Allow all types of sequences | ||
elif isinstance(value, (list, tuple)) and len(value) == 2: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isinstance(value, typing.Sequence)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'd want collections.abc.Sequence
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference, concretely? (Also, sure, go for it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typing.Sequence
is basically a typing-related data structure, only used to construct annotation data.
collections.abc.Sequence
is an actual abstract class with default implementations of a pile of methods.
ppb_vector/vector2.py
Outdated
elif isinstance(value, (list, tuple)) and len(value) == 2: | ||
return cls(value[0].__float__(), value[1].__float__()) | ||
# FIXME: Allow all types of mappings | ||
elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isinstance(value, typing.Mapping)
?
Also, do we want to enforce len(value) == 2
?
It might be useful to be able to build a vector from a dict like {'x': 0, 'y': 1, 'color': 'brown'}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I think we want to keep exact forms to prevent mistakes.
Especially since afaik no code actually uses the dict form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You added Mapping
to the imports in a6161b9 but you forgot to do anything with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did, this comment is outdated.
ppb_vector/vector2.py
Outdated
|
||
@property | ||
def length(self) -> float: | ||
if not hasattr(self, '_length'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the reflection dance with hasattr
to memoize the vector length really faster than recomputing it every time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I did a quick μbench with timeit
, and it looks like the speedup/slowdown of saving the result in _length
is below the measurement noise, so I would recommend removing that code and just doing return hypot(self.x, self.y)
.
$ ./dunder_bench.py
1 successive length computations
Memoized: 0.41153700300492346
Computed: 0.41805144600220956
Speedup: 1.5829543758445919%
5 successive length computations
Memoized: 1.2452478090126533
Computed: 1.2318236450082622
Speedup: -1.0780315297269971%
10 successive length computations
Memoized: 2.252152765984647
Computed: 2.2965358549845405
Speedup: 1.970696200996347%
25 successive length computations
Memoized: 5.274616469978355
Computed: 5.400348082999699
Speedup: 2.3837110003537276%
50 successive length computations
Memoized: 10.367006862012204
Computed: 10.45787951600505
Speedup: 0.8765563214376826%
100 successive length computations
Memoized: 21.056249847984873
Computed: 21.302848722989438
Speedup: 1.1711433744606925%
./dunder_bench.py 81.75s user 0.00s system 99% cpu 1:21.77 total
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does computing it have any significant overhead overhead? I did this to get rid of _fakevector
while keeping the performance benefit of not computing the length at construction time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@astronouth7303 That's what the microbenchmark I wrote was showing: even if you access the computed length 100 times in a row, loading it from self._length
isn't faster than recomputing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simplified the implementation of length
.
except ValueError: | ||
return NotImplemented | ||
rtype = type(other) if isinstance(other, Vector2) else type(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a good idea to move the computation of rtype
before the try/except
?
It can be that _find_lowest_vector
throws an exception in cases where we would previously have returned NotImplemented
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_find_lowest_vector()
doesn't throw exceptions, and they are internal functions meant for DRYing.
@@ -10,7 +10,7 @@ | |||
(Vector2(1, 1), Vector2(-1, 0), 135), | |||
(Vector2(0, 1), Vector2(0, -1), 180), | |||
(Vector2(-1, -1), Vector2(1, 0), 135), | |||
(Vector2(-1, -1), Vector2(-1, 0), 45), | |||
(Vector2(-1, -1), Vector2(-1, 0), -45), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are there unrelated changed in the angle tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because passing tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except that the tests are passing in master
, so there is something wrong going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're going to make my life difficult, I know it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK.
This is actually a bug mypy found.
The two tests in this file had the same name, so only the latter one was being run.
This exposed problems in the earlier test because it wasn't running before.
This can be moved to its own PR if y'all would really rather.
@astronouth7303 Thanks for the link to the mypy discussions, re: the numbers tower. |
""" | ||
return type(self)(self.x * other, self.y * other) | ||
|
||
@typing.overload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
49962d8
to
9333fda
Compare
There is an argument to be made for Vector/Vector operations to return the left type.