mahmoud / glom

☄️ Python's nested data operator (and CLI), for all your declarative restructuring needs. Got data? Glom it! ☄️
https://glom.readthedocs.io
Other
1.9k stars 61 forks source link

Extracting an element from a list #18

Open MrGreenTea opened 6 years ago

MrGreenTea commented 6 years ago

I have some array of objects like this:

target = [
 {'id': 0}, 
 {'id': 1},
 ...
]

I now want to get the object with id=0 for example. Is there a way to do this with glom? My current solution looks like this and doesn't feel that elegant:

glom.glom(target, [lambda t: t if t['id']==0 else glom.OMIT])[0]

I also got it working with

glom.glom(target, ([lambda t: t if t['id'] == pk else glom.OMIT], glom.T[0]))

which still looks not that clean to me.

mahmoud commented 6 years ago

Hey @MrGreenTea! I'm glad you've already discovered T. :)

Depending on what exact semantics you want, the latter solution is definitely most correct. @kurtbrose and I have been discussing some pattern matching possibilities which might clean this up, so it's a good thing you brought up an example.

For now I'd probably stick with the second version, unless @kurtbrose has some ideas. :)

kurtbrose commented 6 years ago

This looks like a job for Check (as soon as I get it done...)

With Check, this would be the following:

glom(target, [Check('id', val=0)])

This is a perfect use case, I'll get on finishing up check and use this for the unit test.

MrGreenTea commented 6 years ago

@kurtbrose This looks exactly like what I just now wanted to suggest. That's awesome. Would this still require me choosing the first element afterwards?

kurtbrose commented 6 years ago

oh, yea; I see id's are unique?

It would be nice if you could just express the dict that you wanted and then select out of it....

glom(target, ([T['id'], T], dict, T[0]) )

Currently there isn't a simple way to construct the list-of-iterables required for the dict. (It can be done, but it is a bit wordy: glom(target, ([Call(tuple, args=[T['id'], T])], dict, T[0])))

Right now, lists are only valid in a glom if they have a single item.

Something else that has seemed to be a potential missing abstraction is some way to construct explicit tuples.

kurtbrose commented 6 years ago

Oh no, even Call(tuple, args=[T['id'], T]) because that will do tuple(T['id'], T) -- what should happen is tuple([T['id'], T]) -- although if you had a clean way to construct a list of two elements, you could skip the Call because list-of-lists-of-two-elements is fine itself as an argument to the dict constructor...

Hmm.....

So, Check still seems like the most direct way to solve this problem and was already on the roadmap / partly written (https://github.com/mahmoud/glom/blob/check/glom/core.py#L399-L447) so I'll proceed with that. Semantics for these kind of explicit lists might also be in the near future though.

Some thoughts --

Record([T['id'], T], type=tuple) -- record is just very explicit about what is intended

[T['id'], T] -- this reads nicely, but feels awkward -- list of 1 item means "unpack the target", list of 2 items means "join the results of multiple together"; seems error prone

Maybe another way to do this would be with zip

glom(target, (Call(zip, args=[[T['id']], T]), dict, T[0]) )
kurtbrose commented 6 years ago

That last one works, but is quite awkward...

>>> target = [{'id': 0}, {'id': 1}]
>>> from glom import glom, T, Call
>>> from glom.core import Spec
>>> glom(target, (Call(zip, args=[Spec([T['id']]), T]), dict, T[0]) )
{'id': 0}

(You need to use the undocumented Spec() so that Call knows [T['id']] is a sub-spec that should be evaluated and not a literal.)

kurtbrose commented 6 years ago

I'll get back to work on Check; honestly, if all you want to do is filter a single-level list like that, I think you're better off with a list comprehension

[e for e in target if e['id'] == 0][0]

even with Check, it's probably better to use language features than a library unless there is a clear advantage to doing so

kurtbrose commented 6 years ago

better dict literal syntax might help

glom(target, ({T['id']: T}, T[0]))
MrGreenTea commented 6 years ago

You're right about the list comprehension of course. It just felt natural to try with my nested data to extract single records. That's why I like the Record as a class.

MrGreenTea commented 6 years ago

I have some ideas for the Record class. I would love some feedback on it and if it fits your vision. I'd be glad to see if I'll manage to implement it then :) I am basing this on the current interface of Check. What do you think?

Record(path, instance_of=_MISSING, types=_MISSING, val=_MISSING, vals=_MISSING, exists=_MISSING, *, first=False)
# Checks for an element at path (like Check). If first==True, it returns the first one that's found. If first==False, it raises an error if an element cannot be uniquely.
kurtbrose commented 6 years ago

The intention of Record is to generate a tuple of items, akin to the list-of-tuples returned by fetchall() in dbapi2 database APIs (e.g. sqlite3 in the standard library).

Some other possible names that would convey the same idea might be Struct or Append.

The basic API is Record(specifiers, type); an example usage would be Record(['a', 'b', 'c.d'], type=tuple) which would give the result (target.a, target.b, target.c.d)

MrGreenTea commented 6 years ago

I understand. My idea was something similar to QuerySets.get() in Django.

kurtbrose commented 6 years ago

in pattern matching there is the notion of a "guard expression" (came across it here https://www.reddit.com/r/Python/comments/725xam/adding_pattern_matching_to_python/dngkgde/)

extract(x) if guard:
   ...

Guard is another alternative for Check perhaps name-wise

ian-whitestone commented 4 years ago

Hey folks, just wanted to check, is glom.glom(target, ([lambda t: t if t['id'] == pk else glom.OMIT], glom.T[0])) still the best solution, or is there a better way to do this now? Thanks

kurtbrose commented 4 years ago

We've got a lot more parts to play with over the last two years:

([Or(M(T.id) == pk, default=SKIP)], T[0])

Hmm not sure if that is any clearer