-
Notifications
You must be signed in to change notification settings - Fork 275
Support tuple unpacking of DataOps #2243
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |||||||||||||
| import operator | ||||||||||||||
| import pathlib | ||||||||||||||
| import re | ||||||||||||||
| import sys | ||||||||||||||
| import textwrap | ||||||||||||||
| import traceback | ||||||||||||||
| import types | ||||||||||||||
|
|
@@ -197,6 +198,28 @@ def _format_data_op_creation_stack(): | |||||||||||||
| return traceback.format_list(stack) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _unpack_arity(): | ||||||||||||||
| """Number of targets in the unpacking assignment being executed, if any. | ||||||||||||||
|
|
||||||||||||||
| Read from the caller's ``UNPACK_SEQUENCE`` instruction, or None if not found. | ||||||||||||||
| """ | ||||||||||||||
| if (getframe := getattr(sys, "_getframe", None)) is None: | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this could be thrown in the same "best effort / catch-all" bag as the rest but don't have a strong preference either way
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ye, I agree, this will make the code a bit more readable |
||||||||||||||
| return None | ||||||||||||||
| try: | ||||||||||||||
| # skip the frames of this function and of DataOp.__iter__ | ||||||||||||||
| frame = getframe(2) | ||||||||||||||
| for instruction in dis.get_instructions(frame.f_code): | ||||||||||||||
| if instruction.offset == frame.f_lasti: | ||||||||||||||
| if instruction.opname == "UNPACK_SEQUENCE": | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you add a short comment saying that we intentionally don't handle UNPACK_EX? thanks! |
||||||||||||||
| return instruction.arg | ||||||||||||||
| return None | ||||||||||||||
| except Exception: | ||||||||||||||
| # this is best-effort introspection: anything unexpected must fall back | ||||||||||||||
| # on refusing to iterate, not raise something else. | ||||||||||||||
| pass | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class DataOpImpl: | ||||||||||||||
| """Base class for all kinds of DataOps (computation graph nodes). | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -700,6 +723,13 @@ def __bool__(self): | |||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| def __iter__(self): | ||||||||||||||
| # Unpacking (`a, b = data_op`) is supported: we know how many values are | ||||||||||||||
| # expected, so we can create a node for each of them. Any other kind of | ||||||||||||||
| # iteration would need the length of the result, which is unknown until | ||||||||||||||
| # the DataOp is evaluated. | ||||||||||||||
| if (arity := _unpack_arity()) is not None: | ||||||||||||||
| values = unpack(self, arity) | ||||||||||||||
| return iter([values[i] for i in range(arity)]) | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nitpick) out of curiosity why do you prefer |
||||||||||||||
| raise TypeError( | ||||||||||||||
| "This object is a DataOp that will be evaluated later, " | ||||||||||||||
| "when your learner runs. So it is not possible to eagerly " | ||||||||||||||
|
|
@@ -1662,6 +1692,33 @@ def pretty_repr(self): | |||||||||||||
| return f"[{_get_preview(self.key)!r}]" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class AsTuple(DataOpImpl): | ||||||||||||||
| """Node created by unpacking a DataOp, e.g. ``a, b = data_op``.""" | ||||||||||||||
|
|
||||||||||||||
| _fields = ["iterable", "expected_length"] | ||||||||||||||
|
|
||||||||||||||
| def compute(self, e, mode, environment): | ||||||||||||||
| # converting to a tuple (rather than indexing into the result directly) | ||||||||||||||
| # allows unpacking any iterable, and makes sure an iterator is consumed | ||||||||||||||
| # only once even though each target indexes into this node. | ||||||||||||||
| result = tuple(e.iterable) | ||||||||||||||
| expected, got = e.expected_length, len(result) | ||||||||||||||
| if got != expected: | ||||||||||||||
| problem = "not enough" if got < expected else "too many" | ||||||||||||||
| raise ValueError( | ||||||||||||||
| f"{problem} values to unpack (expected {expected}, got {got})" | ||||||||||||||
| ) | ||||||||||||||
| return result | ||||||||||||||
|
|
||||||||||||||
| def __repr__(self): | ||||||||||||||
| return f"<{self.__class__.__name__} {short_repr(self.iterable)}>" | ||||||||||||||
|
Comment on lines
+1712
to
+1714
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I know I suggested the current version but on second thought showing the repr of the parent is a bit redundant and not really consistent with most other dataops, showing the number of items is more informative and consistent with eg skrub.concat . (we know that expected_length is an actual number because AsTuple is only created by iter, though even if it wasn't the consequence would not be dramatic)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, makes sense! had a similar thought when I saw the printed DAG |
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @checked_data_op_constructor | ||||||||||||||
| def unpack(iterable, expected_length): | ||||||||||||||
| return DataOp(AsTuple(iterable, expected_length)) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class Call(DataOpImpl): | ||||||||||||||
| _fields = [ | ||||||||||||||
| "func", | ||||||||||||||
|
|
||||||||||||||
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.