views:

133

answers:

5

Suppose I have a function which can either take an iterable/iterator or a non-iterable as an argument. Iterability is checked with try: iter(arg).

Depending whether the input is an iterable or not, the outcome of the method will be different. Not when I want to pass a non-iterable as iterable input, it is easy to do: I’ll just wrap it with a tuple.

What do I do when I want to pass an iterable (a string for example) but want the function to take it as if it’s non-iterable? E.g. make that iter(str) fails.

Edit – my original intention:

I wanted to generalise the zip function in that it can zip iterables with non-iterables. The non-iterable would then repeat itself as often as the other iterables haven’t finished.

The only general solution fo me seems now to be, that I shouldn’t check inside the general_zip function (because of the string issues); but that instead I’ll have to add the repeat iterator to the argument before calling zip. (This actually saves me from inventing the general_zip function — although I still might because with a non-iterable as an input it would be unambiguous without the extra repeat.)

A: 

Specialize it.

def can_iter(arg):
   if isinstance(arg, str):
     return False
   try:
     ...
KennyTM
But this means I need to decide inside the function and can’t make a new decision from occasion to occasion.
Debilski
+2  A: 

Whoo! It appears you want to be able to pass iterables as iterables, iterables as noniterables, noniterables as iterables, and noniterables as noniterables. Since you want to be able to handle every possibility, and the computer can not (yet) read minds, you are going to have to tell the function how you want the argument to be handled:

def foo_iterable(iterable):
    ...
def foo_noniterable(noniterable):
    ...

def foo(thing,isiterable=True):
    if isiterable:
        foo_iterable(thing)
    else:
        foo_noniterable(thing)

Apply foo to an iterable

foo(iterable)

Apply foo to an iterable as a non-iterable:

foo_noniterable(iterable)       # or
foo(iterable, isiterable=False)

Apply foo to a noniterable as a noniterable:

foo_noniterable(noniterable)       # or
foo(noniterable,isiterable=False)

Apply foo to a noniterable as an iterable:

foo((noniterable,))

PS. I'm a believer in small functions that do a single job well. They are easier to debug and unit-test. In general I would advise avoiding monolithic functions that behave differently depending on type. Yes, it puts a little extra burden on the developer to call exactly the function that is intended, but I think the advantages in terms of debugging and unit-testing more than make up for it.

unutbu
The problem is, that the function might have several arguments; would get a little complicated then.
Debilski
@Debilski: Why not add several arguments to `foo`? Maybe I don't know enough about your situation. Why is it complicated?
unutbu
I don’t know. It looks like this would end up as such: `foo([1,2,3], [1,2,3], "abc", isiterable1=True, isiterable2=True, isiterable3=False)`. Or with more arguments even. I think your solution would be fine with only one or two fixed arguments; my situation is that I’d like to have it more general. (Otherwise I think I wouldn’t have cared to ask anyway and just would have done it more or less like you proposed.)
Debilski
+3  A: 

The more I think about it, it seems like it’s not possible to do without type checking or passing argments to the function.

However, depending on the intention of the function, one way to handle it could be:

from itertools import repeat
func(repeat(string_iterable))

func still sees an iterable but it won’t iterate through the charaters of the string itself. And effectively, the argument works as if it’s a constant non-iterable.

Debilski
`repeat(string_iterable)` will return the string endlessly. Did you mean `[string_iterable]` (it will return the string just once)?
J.F. Sebastian
No, for my problem [string_iterable] would be the wrong solution. The analogy should be that of point in one dimension which – when you expand it in two dimensions – corresponds to a whole line of points and not just a single one.
Debilski
A: 

Well, one approach to tell the function how you would like to treat its arguments is to have sensible defaults (making the function treat everything by its original type by default), while being able to specify any tweaks you like with comfort (i.e. with a short and absent-by-default fmt string), like:

def smart_func(*args, **kw):
    """If 'kw' contains an 'fmt' parameter,
    it must be a list containing positions of arguments,
    that should be treated as if they were of opposite 'kind'
    (i.e. iterables will be treated as non-iterables and vise-versa)

    The 'kind' of a positional argument (i.e. whether it as an iterable)
    is inferred by trying to call 'iter()' on the argument.
    """

    fmt = kw.get('fmt', [])

    def is_iter(it):
        try:
            iter(it)
            return True
        except TypeError:
            return False

    for i,arg in enumerate(args):
        arg_is_iterable = is_iter(arg)
        treat_arg_as_iterable = ((not arg_is_iterable)
                                 if (i in fmt) else arg_is_iterable)
        print arg, arg_is_iterable, treat_arg_as_iterable

This gives:

>>> smart_func()
>>> smart_func(1, 2, [])
1 False False
2 False False
[] True True
>>> smart_func(1, 2, [], fmt=[])
1 False False
2 False False
[] True True
>>> smart_func(1, 2, [], fmt=[0])
1 False True
2 False False
[] True True
>>> smart_func(1, 2, [], fmt=[0,2])
1 False True
2 False False
[] True False

Expanding this function (finding the length of the longest iterable, etc), one can construct a smart-zip you are talking about.

[P.s.] Another way will be to call the function in the following way:

smart_func(s='abc', 1, arr=[0,1], [1,2], fmt={'s':'non-iter','some_arr':'iter'})

and have the function match the argument names you supplied ('s' and 'arr', note, there are no such names in the functions signature as it is the same as above) to the 'fmt' "type-hints" (i.e. 'iter' makes an argument considered as an iterable, and 'non-iter' as a non-iterable). This approach can, of course, be combined with the above "toggle-type" one.

mlvljr
There is no reason to use nested functions there. Nested functions are useful for making closures, but using them to define constant functions locally is silly.
Mike Graham
@Mike Graham The reason is to fulfill the function's contract (specified in its doc btw). It's true that the function in question may be moved outside (for example, to make it reusable) as it does not actually depend on local args. But whether it's silly... well, let everyone choose that for himself, sir:)
mlvljr
Well, now I see that `smart_func` is possibly too smart [to be upvoted] :))
mlvljr
A: 

Don't check for iterability. It is a mistake to have a function check things about its elements types/capabilities in order to have a single function perform different tasks. If you want to do two different things, make two different functions.

It sounds like you have come to this conclusion yourself and are providing a consistent API, where you do

from itertools import repeat
zip([1, 2, 3], repeat(5), "bar")

Note that it's almost always useless to do this since you could just do

five = 5
for number, letter in zip([1, 2, 3], "bar")
    # Just use five here since it never changes

Unless of course you are feeding this to something that already uses zip.

Mike Graham
The input which is giving me `five` could as well be a list. And this occurs at several points in my code, so I don’t want to check this all the time before I call the zipping function but instead do the checking inside. My problem was how to deal with edge cases.
Debilski
@Debilski If you want your function to be "prepacked" with iterable/not iterable inspection logic as well as to provide an ability to "override"/swap the way an argument is processed, why not just use a simple (and empty by default) `format` specifier?
mlvljr
@Debilski, Right, of course. And clearly you have discovered the way to pass what you really need—an iterable that yields the same value over and over.
Mike Graham