-1

I'm sure this is a stupid error on my part but I'm sick of debugging so hoping someone can spot the problem. I've included the larger script for reference sake but the part that I'm struggling with is the method _get_subcat within Categories. When I attempt to print the results I get [<generator object Category._get_subcat at 0x000002873CFE76F0>]. Can someone explain this to me?

class GamesDict:
    def __init__(self):
        
        self._dic = json.load(open('full.json'))['eventGroup']
        self.categories = self._dic['offerCategories']

        self.export_runline()
        self.export_moneyline()
        self.export_total()
        self.export_propshomeruns()
        self.export_propshits()
        self.export_propstotalbases()

    def export_runline(self):
        self.runline = Runline().to_dict(self.categories)

    def export_moneyline(self):
        self.moneyline = Moneyline().to_dict(self.categories)
        
    def export_total(self):
        self.total = Total().to_dict(self.categories)

    def export_propshomeruns(self):
        self.propshomeruns = BatterPropsHomeruns().to_dict(self.categories)
        print(self.propshomeruns)

    def export_propshits(self):
        self.propshomeruns = BatterPropsHits().to_dict(self.categories)

    def export_propstotalbases(self):
        self.propshomeruns = BatterPropsTotalBases().to_dict(self.categories)
 
  
class Parser:
    CATEGORY = int
    SUBCATEGORY = int
    DISPLAY_NAME: str

    def to_dict(self, markets: Iterable[dict[str, Any]]) -> list:
        return [market for market in self._get_records(markets)]

    @classmethod
    def _get_records(cls, data: Iterable[dict[str, Any]]) -> Iterator[tuple]:
        raise NotImplementedError()


class Category(Parser):
    @classmethod
    def _get_records(cls, data: Iterable[dict[str, Any]]) -> tuple[tuple, ...]:
        for item in data:
            if item['offerCategoryId'] == cls.CATEGORY:
                yield from cls._get_subcatdes(item['offerSubcategoryDescriptors'])

    @classmethod
    def _get_subcatdes(cls, data: Iterable[dict[str, Any]]) -> tuple[tuple, ...]:
        for item in data:
            if item['subcategoryId'] == cls.SUBCATEGORY:
                yield cls._get_subcat(item['offerSubcategory'].get('offers'))

    @classmethod
    def _get_subcat(cls, data: Iterable[dict[str, Any]]) -> tuple[tuple, ...]:
        for item in data:
            for x in item:
                if x.get('isOpen',()):
                    yield from cls._get_data(x['outcomes']) # why does this not work? 
                        
    
class Runline(Category):
    CATEGORY = 493
    SUBCATEGORY = 4519
    DISPLAY_NAME = 'Run Line'
            
    @classmethod
    def _get_data(cls, market: dict[str, Any]) -> tuple[tuple, ...]:
        event_id = market['eventId']
        away_team, home_team = market['outcomes'][:2]
        away_abbr = TEAMS.get(away_team['label'])
        home_abbr = TEAMS.get(home_team['label'])

        away_odds = away_team['oddsDecimal']
        home_odds = home_team['oddsDecimal']
        away_line = away_team['line']
        home_line = home_team['line']
        return (
            (away_abbr, event_id, away_odds, away_line, BOOK),
            (home_abbr, event_id, home_odds, home_line, BOOK),
        )


class Moneyline(Category):
    CATEGORY = 493
    SUBCATEGORY = 4519
    DISPLAY_NAME = 'Moneyline'
                
    @classmethod
    def _get_data(cls, market: dict[str, Any]) -> tuple[tuple, ...]:
        event_id = market['eventId']
        away_team, home_team = market['outcomes'][:2]
        away_abbr = TEAMS.get(away_team['label'])
        home_abbr = TEAMS.get(home_team['label'])

        away_odds = away_team['oddsDecimal']
        home_odds = home_team['oddsDecimal']
        return (
            (away_abbr, event_id, away_odds, BOOK),
            (home_abbr, event_id, home_odds, BOOK),
        )


class Total(Category):
    CATEGORY = 493
    SUBCATEGORY = 4519
    DISPLAY_NAME = 'Total'
                
    @classmethod
    def _get_data(cls, market: dict[str, Any]) -> tuple[tuple, ...]:
        event_id = market['eventId']
        
        away_team, home_team = market['outcomes'][:2]
        line = away_team['line']
        over = away_team['oddsDecimal']
        under = home_team['oddsDecimal']
        return (
            (event_id, line, over, under, BOOK),
        )


class Props(Category):
    @classmethod
    def _get_data(cls, market: dict[str, Any]) -> tuple[tuple, ...]:
        event_id = market['eventId']

        cont1, cont2 = market['outcomes'][:2]
        player = cont1.get('participant','')
        line = cont1.get('line','')

        over = cont1.get('oddsAmerican','')
        under = cont2.get('oddsAmerican','')
        return (
            (player, event_id, line, over, under),
        )
    

class BatterPropsHomeruns(Props):
    CATEGORY = 743
    SUBCATEGORY = 6606
    DISPLAY_NAME = 'Home Runs'


class BatterPropsHits(Props):
    CATEGORY = 743
    SUBCATEGORY = 6606
    DISPLAY_NAME = 'Hits'

    
class BatterPropsTotalBases(Props):
    CATEGORY = 743
    SUBCATEGORY = 6606
    DISPLAY_NAME = 'Total Bases'



GamesDict()


Nick
  • 367
  • 4
  • 16
  • 1
    What are you expecting? You have a yield in your method. That makes it a generator. – quamrana Aug 21 '22 at 21:29
  • 2
    If you want to turn your generator into a list just call `list` on it: `print(list(my_generator()))` – Oli Aug 21 '22 at 21:34
  • 1
    Did you mean `yield from` not `yield` in `Category._get_subcatdes`? – Oli Aug 21 '22 at 21:38
  • After re-reading the question, I don't think any specific canonical about `yield` or generators can really answer it... because the question isn't clear. Please read [mre] and https://ericlippert.com/2014/03/05/how-to-debug-small-programs/. Instead of trying to make the actual code work (which quickly leads to being "sick of debugging"), focus on figuring out which parts of the code are actually required to cause the problem. – Karl Knechtel Aug 21 '22 at 21:58

1 Answers1

3

I think you meant for _get_subcatdes to yield from cls._get_subcat(...) instead of yield cls._get_subcat(...).

cls._get_subcat is a generator, so cls._get_subcat(...) returns an iterator (over the values yielded by _get_subcat), and that iterator is what _get_subcatdes will yield. yield from cls._get_subcat(...) would cause _get_subcatdes to successively yield the values produced by that iterator, which I think is what you're expecting.

A couple of notes:

  1. One of my debugging heuristics is that if I see that a generator is yielding an iterator instead of the iterator's values, I make sure that I didn't somehow forget to use yield from when I meant yield.

  2. But since OP went to the trouble of annotating the expected types, a more structured approach would be to actually hand this code over to a static type checker. I used mypy, and I had to make some adjustments to the annotations:

    • in class Parser, I replaced CATEGORY = int with CATEGORY: int (and the same with SUBCATEGORY).
    • I changed the return type annotations for the generators from -> tuple[tuple, ...] to -> Iterator[tuple] (although I suppose I could have been more precise).
    • At the suggestion of mypy, I added from typing import Iterable, Iterator, Any.

    I probably could have done some more. That done, mypy reported various problems, two of which seem relevant:

    main.py:58: error: Incompatible types in "yield"
       (actual type "Iterator[Tuple[Any, ...]]", expected type "Tuple[Any, ...]")
    

    That's because yield was used instead of yield from.

    main.py:64: error: "str" has no attribute "get"
    main.py:65: error: Invalid index type "str" for "str";
       expected type "Union[SupportsIndex, slice]"
    

    Those (which might answer the question "why does this not work?") are the result of using for x in item where item is a dict, which iterates over the dictionary's keys (which are of type str). I'm pretty sure the intent was to iterate over item's values.

    There are lots more errors and warnings which would be useful to attend to, but for me it's a clear example of the value of static type-checking. Particularly if you've already gone to the trouble to annotate.

rici
  • 234,347
  • 28
  • 237
  • 341
  • If that's the problem, shouldn't this be considered a typo, and closed as such? OP evidently already understands how to use `yield from`, as it correctly appears elsewhere in the code. – Karl Knechtel Aug 21 '22 at 21:58
  • Thank you so much! That solved the problem. @KarlKnechtel I'm still in the process of learning yield/yield from and actually thought the issue might be due to something else so the answer really helped. – Nick Aug 21 '22 at 22:07
  • This is a good time to study better debugging techniques that don't lead to feeling "sick". Trying to create a MRE is usually a good first step. If part X of the data is processed incorrectly, then the first step is to try editing the code to remove the parts that process Y and Z, and removing those parts from the data (and making as simple of an X example data as can still reproduce the problem). – Karl Knechtel Aug 21 '22 at 22:08
  • 1
    I'm sure my debugging techniques are far from perfect but I obviously provided a good enough example for someone to be able to explain what the problem was and how to fix it. Isn't that what this site is for? – Nick Aug 21 '22 at 22:15
  • @KarlKnechtel: I'm not sure whether there is a precise definition of what a "typo" is, but I think in borderline cases it is more useful to answer the question. Certainly OP is not the only person in history who has made this particular error and been puzzled by the consequences, which are possibly not immediately evident to a novice pythonista. At least, it's worth remembering that if you expect values from a subgenerator and you instead receive the generator itself, your first step is to check if you've left out the `from` in the `yield from`. Maybe I should have said that in the post. – rici Aug 21 '22 at 22:51
  • @Nick: please take a look at the edit I just made to that answer. It might prove useful. – rici Aug 21 '22 at 23:26
  • @karl: in an attempt to make the answer more widely useful, I added a note about using mypy to catch errors like this. – rici Aug 21 '22 at 23:29
  • "Isn't that what this site is for?" *In fact, no*; the site is for building a library of searchable questions that can help others. In part, this entails stripping out details that won't be relevant to others; and it entails not keeping around questions caused by simple oversights (you can, of course, ask a question where you already know the answer; but an appropriately *clear* version of that question will not intermingle it with that knowledge); and it entails cleaning up duplicates. This is **not** a debugging service. See [mre] (again) and the [tour] for more. – Karl Knechtel Aug 22 '22 at 00:08
  • @rici Thank you again. You've been a big help. – Nick Aug 22 '22 at 00:14
  • @KarlKnechtel Fair enough. Could you elaborate or rephrase your point about asking a question you already know the answer to? I've done that before in situations where I'm just looking for a more pythonic/efficient way to solve a problem and the post would get flagged. – Nick Aug 22 '22 at 00:16
  • You can even [answer your own question](https://stackoverflow.com/help/self-answer). If you are looking for "a more pythonic way" then your question might be off topic unless you are looking for a very specific technique (and in that case it is likely a duplicate). We don't deal with subjective questions about code style or elegance here; try [codereview.se]. If you have a performance question, we need an objective metric (big-O complexity, runtime, memory usage, something else) to judge solutions. – Karl Knechtel Aug 22 '22 at 00:17