Managing *args variance in calls to functions - python

Have a method with the following signature:
def foo(self, bar, *uks):
return other_method(..., uks)
Normally this is called as:
instance.foo(1234, a, b, c, d)
However in some cases I need to do something like this:
p = [a, b, c, d]
instance.foo(1234, p)
At the receiving end this does not work, because other_method sees *args being made up of a single list object instead of simply a [a, b, c, d] list construct. If I type the method as:
def foo(self, bar, uks = []):
return other_method(..., uks)
It works, but then I'm forced to do this every time:
instance.foo(1234, [a, b, c, d])
It's not a huge deal I guess, but I just want to know if I'm missing some more pythonic way of doing this?
Thanks!

Python supports unpacking of argument lists to handle exactly this situation. The two following calls are equivalent:
Regular call:
instance.foo(1234, a, b, c, d)
Argument list expansion:
p = [a, b, c, d]
instance.foo(1234, *p)

p = [a, b, c, d]
instance.foo(1234, *p)
The *p form is the crucial part here -- it means "expand sequence p into separate positional arguments".

I think the answers you have here are correct. Here's a fully fleshed out example:
class MyObject(object):
def foo(self, bar, *uks):
return self.other_method(1, uks)
def other_method(self, x, uks):
print "uks is %r" % (uks,)
# sample data...
a, b, c, d = 'a', 'b', 'c', 'd'
instance = MyObject()
print "Called as separate arguments:"
instance.foo(1234, a, b, c, d)
print "Called as a list:"
p = [a, b, c, d]
instance.foo(1234, *p)
When run, this prints:
Called as separate arguments:
uks is ('a', 'b', 'c', 'd')
Called as a list:
uks is ('a', 'b', 'c', 'd')
You said on Alex's answer that you got ([a, b, c, d],), but I don't see how.

Related

Cleanly assign list values to multiple variables when there may not be enough values to unpack

For example, if I want to assign a, b, c from l = [1,2,3,4,5], I can do
a, b, c = l[:3]
but what if l is only [1,2] (or [1] or []) ?
Is there a way to automatically set the rest of the variables to None or '' or 0 or some other default value?
I thought about extending the list with default values before assigning to match the number of variables, but just wondering if there's a better way.
In general, to unpack N elements into N separate variables from a list of size M where M <= N, then you can pad your list slice upto N and slice again:
l = [1,]
a, b, c = (l[:3] + [None]*3)[:3]
a, b, c
# 1, None, None
If you fancy a clean generator-based approach, this will also work:
from itertools import islice, cycle, chain
def pad(seq, filler=None):
yield from chain(seq, cycle([filler]))
a, b, c = islice(pad([1, ]), 3)
a, b, c
# 1, None, None

Iterate a tuple with dict inside

Having trouble iterating over tuples such as this:
t = ('a','b',{'c':'d'})
for a,b,c in t:
print(a,b,c) # ValueError: not enough values to unpack (expected 3, got 1)
for a,b,*c in t:
print(a,b,c) # ValueError: not enough values to unpack (expected 2, got 1)
for a,b,**c in t:
print (a,b,c) # Syntax error (can't do **c)
Anyone know how I can preserve the dictionary value? I would like to see a='a', b='b', and c={'c':'d'}
You'd need to put t inside some other iterable container:
for a, b, c in [t]:
print(a, b, c)
The problem with your attempts is that each on is iterating over a single element from t and trying to unpack that. e.g. the first turn of the loop is trying to unpack 'a' into three places (a, b and c).
Obviously, it's also probably better to just unpack directly (no loop required):
a, b, c = t
print(a, b, c)
Why are you iterating at all when it's a single tuple? Just unpack the single tuple, if that's what you need to do:
a, b, c = t
print(a, b, c)
Or if it's just printing you want to do unpack in the call itself:
print(*t)
Try this:
for a,b,c in [t]:
print(a,b,c)
Putting t inside a list will allow you to unpack it.
You can use tuple unpacking with multiple assignment:
a, b, c = t
print(a, b, c)
try this
for x in t:
print(x)
x will take all the values in t iteratively, so 'a', then 'b' and finally {'c':'d'}.
And to print exactly a = 'a' etc you can do:
for param, val in zip(["a","b","c"], t):
print(param,"=",val)

python membership testing order by rarity

suppose I have a list X that contains a bunch of different items and I am testing if it contains any of the following: (a, b, c).
If a occurs vastly more often than b, which in turn is more common than c, is there a way to force
any(True for v in X if v in (a, b, c))
to check for a first so that it can return faster?
if a in X or b in X or c in X:
runs much faster than the any() statement but it’s messy and not extensible.
any(v in X for v in (a, b, c))
You've put together your any in a pretty weird way. This way gets the effect you want.
If you wanted to do the checks in the way your existing code does it (for example, if earlier elements of X were more likely to match), it'd be cleaner to do
any(v in (a, b, c) for v in X)
If instead of 3 elements in (a, b, c), you have quite a lot, it'd be faster to use a set:
not {a, b, c}.isdisjoint(X)
Did you try?
any(True for v in (a, b, c) if v in X)
That form seems more comparable to your faster example.

How to combine tuples from two generators in python

I want to use two generators in a single for loop. Something like:
for a,b,c,d,e,f in f1(arg),f2(arg):
print a,b,c,d,e,f
Where a,b,c,d and e come from f1 and f comes from f2. I need to use the yield operator because of space constraints.
The above code however doesn't work. Due to some reason it keeps on taking values (for all six variables) from f1 until it is exhausted and then starts taking values from f2.
Please let me know if this is possible and if not is there any workaround. Thank you in advance.
You can use zip (itertools.izip if you're using Python 2) and sequence unpacking:
def f1(arg):
for i in range(10):
yield 1, 2, 3, 4, 5
def f2(arg):
for i in range(10):
yield 6
arg = 1
for (a, b, c, d, e), f in zip(f1(arg), f2(arg)):
print(a, b, c, d, e, f)

Python lambda parameters

I have the following code:
g = lambda a, b, c: sum(a, b, c)
print g([4,6,7])
How do I get the lambda function to expand the list into 3 values?
Expand the list t0 3 values can be done by this:
g(*[4,6,7])
But the sum won't work in your way.
Or you can write this way:
>>> g = lambda *arg: sum(arg)
>>> print g(4, 5, 6)
15
>>>
Or just make your lambda accept a list:
g = lambda alist: sum(alist)
print g([4,6,7])
Why can't you change lambda to take a list. Because sum() doesn't take three arguments:
>>> g = lambda a_list: sum(a_list)
>>> print g([4,6,7])
17
or a non-keyword argument:
>>> g = lambda *nkwargs: sum(nkwargs)
>>> print g(4,6,7)
17
g = lambda L: sum(L)
print g([4,6,7])
would work for any arbitrarily sized list.
If you want to use g = lambda a, b, c: someFunc(a, b, c), then call print g(4,6,7)
You're defining a function which takes three parameters, and the supplying it with a single parameter, a list.
If you want
print g([4,6,7])
to work, your definition should be
g = lambda lst: sum(lst)
The code you are looking for is:
>>> g = lambda a, b, c: sum([a, b, c])
>>> print g(*[4,6,7])
17
What you were trying to do wont work:
>>> g = lambda a, b, c: sum(a, b, c)
>>> print g(*[4,6,7])
Traceback (most recent call last):
File "<pyshell#83>", line 1, in <module>
print g(*[4,6,7])
File "<pyshell#82>", line 1, in <lambda>
g = lambda a, b, c: sum(a, b, c)
TypeError: sum expected at most 2 arguments, got 3
Because sum() can't handle the arguments you gave it.
Since your lambda function is simply a sum() function, why not just call sum() directly?
If your code the following a, b, c values:
>>> a, b, c = range(1,4)
>>> print a,b,c
1 2 3
And you wanted to do:
>>> g = lambda a, b, c: sum([a, b, c])
>>> print g(*[a,b,c])
6
Why not just do the following?:
>>> sum([a,b,c])
6
>>> g=lambda *l: sum(l)
>>> print g(1,2,3)
6
If your lambda expects to have a list/tuple of fixed length passed in, but wants to expand the values in that list/tuple into separate parameter variables, the following will work.
g = lambda (a, b, c): a + b + c
g([4, 6, 7])
Note the parentheses around the parameter list.
This "feature" works in Python 2.x, but was removed in Python 3

Categories