4

This is my first time asking a question in SO, so if I'm somehow not doing it properly don't hesitate to edit it or ask me to modify it.

I think my question is kind of general, so I'm quite surprised for not having found any previous one related to this topic. If I missed it and this question is duplicated, I'll be very grateful if you could provide a link to where it was already answered.

Imagine I need to implement a function with (at least) three parameters: an array a, a start index and an end index. If not provided, the start parameter should refer to the first position of the array (start = 0), while the end parameter should be set to the last position (end = len(a) - 1). Obviously, the definition:

def function(a, start = 0, end = (len(a) - 1)):
    #do_something
    pass

does not work, leading to an exception (NameError: name 'a' is not defined). There are some workarounds, such as using end = -1 or end = None, and conditionally assign it to len(a) - 1 if needed inside the body of the function:

def function(a, start = 0, end = -1):
    end = end if end != -1 else (len(a) -1)
    #do_something

but I have the feeling that there should be a more "pythonic" way of dealing with such situations, not only with the length of an array but with any parameter whose default value is a function of another (non optional) parameter. How would you deal with a situation like that? Is the conditional assignment the best option?

Thanks!

11
  • 1
    Using the def function(a, start=0, end=None): solution seems the most obvious to me as in Python you do not use ints with "weird" value for a different meaning - because Python like explicit over implicit. None has the exact meaning you're looking for i.e. no value was specified. It also allows you for a more explicit and shorter assignment end = end or len(a) Commented Jan 29, 2015 at 10:36
  • 1
    @bvidal end = end or len(a) will assign len(a) to end if end is 0. One would need to do end = end if end is not None else len(a). Commented Jan 29, 2015 at 10:39
  • @DanD. thanks I overlooked this case Commented Jan 29, 2015 at 10:42
  • 1
    possible duplicate of Function with dependent preset arguments Commented Jan 29, 2015 at 10:58
  • @bvidal Thanks! In my example I was using -1 instead of None as in lists a[-1] == a[len(a) - 1], but I agree that None looks more appropiate Commented Jan 29, 2015 at 10:59

5 Answers 5

10

Using a sentinel value such as None is typical:

def func(a, start=0, end=None):
    if end is None:
        end = # whatever
    # do stuff

However, for your actual use case, there's already a builtin way to do this that fits in with the way Python does start/stop/step - which makes your function provide a consistent interface as to the way builtins/other libraries work:

def func(a, *args):
    slc = slice(*args)
    for el in a[slc]:
        print(el)

See https://docs.python.org/2/library/functions.html#slice


If you only want to support start/end in that order, then (note that None effectively means until len(a) when used as end or 0 when used as start):

def func(a, start=0, end=None):
    return a[start:end]
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! I did not know about the slice method, but anyway it does not cover my needs, as the particular values of start and end are needed. I guess the conditional assignment is the best solution. I was considering -1 as an option as it refers to the last element of a list (a[-1] == a[len(a) - 1], but probably None is a more elegant solution
@franrodalg you can use None for both start and end... - that way you can use func(a) (which will just be the whole lot), or func(a, end=5) first 5, or func(a, 2) from 2 onwards... or func(a, 2, 6) etc...
1

Based on the answer provided by @NPE in Function with dependent preset arguments, an alternative to using -1 or (better) None as sentinel values is using an object (a named object?) which can be used even if None is a valid value of the function. For example:

default = object()

def function(a, start = 0, end = default):
    if end is default: end = (len(a) - 1)
    return start, end

allows a call like: function([1,2,3,4]) which returns (0, 3)

I personally find this solution quite convenient, at least for my own purpose

Edit: Maybe the code is even more readable if we use last instead of default:

last = object()

def function(a, start = 0, end = last):
    if end is last: end = (len(a) - 1)
    return start, end

1 Comment

To me, the function is decidedly less readable if you use last. What does "last" mean here? Do you mean the last position of a? Well, that's already indicated by the name of the parameter, end. If I were casually reading your code, I would guess that last is something to save the value from the last call. In other words, if your function is called without end, then use the previous value of end. I think default communicates the intention much better. Another descriptive choice would be sentinel.
0

This isn't possible, functions can't be executed in the parameter list, you can pass along functions through the parameters but not their output.

def function(a, start = 0):
    end = len(a)

1 Comment

I already had that impression, but I was wondering if there was any built-in way to deal with such situations.. which it seems it doesn't exist hehe. Your code solution does not help much, as in my case (and in the general case too) end can take any value, not just len(a)
0

I am unsure of your definition of an array or the premise of your problem, but to my understanding you are trying get end to be assigned to the length of a. If so, just declare end out side the arguments. Like so:

def function(a, start=0):
    end=len(a)

Or do the conditional like you said:

def function(a, start=0, end=False):
    if not end:
        end = len(a)

Or simply declare the end variable before calling the function and pass it in the arguments! Not sure if that answered your question, but hope it helped lol!

Comments

0

This has to be the hackiest code I've ever written. I think it comes close to what you were asking for (another alternative I came up with was using a lambda inside the function definition, but it takes a bit too much room to be pretty IMO):

import inspect
from functools import wraps

class defaultArguments(object):

    def __init__(self):
        self.lazyArgs = []

    def initialize(self, func):

        lazyArgs, self.lazyArgs = self.lazyArgs, []

        @wraps(func)
        def functionWrapper(*args, **kw):

            if lazyArgs:
                
                argNames, defaults = inspect.getargspec(func)[::3]
                argValues = list(args) + [
                    kw[y] if y in kw else defaults[x]
                    for x,y in enumerate(argNames[len(args):])
                ]

                oldGlobals = {}
                for n,v in zip(argNames, argValues):

                    try:
                        oldGlobals[n] = globals()[n]
                    except:
                        oldGlobals[n] = None

                    if v not in lazyArgs:
                        globals()[n] = v
                    else:
                        globals()[n] = kw[n] = eval(v)

                for o,v in oldGlobals.items(): globals()[o] = v

            return func(*args, **kw)

        return functionWrapper

    def __call__(self, x):
        self.lazyArgs.append(x)
        return x

Using it:

d = defaultArguments()

@d.initialize
def function1(a, start=d('a[-1]'), end=d('len(a)-1')):
    print a, start, end


function1([1,2,8])
>>> [1, 2, 8] 8 2

function1([1,2,8,10], end=1)
>>> [1, 2, 8, 10] 10 1


@d.initialize
def function2(a, b, c, start=d('a*b*c'), end=d('a+b+c+start')):
    print a, start, end

function2(2,4,6)
>>> 2 48 60
# Notice that `end` does take the calculated value of `start` into 
# account. The logic here is based on what you'd expect to happen
# with normal assignment if the names were each assigned a value 
# sequentially: a is evaluated, then b, then c, etc...

I do feel guilty for doing this, especially with the way I resorted to using globals and other cheats. However, I think it works as you requested.

Unfortunately, you do have to write extra stuff (using a decorator and having to wrap keyword values in d('') ), but that was inevitable as Python doesn't support this natively.

Edit:

I worked on the sugary part of the syntax a bit. Shortened it down to a simple decorator function.

def initArgs(func):

    @wraps(func)
    def functionWrapper(*args, **kw):

        argNames, defaults = inspect.getargspec(func)[::3]

        for k in kw:
            for i in argNames:
                if k != i and ('_' + k) == i:
                    kw['_' + k] = kw[k]
                    del kw[k]

        argValues = list(args) + [
            kw[y] if y in kw else defaults[x]
            for x,y in enumerate(argNames[len(args):])
        ]

        oldGlobals = {}
        for n,v in zip(argNames, argValues):

            try:
                oldGlobals[n] = globals()[n]
            except:
                oldGlobals[n] = None

            if not n.startswith('_') or n in kw:
                globals()[n] = v
            else:
                globals()[n] = kw[n] = eval(v)

        for o,v in oldGlobals.items(): globals()[o] = v

        return func(*args, **kw)

    return functionWrapper

To use it:

# When using initArgs, the strings associated with the keyword arguments will
# get eval'd only if the name is preceded with an underscore(`_`). It's a 
# bit strange and unpythonic for part of a name to have side effects, but then
# again name mangling works with double underscores (`__`) before methods.

# Example: 

@initArgs
def function1(a, _start='a[-1]', _end='len(a)-1'):
    print a, _start, _end

function1([1,2,8,10])
>>> [1, 2, 8, 10] 10 3

# Removing underscore (`_`) from start
@initArgs
def function2(a, start='a[-1]', _end='len(a)-1'):
    print a, start, _end

function1([1,2,8,10])
>>> [1, 2, 8, 10] 'a[-1]' 3

# Outputs a string normally.

In the caller's frame, the arguments start and end can used with or without their underscores, so changing their names in the function definition at a later point wouldn't affect the caller. The only exception is within the function itself, where removing an underscore(_) would require doing the same everywhere else inside.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.