Suppressing treatment of string as iterable

こ雲淡風輕ζ 提交于 2019-11-27 23:36:54

There aren't any ways to do this automatically, unfortunately. The solution you propose (a str subclass that isn't iterable) suffers from the same problem as isinstance() ... namely, you have to remember to use it everywhere you use a string, because there's no way to make Python use it in place of the native class. And of course you can't monkey-patch the built-in objects.

I might suggest that if you find yourself writing a function that takes either an iterable container or a string, maybe there's something wrong with your design. Sometimes you can't avoid it, though.

In my mind, the least intrusive thing to do is to put the check into a function and call that when you get into a loop. This at least puts the behavior change where you are most likely to see it: in the for statement, not buried away somewhere in a class.

def iterate_no_strings(item):
    if issubclass(item, str):   # issubclass(item, basestring) for Py 2.x
        return iter([item])
    else:
        return iter(item)

for thing in iterate_no_strings(things):
    # do something...

To expand, and make an answer out of it:

No, you shouldn't do this.

  1. It changes the functionality people expect from strings.
  2. It means extra overhead throughout your program.
  3. It's largely unnecessary.
  4. Checking types is very unpythonic.

You can do it, and the methods you have given are probably the best ways (for the record, I think sub-classing is the better option If you have to do it, see @kindall's method) but it's simply not worth doing, and it's not very pythonic. Avoid the bugs in the first place. In your example, you might want to ask yourself if that's more an issue with clarity in your arguments, and whether named arguments or the splat might be a better solution.

E.g: Change the ordering.

def set_fields(record, value, *fields):
  for f in fields:
    record[f] = value

set_fields(weapon1, 'Dagger', *('Name', 'ShortName')) #If you had a tuple you wanted to use.
set_fields(weapon2, 'Katana', 'Name')
set_fields(weapon3, 'Wand', 'Name')

E.g: Named arguments.

def set_fields(record, fields, value):
  for f in fields:
    record[f] = value

set_fields(record=weapon1, fields=('Name', 'ShortName'), value='Dagger')
set_fields(record=weapon2, fields=('Name'), value='Katana')
set_fields(record=weapon3, fields='Name', value='Wand') #I find this easier to spot.

If you really want the order the same, but don't think the named arguments idea is clear enough, then what about making each record a dict-like item instead of a dict (if it isn't already) and having:

class Record:
    ...
    def set_fields(self, *fields, value):
        for f in fileds:
            self[f] = value

weapon1.set_fields("Name", "ShortName", value="Dagger")

The only issue here is the introduced class and the fact that value parameter has to be done with a keyword, although it keeps it clear.

Alternatively, if you are using Python 3, you always have the option of using extended tuple unpacking:

def set_fields(*args):
      record, *fields, value = args
      for f in fields:
        record[f] = value

set_fields(weapon1, 'Name', 'ShortName', 'Dagger')
set_fields(weapon2, 'Name', 'Katana')
set_fields(weapon3, 'Name', 'Wand')

Or, for my last example:

class Record:
    ...
    def set_fields(self, *args):
        *fields, value = args
        for f in fileds:
            self[f] = value

weapon1.set_fields("Name", "ShortName", "Dagger")

However, these do leave some weirdness when reading the function calls, due to the fact one usually assumes that arguments would not be handled this way.

Type checking in this case is not unpythonic or bad. Just do a:

if isinstance(var, (str, bytes)):
    var = [var]

In the beginning of the call. Or, if you want to educate the caller:

if isinstance(var, (str, bytes)):
    raise TypeError("Var should be an iterable, not str or bytes")

What do you think about creating a non-iterable string?

class non_iter_str(str):
    def __iter__(self):
        yield self

>>> my_str = non_iter_str('stackoverflow')
>>> my_str
'stackoverflow'
>>> my_str[5:]
'overflow'
>>> for s in my_str:
...   print s
... 
stackoverflow

Instead of trying to make your strings non-iterable, switch the way you are looking at the problem: One of your parameters is either an iterable, or a ...

  • string
  • int
  • custom class
  • etc.

When you write your function, the first thing you do is validate your parameters, right?

def set_fields(record, fields, value):
    if isinstance(fields, str):
        fields = (fields, )  # tuple-ize it!
    for f in fields:
        record[f] = value

This will serve you well as you deal with other functions and parameters that can be either singular, or pluralized.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!