开发者

Python method to remove iterability

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 开发者_JAVA百科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.)


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.


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.


Specialize it.

def can_iter(arg):
   if isinstance(arg, str):
     return False
   try:
     ...


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.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜