0

I'm writing a command-line enhancer program. I chose to do this instead of using optparse, argparse, or the others available because: 1) I wanted the experience; and 2) I don't like them. ;)

A simple example:

@Script(
    live_daemon=Spec('test the installed daemon', FLAG, None, remove=True),
        verbose=Spec('more info on tests', FLAG),
        port=Spec('port for daemon to use for mini-tcp server', OPTION, type=int, remove=True),
        log=Spec('specific tests to log', MULTI, remove=True),
        tests=Spec('specific tests to run', OPTION),
        )
def main(live_daemon, verbose, log, port=8079, *tests):
    pass

As you can see from the port option above it is possible to specify the type of the argument that will be passed in to the function; the default type is str, but any callable is allowed.

One possibility not shown in the above example is a mapping:

@Script( random_vars=('for example', OPTION, type=(str, int)) )
def main(**random_vars):
    pass

Deep in the bowels of this utility program is a function that converts the incoming parameters to the default or requested types -- all it has to work with is a list to store the results, the function to use to do the conversion, and the data (which may be a singe item, or two items).

This is the sticky point: the default converter function should correctly handle any number of input items, and simply return them as-is -- rather than have a series of if/else blocks to select the correct default converter... in other words, an identity function.

The two lambdas I have now look like:

lamba x: x
lambda x, y: (x, y)

and what I would really like is a single function:

identity(x) --> x
identity(x, y) --> x, y
jfs
  • 399,953
  • 195
  • 994
  • 1,670
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • 1
    This might appear to be a duplicate of http://stackoverflow.com/q/8748036/208880, but in that question the OP accepted his own answer, which only gives the two wrong solutions. – Ethan Furman Oct 16 '14 at 00:38
  • ok. I've looked at the link. It appears to be the exact duplicate (as [@Ethan Furman's (OP) answer](http://stackoverflow.com/a/8748063/4279) demonstrates). – jfs Oct 16 '14 at 01:43
  • @J.F.Sebastian: rewrote question – Ethan Furman Oct 16 '14 at 02:31

1 Answers1

0

No, Python does not have an identity function, and is unlikely to get one.

What would one look like? Easier, perhaps, to answer what it should do:

something = ...
something is identity(something)

in other words, you should get back exactly what you put in, and, ideally, you should be able to put anything in.

A tempting, but inadequate, solution:

wrong_id1 = lambda x: x

The problem with this version is that we can only pass single items in, so this works:

>>> wrong_id1('a thing')
'a thing'

but this does not:

>>> wrong_id1('a thing', 'and another thing')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 1 argument (2 given)

The next tempting solution might be:

wrong_id2 = lambda *x: x

and this does work for multiple inputs:

>>> (1, 2, 3) == wrong_id2(1, 2, 3)
True

but no so well for single inputs:

>>> 9 == wrong_id2(9)
False

The problem is that in order to accept multiple inputs, *args must be used, but it has the side-effect of transforming even single-item inputs into a tuple, and the naive wrong_id2 simply returns that tuple:

>>> wrong_id2(9)
(9,)

Now that we know what the problems are, we can write a correct identity function:

def identity(*args):
    if len(args) == 1:
        # only one item passed in, return that one item
        return args[0]
    # many items passed in, return them as a tuple
    return args

Some simple tests to show it works as advertised:

>>> 'a thing' == identity('a thing')
True

>>> (1, 2, 3) == identity(1, 2, 3)
True

>>> a_tuple = 7, 8, 9
>>> a_dict = {True: 'Python Rocks!', False: 'up is down'}
>>> none = None
>>> (a_tuple, a_dict, none) == identity(a_tuple, a_dict, none)
True
>>> a_tuple is identity(a_tuple)
True
>>> a_dict is identity(a_dict)
True
>>> none is identity(none)
True

DaoWen has made some interesting points against this implementation of identity, the chief one being consistency of return value -- that is, if even the single item case returns a tuple, then we can always treat it the same: call len() on it, put it in a for loop, etc, etc.

While that can be very handy behavior, that is not an identity function, for the simple reason that 1 != (1, ) -- therefore, identity(1) should return 1 and not (1, ).

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • The whole issue with `lambda x: x` being a unary function doesn't make sense to me. What's wrong with having a separate identity function for scalars vs tuples? It should always be obvious which is the correct choice for a given situation. However, I can't actually think of a concrete situation where I'd need a non-unary identity function. – DaoWen Oct 16 '14 at 01:46
  • 1
    your `identity()` function doesn't work with `starmap()`. Why do you support multiiterable `map()` variant but not `starmap()` variant? What is the logic here? – jfs Oct 16 '14 at 01:54
  • On another note, your final "correct" identity function still seems broken. You really just need separate tuple vs scalar identity functions. Here's a simple example: `xs = [[1,2], [3], [4,5,6], [7], [8,9]]; ys = [ identity(*x) for x in xs ]`. The resulting value for `ys` is `[(1, 2), 3, (4, 5, 6), 7, (8, 9)]`. I would have expected 1-tuples to be returned for 3 and 7. As a result, this expression gives a type error: `zs = map(len, ys)`. *Edit:* I actually think that this is the same point that @J.F.Sebastian was making about `starmap`. – DaoWen Oct 16 '14 at 01:54
  • There are cases where you do not know ahead of time which you will need. In those cases you can either put in an if/else block to select the correct one, or just have a single function smart enough to handle both situations. – Ethan Furman Oct 16 '14 at 01:55
  • @DaoWen: Passing in `*x` is *not* the same as passing in `x`. Take out the `*` and you get exactly what you put in. – Ethan Furman Oct 16 '14 at 01:55
  • @EthanFurman - I don't see how that addresses the problem. Why would unpacking an argument list be an invalid case? `f(*[1])` should always be equivalent to `f(1)`, just as `g(*[1,2,3])` should be equivalent to `g(1,2,3)`. Again, I think the right answer here is that you need a separate scalar and tuple identity function. If you can think of a case where it wasn't clear which one to use, I'd be very interested to see it. – DaoWen Oct 16 '14 at 01:59
  • @J.F.Sebastian: `identity` is not about supporting `map`, that was just a familiar use-case; my use-case does not use `map` at all. For that matter, `starmap` is not about identity, but about tuple unpacking -- what is your code sample combining the two? – Ethan Furman Oct 16 '14 at 02:03
  • @DaoWen: The point is that if you want to get `[1]` back from the identity function, you can't have Python unpack it first; to put it another way: `f([1])` is *not* the same as `f(1)`, so `identity([1])` will not be the same as `identity(1)`. – Ethan Furman Oct 16 '14 at 02:06
  • @EthanFurman - I agree with what you said in both of your comments, but you still haven't explained why my example would be invalid. You've done nothing to convince me that unifying the tuple and scalar identity functions is a good idea rather than a terrible one. – DaoWen Oct 16 '14 at 02:14
  • @DaoWen: Rewrote my question, which should hopefully provide better context. As for why your example is invalid: an identity function should return all its arguments unchanged, so why should `identity(1)` return `[1]`? – Ethan Furman Oct 16 '14 at 02:30
  • @EthanFurman - If `len(identity(1,2,3))` returns `3`, `len(identity(1,2))` returns `2`, and `len(identity())` returns `0`, then I'd expect `len(identity(1))` to return `1`. However, with your implementation `len(identity(1))` raises a type error! It seems very broken to have one case return a scalar (or whatever type was passed in) when _every other case_ returns a tuple (even the 0-ary case). I just think it makes way more sense to have _two separate identity functions_: `identity` for scalars and `staridentity` for tuples. – DaoWen Oct 16 '14 at 02:49
  • @DaoWen: Ah, I see your point. I think the crux of the matter is that `identity` is not meant for `len`; consider `len(1, 2)`, which raises a TypeError. I will also play the `Practicality beats Purity` and `Readablity counts` cards, since `a = some_func(*some_parameters)` is much nicer than `a, = some_func(*some_parameters)`. [Notice the tiny comma hiding after the 'a' in the second example.] – Ethan Furman Oct 16 '14 at 03:03
  • @EthanFurman - My point is actually that it's both impure _and_ impractical to have a function like you're suggesting. This is because it totally breaks the uniformity of dealing with the output. Assuming you don't know the length of `some_parameters` ahead of time, `a = some_func(*some_parameters)` becomes very difficult to handle because you don't know if `a` is a single value (in the case that the length was 1) or a tuple (in every other case). You want it to just return a tuple in every case so that you can uniformally handle the output with `for` or something similar. – DaoWen Oct 16 '14 at 03:18