Adding a .find() method to list

In its current implementation, the list type does not provide a simple and straightforward way to retrieve one of its elements that fits a certain criteria. If you had to get the user where user['id'] == 2 from this list of users, for example, how would you do it? users = [ {'id': 1,'name': 'john'}, {'id': 2, 'name': 'anna'}, {'id': 3, 'name': 'bruce'}, ] # way too verbose and not pythonic ids = [user['id'] for user in users] index = ids.index(2) user_2 = users[index] # short, but it feels a bit janky user_2 = next((user for user in users if user['id'] == 2), None) # this is okay-ish, i guess users_dict = {user['id']: user for user in users} user_2 = users_dict.get(2) In my opinion, the list type could have something along these lines: class MyList(list): def find(self, func, default=None): for i in self: if func(i): return i return default my_list = MyList(users) user_2 = my_list.find(lambda user: user['id'] == 2) print(user_2) # {'id': 2, 'name': 'anna'}

Hi Lucas.rs You wrote:
Thank you. You've asked a good question. I hope my answer will be helpful. Here's my preferred solution, using Python builtins:
next(filter(func, users)) {'id': 2, 'name': 'anna'}
For comparison, here's your solution using your subclass of list.
my_list.find(func) {'id': 2, 'name': 'anna'}
I prefer using the builtin filter function because: 1. I already know what 'filter' means. 2. I already know what 'next' means. 3. I already know what will happen if the object is not found (or if there are several solutions. 4. It's fairly easy to modify the code to given different behaviour, eg
list(filter(func, users)) [{'id': 2, 'name': 'anna'}]
Finally, perhaps your code would be better if the items in the 'user' list were members of a class, rather than being bare dicts. And once you've done that, you could create a new class for holding a collection (or set) of members. And now we're moving towards how Django (and other frameworks) handle interactions with a database. -- Jonathan

On Sat, May 7, 2022 at 9:15 AM Jonathan Fine <jfine2358@gmail.com> wrote:
The OP did suggest a comprehension version, which I personally prefer -- but thought it " feels a bit janky" -- I can say that lambda/filter feels even jankier to me :-) you could create a new class for holding a collection (or set) of members.
and then you'd be writing this code in your class -- but that's a good thing, as you could then refactor it to optimize it or use persistent storage, or .... Anyway, I agree that once. you need to customize your matching function, you might as just write the code. -CHB -- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

On Sat, 7 May 2022 at 16:42, <python-ideas@lucas.rs> wrote:
You seem to want a function, but it's not obvious to me why you need that. found = None for user in users: if user["id"] == 2: found = user break seems fine to me. If you need a function def find_user(id): for user in users: if user["id"] == id: return user works fine. Python is very much a procedural language, and "simple and straightforward" often equates to a few statements, or a loop, or similar. Unlike functional languages, where people tend to think of "simple" code as being about combining basic functions into compound expressions that do "clever stuff", Python code tends to be viewed as "simple and straightforward" (or "Pythonic" if you like) if it *doesn't* try to combine too much into one expression, but describes what you're doing in a step by step manner. So yes, a list doesn't provide the sort of "find" method you're suggesting. That's because a loop is easy, and does the job just fine. Paul

Hi Paul You wrote:
You [the original poster] seem to want a function[to express the criteria], but it's not obvious to me why you need that.
A function has several advantages. 1. It can be reused. 2. It can be tested (a special case of 1). 3. It can be passed as a parameter (a special case of 1). 4. It can be imported from a different Python module (a special case of 1). 5. The code that uses function can be reused (if the function is passed as a parameter). These advantages were sufficient for me at the time to accept the original poster's desire "to retrieve one of its elements that fits a certain criteria". That is not to say that in every case a function is the best way to do things. It's enough that in some (or many) cases using a function is a good thing. -- Jonathan

Still - the "filter" call is almost as simple as it can get for a generic enough way to do what you are requesting. There is some boiler plate needed around it if you want an actual eager result or a default value, if no match is found, that is true - but still, given a list like On Sat, May 7, 2022 at 1:51 PM Paul Moore <p.f.moore@gmail.com> wrote:
user_2 = next(filter(lambda record: record["id"] == 2, users), default_val) Or otherwise, use some library to have some higher level options to manipulate your data structures, even if they are lists of dictionaries. I am right now working on "rows" (github: turicas/rows) package and charged with creating possibilities of lazy querying the data structures. This is currently working on my development branch: In [26]: import rows In [27]: zz =rows.table.FlexibleTable() In [28]: zz.extend(users) In [29]: zz Out[29]: <rows.Table 2 fields, 3 rows> In [30]: zz.filter = "id = 2" In [31]: zz Out[31]: <rows.Table 2 fields, 1 rows> In [32]: zz[0] Out[32]: Row(id=2, name='anna') (or, if you want dicts back, you have to configure the Table instance: In [33]: zz.row_class = dict In [34]: zz[0] Out[34]: {'id': 2, 'name': 'anna'} ) https://github.com/turicas/rows/tree/feature/queries

On Sat, May 07, 2022 at 04:01:34AM -0000, python-ideas@lucas.rs wrote:
user = None for record in users: if record['id'] == 2: user = record break else: # for...else raise LookupError('user id not found') If I needed to do it more than once, or if it needed testing, I would change the break into `return record` and put it into a function. If I really needed to lookup user IDs a lot, I wouldn't use a list, I would use something like this: users = { # map user ID to user name 1: 'john', 2: 'anna', 3: 'bruce', } so that user ID lookups are simple and blazingly fast: user = users[2] rather than needing to walk through the entire list inspecting each item. Picking the right data structure for your problem is 9/10th of the battle.
Three lines is not "too verbose". I wouldn't call it "not Pythonic", I would just call it poor code that does too much unnecessary work.
# short, but it feels a bit janky user_2 = next((user for user in users if user['id'] == 2), None)
Seems OK to me.
More unnecessary work, building an entire temporary dict of potentially millions of users just to extract one. Of course if you are going to be using it over and over again, this is the right solution, not the list. -- Steve

Hi Lucas.rs You wrote:
Thank you. You've asked a good question. I hope my answer will be helpful. Here's my preferred solution, using Python builtins:
next(filter(func, users)) {'id': 2, 'name': 'anna'}
For comparison, here's your solution using your subclass of list.
my_list.find(func) {'id': 2, 'name': 'anna'}
I prefer using the builtin filter function because: 1. I already know what 'filter' means. 2. I already know what 'next' means. 3. I already know what will happen if the object is not found (or if there are several solutions. 4. It's fairly easy to modify the code to given different behaviour, eg
list(filter(func, users)) [{'id': 2, 'name': 'anna'}]
Finally, perhaps your code would be better if the items in the 'user' list were members of a class, rather than being bare dicts. And once you've done that, you could create a new class for holding a collection (or set) of members. And now we're moving towards how Django (and other frameworks) handle interactions with a database. -- Jonathan

On Sat, May 7, 2022 at 9:15 AM Jonathan Fine <jfine2358@gmail.com> wrote:
The OP did suggest a comprehension version, which I personally prefer -- but thought it " feels a bit janky" -- I can say that lambda/filter feels even jankier to me :-) you could create a new class for holding a collection (or set) of members.
and then you'd be writing this code in your class -- but that's a good thing, as you could then refactor it to optimize it or use persistent storage, or .... Anyway, I agree that once. you need to customize your matching function, you might as just write the code. -CHB -- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

On Sat, 7 May 2022 at 16:42, <python-ideas@lucas.rs> wrote:
You seem to want a function, but it's not obvious to me why you need that. found = None for user in users: if user["id"] == 2: found = user break seems fine to me. If you need a function def find_user(id): for user in users: if user["id"] == id: return user works fine. Python is very much a procedural language, and "simple and straightforward" often equates to a few statements, or a loop, or similar. Unlike functional languages, where people tend to think of "simple" code as being about combining basic functions into compound expressions that do "clever stuff", Python code tends to be viewed as "simple and straightforward" (or "Pythonic" if you like) if it *doesn't* try to combine too much into one expression, but describes what you're doing in a step by step manner. So yes, a list doesn't provide the sort of "find" method you're suggesting. That's because a loop is easy, and does the job just fine. Paul

Hi Paul You wrote:
You [the original poster] seem to want a function[to express the criteria], but it's not obvious to me why you need that.
A function has several advantages. 1. It can be reused. 2. It can be tested (a special case of 1). 3. It can be passed as a parameter (a special case of 1). 4. It can be imported from a different Python module (a special case of 1). 5. The code that uses function can be reused (if the function is passed as a parameter). These advantages were sufficient for me at the time to accept the original poster's desire "to retrieve one of its elements that fits a certain criteria". That is not to say that in every case a function is the best way to do things. It's enough that in some (or many) cases using a function is a good thing. -- Jonathan

Still - the "filter" call is almost as simple as it can get for a generic enough way to do what you are requesting. There is some boiler plate needed around it if you want an actual eager result or a default value, if no match is found, that is true - but still, given a list like On Sat, May 7, 2022 at 1:51 PM Paul Moore <p.f.moore@gmail.com> wrote:
user_2 = next(filter(lambda record: record["id"] == 2, users), default_val) Or otherwise, use some library to have some higher level options to manipulate your data structures, even if they are lists of dictionaries. I am right now working on "rows" (github: turicas/rows) package and charged with creating possibilities of lazy querying the data structures. This is currently working on my development branch: In [26]: import rows In [27]: zz =rows.table.FlexibleTable() In [28]: zz.extend(users) In [29]: zz Out[29]: <rows.Table 2 fields, 3 rows> In [30]: zz.filter = "id = 2" In [31]: zz Out[31]: <rows.Table 2 fields, 1 rows> In [32]: zz[0] Out[32]: Row(id=2, name='anna') (or, if you want dicts back, you have to configure the Table instance: In [33]: zz.row_class = dict In [34]: zz[0] Out[34]: {'id': 2, 'name': 'anna'} ) https://github.com/turicas/rows/tree/feature/queries

On Sat, May 07, 2022 at 04:01:34AM -0000, python-ideas@lucas.rs wrote:
user = None for record in users: if record['id'] == 2: user = record break else: # for...else raise LookupError('user id not found') If I needed to do it more than once, or if it needed testing, I would change the break into `return record` and put it into a function. If I really needed to lookup user IDs a lot, I wouldn't use a list, I would use something like this: users = { # map user ID to user name 1: 'john', 2: 'anna', 3: 'bruce', } so that user ID lookups are simple and blazingly fast: user = users[2] rather than needing to walk through the entire list inspecting each item. Picking the right data structure for your problem is 9/10th of the battle.
Three lines is not "too verbose". I wouldn't call it "not Pythonic", I would just call it poor code that does too much unnecessary work.
# short, but it feels a bit janky user_2 = next((user for user in users if user['id'] == 2), None)
Seems OK to me.
More unnecessary work, building an entire temporary dict of potentially millions of users just to extract one. Of course if you are going to be using it over and over again, this is the right solution, not the list. -- Steve
participants (6)
-
Christopher Barker
-
Joao S. O. Bueno
-
Jonathan Fine
-
Paul Moore
-
python-ideas@lucas.rs
-
Steven D'Aprano