Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A function-like variable

[Inspired by this question]

Suppose I have two lists:

list1 = ['tom', 'mary', 'frank', 'joe', 'john', 'barry']
list2 = [1, 2, 3, 4]

I want to match each name in list1 with a number in list2. Since there are four numbers in list2, I would like to pair each of the first four names to the corresponding number in list2, and assign random numbers to the remaining names in list1.

I know that I can solve this problem using a for-loop with enumerate and random.choice. Indeed, I've provided such a solution to the original problem.
I would like to know however, if it is possible to do something like this:

for name, number in itertools.izip_longest(list1, list2, fillvalue=MAGIC):
    print name, number

Originally, I thought of using something like this:

MAGIC = random.choice(list1)

However, that performs random.choice(list1) first, and then uses the answer as the fillvalue for the zipping operation. This is unattractive, as it does not choose a new random value for each pair of values that it zips. It is therefore clear, that itertools.izip_longest requires for its fillvalue, something that has a value by itself, which it doesn't call. Indeed, if I were to provide it a function, it would yield a pair consisting of a name, and a callable function, which is also undersired. For this reason, lambda functions are not feasible solutions.

How would I go about creating a variable that calls some function when it is called upon? How does itertools.izip_longest use the fillvalue variable? Is the __repr__ of that variable called? If so, can I make a class with a __repr__ that calls a function inside it?

like image 741
inspectorG4dget Avatar asked Dec 19 '22 18:12

inspectorG4dget


2 Answers

Seems the simplest approach here would be to create a generator that yields all the filler values for eternity, then chain this with yielding the values in list2:

def inf_gen(f):
    while True:
        yield f()

Note that inf_gen(f) is actually equivalent to iter(f, None), given that f never returns. iter with two arguments calls the first argument until it returns the second argument.

Then use it as such:

>>> from itertools import izip, chain
>>> for name, number in izip(list1,chain(list2, iter(lambda: random.choice(list2), None))):
    print name, number

tom 1
mary 2
frank 3
joe 4
john 1
barry 4

You can also do this just with itertools stuff without defining a separate function by using a generator comprehension with itertools.count:

>>> from itertools import izip, chain, count
>>> for name, number in izip(list1, chain(list2, (random.choice(list2) for _ in count()))):
    print name, number
like image 97
Claudiu Avatar answered Jan 02 '23 14:01

Claudiu


If you want it to "scale" to multiple iterators, you could do it with a slight modification to the code given in http://docs.python.org/2.7/library/itertools.html?highlight=izip_longest#itertools.izip_longest :

from itertools import repeat, chain, count

class ZipExhausted(Exception):
    pass

def izip_longest2(*args, **kwds):
    fillvalue = kwds.get('fillvalue')
    fillgen = kwds.get('fillgen', repeat(fillvalue))  # added
    counter = [len(args) - 1]
    def sentinel():
        if not counter[0]:
            raise ZipExhausted
        counter[0] -= 1
        yield next(fillgen)  # modified
    iterators = [chain(it, sentinel(), fillgen) for it in args]  # modified
    try:
        while iterators:
            yield tuple(map(next, iterators))
    except ZipExhausted:
        pass


a = ['bob', 'mary', 'paul']
b = ['eggs', 'spam']
c = ['a', 'b', 'c', 'd']

for x in izip_longest2(a, b, c, fillgen=count()):
    print '\t'.join(map(str, x))

# output:
# bob   eggs    a
# mary  spam    b
# paul  0   c
# 1 2   d
like image 26
Brave Sir Robin Avatar answered Jan 02 '23 14:01

Brave Sir Robin