Welcome, guest | Sign In | My Account | Store | Cart

Sometimes I wish slice creation had a form that looked more like its invocation: [1:5], for example. This recipe allows slice arguments to be passed that look like {1:5}. NOTE: I don't recommend actually using this in practice. It will most likely just confuse any readers of your code. It is just a proof-of-concept. In an interview with Guido van Rossum, Guido was explaining the benefits of Python's built-in types. He gave an example of being able to pass slices as arguments instead of having individual start, stop and step arguments -- and this is very true. But I don't think a syntax to concisely create slices should be out of the question. (Step is not supported by this syntax, it wouldn't be possible and I rarely find myself using step anyway).

Python, 130 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# Only read this big ol' function if you want to know how it is implemented.
# For usage just scroll down.
def quickslice(*sliceArgs, **sliceKeys):
    """A decorator that allows for an abbreviated slice syntax in function
    arguments: {start:end}. Step is not supported. Valid quickslices are
    dictionaries with a single integer key which has an integer value.

    Arguments can be strings or integers, and indicate the positional or keyword
    arguments that should be checked for possible quickslices. By default, no
    exception is raised if the indicated arguments turn out not to be
    quickslices (consider __getitem__, which can accept slices or integers). To
    change this behavior, set the keyword argument 'argAssert' to True.

    The optional 'argSearch' keyword argument can be True or False, and
    specifies whether or not to check if *every* argument of the decorated
    function is a 'quickslice' rather than just the indicated arguments. The
    default is False.
    """
    argSearch = sliceKeys.get('argSearch', False)
    argAssert = sliceKeys.get('argAssert', False)
    
    def isqslice(arg):
        """Determine if arg is of the proper quickslice form.
        
        Proper quickslice form is a single-element dictionary whose key and
        value are both integers, like: {0:9} (key->start, value->stop)
        """
        return (isinstance(arg, dict) and len(arg) == 1 and
                isinstance(arg.keys()[0], int) and
                isinstance(arg.values()[0], int))
    
    def decorate(f):
        def _wrapper(*args, **kwargs):
            args = list(args)
            if argSearch:
                for i, arg in enumerate(args[:]):
                    if isqslice(arg):
                        start, stop = arg.popitem()
                        args[i] = slice(start, stop)
                for kw, arg in kwargs.iteritems():
                    if isqslice(arg):
                        start, stop = arg.popitem()
                        kwargs[kw] = slice(start, stop)
            elif sliceArgs:
                for arg in sliceArgs:
                    if isinstance(arg, int):
                        try:
                            if isqslice(args[arg]):
                                start, stop = args[arg].popitem()
                                args[arg] = slice(start, stop)
                            elif argAssert:
                                raise ValueError, (
                                    "argument %s expected a quickslice" % arg)
                        except (AttributeError, KeyError, IndexError):
                            pass
                    elif isinstance(arg, basestring):
                        try:
                            if isqslice(kwargs[arg]):
                                start, stop = kwargs[arg].popitem()
                                kwargs[arg] = slice(start, stop)
                            elif argAssert:
                                raise ValueError, (
                                    "argument %s expected a quickslice" % arg)
                        except (AttributeError, KeyError, IndexError):
                            pass
                    else:
                        raise TypeError, (
                            "quickslice expects integers or strings")
            return f(*args, **kwargs)
        return _wrapper
    return decorate

# These testslice functions just demonstrate the changes that will be made
# to the resulting function's arguments

# This will call the function with no changes to its arguments
@quickslice()
def testslice(*args, **kwargs):
    print args, kwargs

>>> testslice({0:5}, select={10:20})
({0: 5},) {'select': {10: 20}} # The arguments are not affected

# Argument 1 and keyword argument 'select' will be checked for quickslice
# values, and an exception will be raised if they aren't of the expected form.
# (due to argAssert)
@quickslice(1, 'select', argAssert=True)
def testslice2(*args, **kwargs):
    print args, kwargs

>>> testslice2('hello', {3:5}, select={1:2}, test={3:4})
('hello', slice(3, 5, None)) {'test': {3: 4}, 'select': slice(1, 2, None)}
# Argument 1 and keyword argument 'select' are now slices, while 'test' is
# left as is.

>>> testslice2('hello', {'yo': 3}, select=1) # Argument 1 is not a quickslice!
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "quickslice.py", line 44, in _wrapper
    raise ValueError, (
ValueError: argument 1 expected a quickslice

# Keyword arguments 'group' and 'span' will be checked for quickslices, but
# no exceptions will be raised if they turn out not to be.
@quickslice('group', 'span')
def testslice3(*args, **kwargs):
    print args, kwargs

>>> testslice3(group=1, span={10:13}) # No exception will be raised for 'group'
() {'group': 1, 'span': slice(10, 13, None)}

# argAssert is pointless here, since we are searching for quickslices. argSearch
# will take precedence.
@quickslice(argSearch=True, argAssert=True)
...

# This also doesn't make sense: telling it to expect a quickslice at the first
# argument, but also searching for quickslices in every argument. These options
# are mutually exclusive. The argSearch will take precedence.
@quickslice(0, argSearch=True)
def testslice4(*args, **kwargs):
    print args, kwargs

>>> testslice4('hello', {3:4}, {5:6}, select={7:8})
('hello', slice(3, 4, None), slice(5, 6, None)) {'select': slice(7, 8, None)}
# Everything that fits the quickslice 'protocol' is converted to a slice.

# To support dynamic function arguments, 'checked' arguments that don't actually
# exist in the function call won't raise an exception. This could also be made
# into a condition for argAssert if necessary.

There are clearly more downsides to this recipe than benefits: decreased readability, increased function call time, the possibility of receiving a 'matching' dictionary that isn't intended to be a quickslice. Slices as arguments are admittedly not used often enough to justify this recipe. But dictionaries can be created with {}, lists with [], and tuples with (), so I wanted to try to quickly create slices that looked a little more like their invocation.

Created by Brian Beck on Sun, 29 May 2005 (PSF)
Python recipes (4591)
Brian Beck's recipes (5)

Required Modules

  • (none specified)

Other Information and Tasks