Adding op.unpack to the operator module

why does the operator module not have a function to unpack?
something like,

import operator as op
l = [1, 2]
op.unpack(l)
1 2

which would be the substitute for the non functional way of unpacking using *

plus why does this throw a syntax error?

def unpack(args):
  return *args

I’m not a Python expert but isn’t unpacking something that happens as part of assignment and not an expression by itself.

I guess my question would be what do you expect the value of this to be:

type(op.unpack([1, 2]))

probably, running type on it should throw an error,
like this,

TypeError: type() takes 1 or 3 arguments

it would be used in situations like these,

import operator as op
x = op.eq(op.unpack(list(map(str.upper, ['a', 'A']))))

where otherwise the substitute is,

x = op.eq(*map(str.upper, ['a', 'A']))

and I do not know if there is a functional equivalent to replacing the * here.

Unary * is not an operator. *args is not an expression.

then where else would this function be appropriate.
plus

def unpack(args):
    print(*args)

does not throw an error, is there a way to return the unpacked arguments also?

I think you are a bit confused. In Python functions can only return one object. A function foo() defined like this:

def foo():
    return 1, 2, 3

return a single object: a tuple of three integers. The *args notation is used to tell Python to pass the elements of an iterable object as positional arguments to a function. Similarly, the **kwargs notation instructs Python to pass the elements of a dictionary as keyword arguments to a function. These are not operators, but simply syntactic support these two operations.

It is not clear to me what you would like the hypothetical operator you are asking for to do. If you have a tuple or list of arguments you want to pass to a function you write func(*arguments). Otherwise you can handle it as a regular tuple or list. If you want to assign the content of the list to local variables, you do

arguments = ('a', 'b', 'c', 'd')
first, second, *others = arguments

Please note that the * here has a different meaning.

Of course you can combine these two things. Assuming the previously define function foo():

one, two, three = foo()

and one == 1, two == 2, three == 3.

I hope this helps a bit.

2 Likes

my main aim is to avoid using * in places like these,

x = op.eq(*map(str.upper, ['a', 'A']))
l = [['x', 'y'], [1, 2]]
print(dict(zip(*l)))

one way to do it is by using functools.reduce

d = functools.reduce(op.eq, map(str.upper, ['a', 'A']))
e = dict(functools.reduce(zip, l))

but both the above ways appear less readable than something like this,

x = op.eq(unpack(list(map(str.upper, ['a', 'A'])))) 
y = dict(zip(unpack(l)))

but how do I implement this unpack function which acts as a substitute for *

Moving this to users category as it seems to be about learning a core concept of Python rather than an idea that can actually lead to a change to Python.

2 Likes

Use return args.

You can use return (*args,) to unpack the args into a tuple, but they are already a tuple, so that’s a total waste of time.

1 Like

As @daniele mentioned, a function can only return a single value. You can return a tuple/list of values but the thing you want is impossible to do with a function. I think, this can only be done with a new keyword, but that would be really unnecessary.

If you don’t want to use asterisks -I don’t agree with you at this point, it is really easy to use and has a clean syntax-, then, you should handle(redefine) the functions you want to use. Here, I created an example code for you:

class unpack:
	def __init__(self,iterator):
		self.iterator = iterator
		
def func_handler(*funcs):
	redefined_funcs = []
	for func in (*funcs,):
		def new_func(*args, func = func):
			all = []
			for i in (*args,):
				if type(i) == type( unpack([]) ): all.extend([a for a in i.iterator])
				else: all.append(i)
			return func(*all)
		redefined_funcs.append(new_func)
	return redefined_funcs if len(redefined_funcs)>1 else redefined_funcs[0]
	
##EXAMPLE:
def foo(x,y,z,t,w):
	return x+y+z+t+w
def bar(a,b,c):
	return a*b*c
_foo, _bar = func_handler(foo, bar)
print(   _foo(  unpack( [1,2] ), 3, unpack( [4,5] )  )   )
print(   _bar(  10, unpack( [3,5] )  )   )
print(   _foo( unpack([10,10,10,10,10]) )   )

#or, you can use a "tricky" way to save functions' original names:
#_foo, _bar = foo, bar
#foo, bar = func_handler(_foo, _bar)

yes, I agree the problem here is that a function cannot return multiple objects.
so,

def func():
    return 1, 2, 3
type(func())

gives,

tuple

for unpack to work, it should return multiple objects, so it cannot be a function.

a hypothetical unpack should work this way,

def unpack(args):
    return *args
type(unpack(['a', 1])
str, int

but there is no way to do this, plus the number of objects returned would also not be fixed, like,

type(unpack(['a', 1, True])

should give,

str, int, bool

so, unpack cannot be a function.
i dont think so there is anyway to achieve this, so, there probably is no direct substitute to using *, apart from the functools.reduce way. which is again not that readable in my opinion.

to me, the use of * for unpacking does not appear to be part of functional programming, that is the main reason for me not wanting to use it, rest there is no other issue with it.

The functional programming approach would be to define apply and use that:

import operator as op

def apply(f, args):
    return f(*args)

x =  apply(op.eq, map(str.upper, ['a', 'A']))

But since Python 2.3 using * is preferred and there is no reason to use apply.

3 Likes

I’m not sure what the “functional programming” exactly is, but Python never returns multiple-value like lisp. The * syntax is useful, but you can also do that complexity without * using partial.

>>> from functools import partial
>>> class apply(partial):
...     def __rmatmul__(self, argv):
...         return self(argv)
... 
>>> import operator as op
>>> p = apply(print)
>>> u = apply(map, str.upper)
>>> eq = apply(reduce, op.eq)

Usage:

>>> p(eq(u(('a','A'))))
True
>>> ('a','A') @u @eq @p
True

The second flavor could be useful for typing on REPL.

@vainaixr Are you aware of itertools.starmap (in case that helps)?

yes, I am aware of it, but where I was using asterisk, i think the apply + map technique appears to be the most readable way to me.

Hello again. If you are still interested in, my friend has recently uploaded a simple package about the same topic. You can have a look at it.
Github Repository: GitHub - SoilLifeL/unpacktools: This package unpacks iterators without the need of asterisk.
Note: You may find it a bit noob-job at first, but it works fine.

thanks for the package, but currently I am sticking to the standard library.
as I would not like to use an external package for my use case.

1 Like

the use of apply could probably be used as a substitute to *args, **kwargs, like this,

def apply(f, args, kwargs):
  return f(*args, **kwargs) # only need to use * three times here, and not again
def food(**kwargs): # unfortunately, will have to specify * here
  for items in kwargs:
    print(f"{kwargs[items]} is a {items}")
      
      
dict_1 = {'fruit' : 'cherry', 'vegetable' : 'potato', 'boy' : 'srikrishna'}
# using asterisk
food(**dict_1)
# no asterisk
apply(food, '', dict_1) # if had to specify no keyword arguments 
                        # then would have to pass {}, like apply(f, args, {})

there are a few more places where I see the use of *,
I would like to completely avoid using *.
for the common use cases like,

2 * 3, 2 ** 3

one could use,

import operator as op
op.mul(2, 3), op.pow(2, 3)

for a case like this,

x = {'a': 1, 'b': 2}
y = {'c': 3, 'd': 4}
z = {**x, **y}

I could use a ChainMap,

from collections import ChainMap
z = ChainMap(x, y)

but for cases like these,

a, b, *c = [1, 2, 3, 4]

the only substitute that appears is to change the list itself

a, b, c = [1, 2, [3, 4]]
numbers = [2, 1, 3, 4, 7]
more_numbers = [*numbers, 11, 18]

probably no substitute here, as,

more_numbers = [numbers[0:len(numbers)], 11, 18]
more_numbers = [[i for i in numbers], 11, 18]

lead to creating new list, (could also have been a set),

more_numbers
[[2, 1, 3, 4, 7], 11, 18]

something like this,

more_numbers = [i for i in numbers, 11, 18]

is invalid

Why are you trying to do this? * is part of Python’s syntax, trying to avoid it specifically makes as much sense as say avoiding using e anywhere.

the main reason for me to avoid the use of * is that there appear to be multiple meanings associated with *.

when I see, let us say,
+,
then I know it would be used for either of these,

  1. addition
  2. concatenation of lists, strings, tuples
  3. in f-strings, word = 1; print(f"{word:+d}") which outputs, +1

it does not have as many meanings as *.

the same * is used,

  1. for multiplication
  2. for power
  3. to unpack an iterable
  4. to capture positional arguments as a tuple
  5. to capture keyword arguments as a dictionary
  6. they even use it like this,
def func(a, b, *, c='d'):
    pass

there might even be more …

that is why I would like to avoid using it.

it appears to have more meanings than other symbols, for example,

@

  1. matrix multiplication
  2. for decorator like,
@deco
def f():
    pass

& - and
~ - inversion
; - in places like a = 1; b = 2
\ - in places like these, print('\n')

,

  1. to seperate between two values
  2. in f-strings, like num = 123456789; print(f"{num:,d}") which outputs, 123,456,789

|

  1. or
  2. to combine two dictionaries/sets, (just got to know about it, could use this instead of ChainMap in the last post)

#

  1. comment
  2. in f-strings, like, number = 32; print(f'{number:#b}') which outputs, 0b100000

!

  1. to check for not equal
  2. in f-strings, name = 'Fred'; f'{name!r}' which outputs, "'Fred'"
    although this could be avoided using something like f'{repr(name)}'

' or "

  1. to indicate string, like 'abc' or "abc"
  2. ''' or """ for comment with more than one line

.

  1. attribute access
  2. decimal to indicate float
  3. in ellipsis like ...
  4. in f-strings, like,
    word = 'hello'; number = 32.32; print(f'{word:.2} {number:.1f}')
    which outputs, he 32.3

_

  1. to indicate that dont care about the name
  2. for something like this,
    x = 1; x
    _
    which outputs 1 here, _ means x
  3. in numbers like 1_000_000
  4. in function/class/… name, including sunder, dunder method names
  5. in f-strings, like, num = 123456789; print(f"{num:_d}") which outputs, 123_456_789

=

  1. assignment, like x = 2
  2. comparison, like x == 2, x >= 2, x <= 2
  3. in place assignment, like x += [1]
  4. with walrus, like (x := 1)
  5. in f-strings, like, x = {'a': 1, 'b': 2}; print(f'{x["a"]=}')
    which outputs, x["a"]=1

:

  1. at the end of if/else/for/function/class/match/case/try/except
  2. walrus operator
  3. annotation, like,
def func(x: int):
    pass
  1. for slicing
  2. to create key value pairs in dictionary
  3. in f-string, word = 'hello'; print(f"{word:_>10}")
    which outputs, _____hello

^

  1. xor
  2. in f-string, word = 'hello'; print(f"{word:_^9}")
    which outputs, __hello__

()

  1. to create a tuple
  2. for generator expression
  3. to specify arguments in function

[]

  1. to create a list, list comprehension
  2. in docs to specify arguments to a function
  3. in places like these,
class Starship:
    stats: ClassVar[Dict[str, int]] = {}
  1. to getitem, slice

{}

  1. to create a set (like {*()}), set comprehension
  2. to create a dict, dict comprehension
  3. in f-strings, like, x = 1; print(f'{x}') which outputs, 1

/

  1. truediv
  2. floordiv
  3. in function like this,
def func(a, b, /, c='d', *, e='f'):
    pass

%

  1. modulo
  2. in ipython, like %%time, or %timeit
  3. in places like these, print("%s %s"
  4. continued from last point, % (name, age))
    although point 3 and 4 could be substituted by f-strings.

-

  1. subtract
  2. minus of a number
  3. in places like these,
def func() -> int:
    return 1

>

  1. greater than, greater than or equal to
  2. third point of last one
  3. right shift
  4. point 6 of :

<

  1. less than, less than or equal to
  2. left shift
  3. point 6 of : with <

(space)

  1. to separate
  2. to indent

(assume we do not consider regex, probably should not consider f-strings also, as it is like a mini language on its own)

but * appears to have a higher number of meanings.