Python Forum
Iterating over dictionaries with the namedtuple function (PyBite #108)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Iterating over dictionaries with the namedtuple function (PyBite #108)
#1
I’m learning how to use the functionnamedtuple() found in the collections module / library.

Here is the exercise that I am grappling with:

Quote:Calculate the amount of points awarded on PyBites given the ninja_belts dictionary, formula: belt score x belt owners (aka ninjas). Make your code generic so if we update ninja_belts to include more belts (which we do in the tests) it will still work. Ah and you can get score and ninjas from the namedtuple with nice attribute access: belt score / belt.ninjas (reason why we get you familiar with namedtuple here, because we love them and use them all over the place!)

Return the total number of points int from the function.

Here is the sample code:

from collections import namedtuple

BeltStats = namedtuple('BeltStats', 'score ninjas')

ninja_belts = {'yellow': BeltStats(50, 11),
               'orange': BeltStats(100, 7),
               'green': BeltStats(175, 1),
               'blue': BeltStats(250, 5)}
The expected end result for this exercise is simply: 2675

When I started this exercise, I immediately found the official Python doc which describes namedtuple() here.

The doc does a terrific job demonstrating how to use namedtuple()’s in Python’s interactive REPL shell. Based on the doc, I adapted the namedtuple() demo to come up with the expected output for my exercise using the following:

>>> from collections import namedtuple
>>> BeltStats = namedtuple('BeltStats', 'score ninjas')
>>> ninja_belts = {'yellow': BeltStats(50, 11),
...                'orange': BeltStats(100, 7),
...                'green': BeltStats(175, 1),
...                'blue': BeltStats(250, 5)}
>>> BY = ninja_belts['yellow']
>>> BO = ninja_belts['orange']
>>> BG = ninja_belts['green']
>>> BB = ninja_belts['blue']
>>> test = (BY[0]*BY[1] + BO[0]*BO[1] + BG[0]*BG[1] + BB[0]*BB[1])
>>> print(test)
2675
Notice the result at the very end there? It says: 2675. This matches the expected output. Hooray! I did it? Well, almost. I successfully manipulated the dictionary to calculate the desired output. My problem is how to reformat those operations into a function inside an actual script to automate that output. I’m completely lost trying to do that.

Without providing the solution for me, what hints or comments would you people be able to suggest to help nudge me in the right direction?

I came up with my shell ‘solution’ (above) based on Udemy courses I’ve been taking by Fred Batiste and Jose Portilla. I can’t share the specific lectures/lessons on dictionary manipulation because it is paywalled. Jose Portilla’s course in particular primarily uses Jupyter Notebooks which kinda explains why my Python shell chops are superior when compared to me trying to write actual scripts.

For more about this exercise and what I am trying to achieve, see this:

Quote:In this Bite you calculate the total amount of points earned with Ninja Belts by accessing the given ninja_belts dict.

You learn how to access score and ninjas (= amount of belt owners) from no less than a namedtuple (if you're new to them, check out the basic Point example in the docs).

Why a namedtuple, you did not even mention a tuple yet?!

Good point, well in our Bites we might actually use them even more so let's get to know them here (if you have a free evening read up on the collections module as well and thank us later).

The function returns the total score int. You learn to write generic code because we test for an updated ninja_belts dict as well, see the TESTS tab.
Reply
#2
You can change this code:
test = (BY[0]*BY[1] + BO[0]*BO[1] + BG[0]*BG[1] + BB[0]*BB[1])
Into this:
import math

test = sum(map(math.prod, ninja_belts.values()))
math.prod is like sum, but instead it's calculating the product of values.
ninja_belts.values() return as dict_values iterator.
The map function takes each element from ninja_belts.values() and call each element with math.prod.
The sum function consumes the map.

If you just want to get a new list with products, without building the sum:
import math

products = list(map(math.prod, ninja_belts.values()))
math.product were introduced with Python 3.8
Almost dead, but too lazy to die: https://sourceserver.info
All humans together. We don't need politicians!
Reply
#3
My compliments DeaD_EyE, your answers are always very informative. But it is not what Drone asks.
(Jul-03-2020, 09:01 AM)Drone4four Wrote: My problem is how to reformat those operations into a function inside an actual script to automate that output. I’m completely lost trying to do that.

Drone4four, I do not understand your question. You can, also in Jupyter, fill in your code and save it? You must have learned how to define a function? The result should be something like:
from collections import namedtuple
 
BeltStats = namedtuple('BeltStats', 'score ninjas')
 
ninja_belts = {'yellow': BeltStats(50, 11),
               'orange': BeltStats(100, 7),
               'green': BeltStats(175, 1),
               'blue': BeltStats(250, 5)}

def add_stats(some_dict):
    test=0
    "add your code here
    "also study Dead_eye's advice
    return test

print(add_stats(ninja_belts))
Reply
#4
(Jul-03-2020, 11:29 AM)ibreeden Wrote:
(Jul-03-2020, 09:01 AM)Drone4four Wrote: My problem is how to reformat those operations into a function inside an actual script to automate that output. I’m completely lost trying to do that.
Drone4four, I do not understand your question. You can, also in Jupyter, fill in your code and save it? You must have learned how to define a function? The result should be something like:

def add_stats(some_dict):
    test=0
    "add your code here
    "also study Dead_eye's advice
    return test
print(add_stats(ninja_belts))

I didn’t do a very good job explaining what I meant when I said that I am lost trying to write my own function. The basic syntax of function definitions and returning their values is obvious and very clear in my mind. It’s filling the space in between the def add_stats(some_dict): and return test that I am struggling with.

What I was asking for was a nudge and a hint or two pointing me in the direction of my next baby steps in exploring how to model slicing dictionaries involving namedtuples. But then, @DeaD_EyE, you went ahead and completed my Python exercise for me on the subforum specifically for students who don’t want other forum members solving their Python problems.

DeaD_EyE: You've done this before to me. See my previous thread titled “Rounding exercise: UnboundLocalError: local variable referenced before assignment” (Post #3). In my original post in that thread I specifically asked: “What kind of hints or advice could you people provide without giving me the solution entirely?” Yet, you then proceeded to provide me with 3 complete answers.

DeaD_EyE: Please keep your sophisticated elegant best practice Python algorithms for the main “General Coding Help” where they belong. Like other Python novices here on the homework forum, I am looking for hints and nudges to write my own basic, rudimentary and NAIVE algorithms. That’s a given. That’s expected. Common, my friend. If I ask another question here again next week and you do my exercise for me when I clearly asked you not to, I’ll be reaching out to a mod.
Reply
#5
(Jul-04-2020, 12:37 AM)Drone4four Wrote: I didn’t do a very good job explaining what I meant when I said that I am lost trying to write my own function. The basic syntax of function definitions and returning their values is obvious and very clear in my mind. It’s filling the space in between the def add_stats(some_dict): and return test that I am struggling with.

Well it is easier than you think. You already found the lines that had to be added:
(Jul-03-2020, 09:01 AM)Drone4four Wrote: >>> BY = ninja_belts['yellow']
>>> BO = ninja_belts['orange']
>>> BG = ninja_belts['green']
>>> BB = ninja_belts['blue']
>>> test = (BY[0]*BY[1] + BO[0]*BO[1] + BG[0]*BG[1] + BB[0]*BB[1])

Without the ">>> " of course and with just as many spaces before them as test=0 and return test. (4 spaces is the rule for indentation.)
Reply
#6
Hi @ibreeden: Thank you for your reply! I inserted those lines from my REPL into the function in my script. I ran the pytest script. It passes the first half. Hooray! I’ve made progress. The expected result matches the result produced inside my function. So this is a good first step.

But there is more to this exercise. My next step to complete the exercise is to write a function which can accept (alternate) dynamic input from other variables such as a variable like this:

more_belts = dict(brown=BeltStats(400, 2),
                 black=BeltStats(600, 5))
To better illustrate the task in full, here is the pytest script I am working with:

from belts import BeltStats, ninja_belts, get_total_points

def test_get_total_points_given_belts():
   assert get_total_points(ninja_belts) == 2675


def test_get_total_points_more_belts():
   more_belts = dict(brown=BeltStats(400, 2),
                     black=BeltStats(600, 5))

   # this way to dict merge is >= 3.5 (PEP 448)
   ninja_belts_updated = {**ninja_belts, **more_belts}

   assert get_total_points(ninja_belts_updated) == 6475
When I run this pytest script, the first of two functions passes. My current iteration of the script looks like this:

from collections import namedtuple

BeltStats = namedtuple('BeltStats', 'score ninjas')

ninja_belts = {'yellow': BeltStats(50, 11),
              'orange': BeltStats(100, 7),
              'green': BeltStats(175, 1),
              'blue': BeltStats(250, 5)}


def get_total_points(belts=ninja_belts):
   BY = ninja_belts['yellow']
   BO = ninja_belts['orange']
   BG = ninja_belts['green']
   BB = ninja_belts['blue']
   test = (BY[0]*BY[1] + BO[0]*BO[1] + BG[0]*BG[1] + BB[0]*BB[1])
   return test
The problem with this is that it is far too specific. Here I micromanage the dictionary slicing. As the description quoted in my original post, the exercise calls for me to write my function so that it is generic. The function needs to return an integer of 6475 with different parameters. The exercise also suggests to loop over a dictionary.

In terms of my exploration on Google so far, I found Dan Bader’s Real Python tutorial titled “How to Iterate Through a Dictionary in Python” which exhaustively covers all the many different approaches of looping over dictionaries. Since I am trying to calculate the product of the contents of the namedtuple, in Dan Bader’s guide I figure the most relevant explanations are the two titled: “Doing Some Calculations” as well as the one on Python’s builtin “map()” function. So I combined these two sub-lessons together and adapted them to my task at hand. Here is my latest attempt assembling everything I’ve learned so far:

from collections import namedtuple

BeltStats = namedtuple('BeltStats', 'score ninjas')

ninja_belts = {'yellow': BeltStats(50, 11),
              'orange': BeltStats(100, 7),
              'green': BeltStats(175, 1),
              'blue': BeltStats(250, 5)}

def get_total_points(belts=more_belts):
   total = 0
   for value in belts.values():
       # Add the the multiplied values together:
       total = sum(BeltStats[value[0]]*[value[1]])
   return total
That’s my best attempt. When I run my test script, I get this traceback:

Error:
$ python -m pytest test_belts.py ======================================== test session starts ========================================= platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 rootdir: /home/gnull/dev/projects/python/2018-and-2020/bitesofpy/Intro-freebies-101-110/inprogress/Bite 108 - Loop over a dict of namedtuples calculating a total score collected 0 items / 1 error =============================================== ERRORS =============================================== ___________________________________ ERROR collecting test_belts.py ___________________________________ test_belts.py:1: in <module> from belts_mine2 import BeltStats, ninja_belts, get_total_points belts_mine2.py:10: in <module> def get_total_points(belts=more_belts): E NameError: name 'more_belts' is not defined ====================================== short test summary info ======================================= ERROR test_belts.py - NameError: name 'more_belts' is not defined !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
It’s a name error pointing to the way I have imported the ‘belts_mine2’ module inside the test_belts.py script. That line right now looks like this:

 from belts_mine2 import BeltStats, ninja_belts, get_total_points 
The name of my module in the working directory is : belts_mine2.py. My linter in my text editor is highlighting that line as well but as far as I can tell, my script name and import line are accurate.

I realize that resolving the import error is kinda ulterior and is just a tiny obstacle interfering with the real problem that I’d rather be troubleshooting - - how to loop over a dictionary with namedtuples. Once the import error is resolved, I am looking forward to troubleshooting my dictionary for loop.

For the dictionary for loop, I am asking for * * several hints and nudges in the right direction * *, not for a complete solution. So @DeaD_EyE, if you are going to reply, I am begging you to not give me the full solution. This is not a homework assignment for school, but I am pretending that it is.
Reply
#7
(Jul-09-2020, 02:01 PM)Drone4four Wrote: def get_total_points(belts=ninja_belts):

Remember: a function does nothing until it is called. In the definition of the function you must give a generic name for the parameter (belts). Like:
def get_total_points(belts):
And after the function is defined you can call the function with the actual parameter:
get_total_points(ninja_belts)
It is possible to give a default for a formal parameter but then you must give real values:
def get_total_points(belts={'yellow': BeltStats(50, 11),
              'orange': BeltStats(100, 7),
              'green': BeltStats(175, 1),
              'blue': BeltStats(250, 5)}):
But that is not what you want. You want a generic function.
Reply
#8
(Jul-09-2020, 02:01 PM)Drone4four Wrote:
   test = (BY[0]*BY[1] + BO[0]*BO[1] + BG[0]*BG[1] + BB[0]*BB[1])

Note that the good thing about using namedtuple is that the fields in the object can be accessed by name, as well as index. I suggest using the former, because it helps make your code more readable.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Dictionary lookups exercise (PyBite #109) Drone4four 14 5,769 Aug-10-2020, 06:41 PM
Last Post: Intr0spective
  Type conversion and exception handling (PyBite #110) Drone4four 5 32,169 Jul-27-2020, 11:33 AM
Last Post: DeaD_EyE
  "Slicing and dicing strings" - - PyBite #105 Drone4four 8 4,303 Jun-11-2020, 09:28 PM
Last Post: knackwurstbagel
  Help with homework problem - iterating a function midnitetots12 4 3,479 Feb-21-2018, 10:51 PM
Last Post: nilamo

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020