[Tutor] Playing with generators
dn
PythonList at DancesWithMice.info
Wed Aug 10 20:48:34 EDT 2022
On 10/08/2022 23.32, Leam Hall wrote:
> I'm wondering if there's a way to combine "make_person" with the
> generator in "for fname, peep in". Even if it can be done, should it be
> done? Or would the code be overly complex?
>
> The goal is to parse a group of data, and create a dict where the value
> is an object created from the data, and the key is an attribute of the
> object itself.
>
>
> class Person():
> def __init__(self, data = {}):
> self.fname = data.get('fname', 'fred')
> self.lname = data.get('lname', 'frank')
>
> data = [
> { 'fname':'Wilbur', 'lname':'Lefron' },
> { 'fname':'Al', 'lname':'Lefron' },
> { 'fname':'Jo', 'lname':'Franco' },
> { 'fname':'Mon', 'lname':'Pascal' },
> { 'fname':'Gray', 'lname':'Webb-Marston'},
> ]
>
> def make_person(data):
> ps = ( Person(d) for d in data)
> return ps
> peeps = {}
>
> for fname, peep in ( (p.fname, p) for p in make_person(data) ):
> peeps[fname] = peep
>
>
> for person in peeps.values():
> print(person.lname)
The stated code-goal is to create a dictionary of objects, where the key
is also an attribute of said-objects. Let's deal with that first,
because the code (above) is confusing, unnecessarily complex - and as a
result, somewhat obfuscatory.
In a presentation I'm creating for our local (but also virtual) PUG, I'm
encouraging folk to train themselves in 'new features' by experimenting
in Python's REPL (or PyCharm's Python Console, etc, etc).
Herewith a step-by-step, building-block-by-building-block, approach
which is appropriate at any time of day - not just those moments before
the first-coffee encourages synaptic-connections.
(with due apologies for appearing treat you, reader or OP, as a 'Beginner')
The REPL is your friend! In the REPL, the starting point is:
>>> class Person():
... def __init__(self, data = {}):
... self.fname = data.get('fname', 'fred')
... self.lname = data.get('lname', 'frank')
...
Without repeating advice (elsewhere) concerning the "data = {}", I'll
instead question the idea of providing default names for your person. If
data is missing (for reasonable or other reason), then the data will
become polluted in a confusing fashion - at least if the data was None,
it would be 'known' to be faulty (maybe!) and 'could' be found - how
could one distinguish between "fred" or "frank" that is correct-data and
the same which has been used in the name of 'completeness'?
fname and lname are understandable, but perhaps not the best choice in
identifier (IMHO). That said, any objection pales into insignificance in
contrast to the totally-meaningless "data", which follows:-
>>> data = [
... { 'fname':'Wilbur', 'lname':'Lefron' },
... { 'fname':'Al', 'lname':'Lefron' },
... { 'fname':'Jo', 'lname':'Franco' },
... { 'fname':'Mon', 'lname':'Pascal' },
... { 'fname':'Gray', 'lname':'Webb-Marston'},
... ]
Let's assume we can't see the above definitions: a basic for-loop will
iterate over the data and reveal its contents. The original code had an
additional loop to print/prove the results. Here the printing/proving
will be fitted-in, step-by-step, and providing visual-proof that we're
making progress towards a solution:-
>>> for person_as_dict in data:
... print( person_as_dict )
...
{'fname': 'Wilbur', 'lname': 'Lefron'}
{'fname': 'Al', 'lname': 'Lefron'}
{'fname': 'Jo', 'lname': 'Franco'}
{'fname': 'Mon', 'lname': 'Pascal'}
{'fname': 'Gray', 'lname': 'Webb-Marston'}
However, the objective is not to have the data represented as a dict,
but as attributes in a Person object. So:-
>>> for person_as_dict in data:
... person_as_class = Person( person_as_dict )
... print( person_as_class )
...
Person()
Person()
Person()
Person()
Person()
Yuk! That 'hides' the detail/doesn't demonstrate 'proof'. How to fix?
>>> class Person():
... def __init__(self, data = {}):
... self.fname = data.get('fname', 'fred')
... self.lname = data.get('lname', 'frank')
... def __str__( self ):
... return f"{ self.fname } { self.lname }"
...
>>> for person_as_dict in data:
... person_as_class = Person( person_as_dict )
... print( person_as_class )
...
Wilbur Lefron
Al Lefron
Jo Franco
Mon Pascal
Gray Webb-Marston
OK, so the work has been proven (and the class definition improved). The
next step is to assemble the dictionary (critique of "peeps" elsewhere -
one of the problems of using generic, cf specific, terminology):-
>>> people = dict()
>>> for person_as_dict in data:
... person_as_class = Person( person_as_dict )
... people[ person_as_class.fname ] = person_as_class
... print( person_as_class )
...
Wilbur Lefron
Al Lefron
Jo Franco
Mon Pascal
Gray Webb-Marston
Perhaps that debug-print should have been:
print( person_as_class.fname, people[ person_as_class.fname ] )
to truly claim to be "proof"?
If preferred/complete evidence is:-
>>> print( people )
{'Wilbur': <__main__.Person object at 0x7f5de21c3df0>, 'Al':
<__main__.Person object at 0x7f5de21c3f40>, 'Jo': <__main__.Person
object at 0x7f5de21c3f10>, 'Mon': <__main__.Person object at
0x7f5de21c3cd0>, 'Gray': <__main__.Person object at 0x7f5de21c3c70>}
In keeping it simple, the question of locating the generator-expression
in place of a for-loop's iterable 'disappears'. Obviously, it can be
done - you've done it! However, it has already been criticised as
difficult to comprehend - and now shown to be an unnecessary
affectation/disease of a sleep-befuddled mind - or evidence of a brain
way-smarter than mine!
The later-stated objective is:
> The task is to learn generators, and I tried to minimize the code to focus on my main question, "can a generator return a key and an object as a value, such that the key is an attribute of the value object?" My code, while it needs work, proves that it the generator can.
Thus, an exploration of "generators" (NB 'wider' than "generator
expression"). It seems, from the above, that a generator-expression is
unnecessary to achieve the first objective. So, we may be back to square
one, on that.
The advantage of a generator over an iterator is lazy-evaluation. Given
that "people" must exist, is there a storage-advantage to be gained by
using a generator in this scenario?
Given that people is a simple dictionary, accessing the Person-s within
is no challenge:-
>>> def standalone_generator():
... for person in people:
... yield people[ person ].fname, people[ person].lname
...
>>> for fname, lname in standalone_generator():
... print( fname, lname )
...
Wilbur Lefron
Al Lefron
Jo Franco
Mon Pascal
Gray Webb-Marston
Was one of the questions in your mind, the ability to yield multiple
values from a generator? The short answer is that you can AND you can't
- just as for any function. The explanation is that multiple values can
be return-ed/yield-ed, but (only) in the guise of a (single) tuple - as
demonstrated, above.
(am assuming you already know that whilst "manners maketh man", it is
the comma-separated list of identifiers/values that make(th) a tuple!
(and not the parentheses) If you/your colleagues find the parentheses
helpful, please go-ahead)
Back to the generator learning-challenge:
you could ask something like, which people have first-/last-names
beginning with a certain letter, or within a range of letters; but it's
a bit artificial!
To make it interesting, try adding another attribute to "data" and
Person, eg eye_color. Thereafter, you could try adding a
generator-method to Person which will provide the (?first) names of all
of the Person(s) with blue eyes*. This will require a generator which is
not merely a lazy-iterator!
The message there, is that generators are extremely powerful, but that
most of the training-course toy-examples are over-simplified and
typically offer situations where a simpler-solution exists. Until it
saves storage-space or reduces 'complication' to the retrieval,
generators probably don't come into their own. As always, YMMV!
* or: all of your respondents to whom you'd like to give 'black eyes'...
--
--
Regards,
=dn
More information about the Tutor
mailing list