4

Basically something like this:

DEFAULT_TIMEOUT = 10
# or even: from my_settings import DEFAULT_TIMEOUT

def get_google(timeout=DEFAULT_TIMEOUT):
    return requests.get('google.com', timeout=timeout)

I would assume that as long as the constant really stays constant this should work just fine. Yet I sometimes see a pattern like this:

DEFAULT_TIMEOUT = 10

def get_google(timeout=None):
    if timeout is None:
        timeout = DEFAULT_TIMEOUT
    return requests.get('google.com', timeout=timeout)

Are these equivalent or should I prefer one over the other?

gmolau
  • 2,815
  • 1
  • 22
  • 45
  • Can you give an example of where you saw that second pattern? – BrenBarn Dec 30 '17 at 01:26
  • @BrenBarn From the top of my head, no, but I've seen it often enough to have it memorized as one way to do this. – gmolau Dec 30 '17 at 01:29
  • The second pattern is more useful when the default is mutable like `[]`. With immutable types such as numbers you should be fine using them directly – Paul Panzer Dec 30 '17 at 01:29
  • The second example will not allow a timeout of `0` – Stephen Rauch Dec 30 '17 at 01:30
  • @mxgx: As Paul Panzer's comment says, that pattern is more common for a different situation, which is why I was asking. What I'm curious about is whether you have specifically seen someone using the default-None-then-if-inside-function *together* with a module-level constant as the default value, or if you've just seen those two things separately. – BrenBarn Dec 30 '17 at 01:30
  • @BrenBarn I'm specifically asking about the module constant, normal default args should of course always go into the function header. – gmolau Dec 30 '17 at 01:33
  • @StephenRauch You're right, fixed the if for that. – gmolau Dec 30 '17 at 01:34
  • These examples are now equivalent, unless, in the second example, the caller explicitly passes None, they will then get the default. – Stephen Rauch Dec 30 '17 at 01:37

2 Answers2

3

There's no problem with using the "constant" as a default value. As you say, as long as the "constant" is really constant, it won't matter. The only thing is that you do have to make sure the constant is defined before the function, but usually people put all their constants at the top of the file so that's not an issue.

The second pattern you describe is common when the desired default is a mutable value, such as a list. You often see things like this:

def foo(x=None):
    if x is None:
        x = []

instead of def foo(x=[]). You can find many questions about this, but essentially it is because if you don't do it this way, the mutable default will persist across multiple calls to the function, which usually isn't desirable.

However, using this pattern for a mutable module-level constant wouldn't fix that problem. If you have:

SOME_CONSTANT = []

def foo(x=None):
    if x is None:
        x = SOME_CONSTANT

. . . then you're still reusing the same mutable value across multiple calls. (Of course, defining a mutable value as a "constant" is probably not a good idea anyway.) That's why I was asking in the comments if you've seen someone specifically doing this kind of thing with module constants.

This None-then-if pattern would also be used if the module-level default were actually not a constant, but a value intended to be changed by other code. If you do def foo(x=DEFAULT_TIMEOUT), the default value of x is whatever DEFAULT_TIMEOUT was at the time you defined the function. But if you use the None-then-if pattern, the default will be whatever DEFAULT_TIMEOUT is at the time you call the function. Some libraries define module-level values that aren't meant to be constant, but are rather configuration values that may be changed in the course of execution. This allows users to do things like set DEFAULT_TIMEOUT = 20 to change the default timeout for all subsequent calls, rather than having to pass timeout=20 every time. In this case you would want the if check inside the function, to ensure that every call uses the "current" value of DEFAULT_TIMEOUT.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • Thanks for the explanation, it looks to me like there are actually two subtle differences: The first one being the one your described in your last paragraph, i.e. that the none-then-if pattern gives the user the option to override the module constant for repeated calls of that function. The other difference would be that the first pattern allows the user to choose between my own default value and the default value of the underlying request function. By passing None in the first case one would get the default request.get() timeout, which the user isn't meant to know, but could be made aware of. – gmolau Dec 30 '17 at 01:52
  • Not that I would know of any practical use case for that, but it helps to understand what's happening. – gmolau Dec 30 '17 at 01:53
  • @mxgx: Yes, that is true. – BrenBarn Dec 30 '17 at 01:59
1

UPDATE: I highly recommend reading this post about instance and class attributes, it includes best practices of using both types of attributes, and when one is preferred over the other.

As you mentioned, second pattern can occur in modules, where keyword self is used, to define constant as instance attribute (you can read more about attributes there and there) for instance:

class Module:
    def __init__(self):
        self.DEFAULT_TIMEOUT = 10

    def get_google(timeout=self.DEFAULT_TIMEOUT):
        return requests.get('google.com', timeout=timeout)

would produce an error: NameError: name 'self' is not defined

class Module:
    def __init__(self):
        self.DEFAULT_TIMEOUT = 10

    def get_google(timeout=None):
        if timeout is None:
            timeout = self.DEFAULT_TIMEOUT
        return requests.get('google.com', timeout=timeout)

In another question problem is solved by mgilson in more clever way. It suggests creating sentinels:

The common idiom here is to set the default to some sentinel value (None is typical, although some have suggested Ellipsis for this purpose) which you can then check.

class Example(object): #inherit from object.  It's just a good idea.  
    def __init__(self, data = None):
        self.data = self.default_data() if data is None else data

    def default_data(self):  #probably need `self` here, unless this is a @staticmethod ...
        # ....
        return something

You might also see an instance of object() used for the sentinel.

SENTINEL = object()
class Example(object):
    def __init__(self, data = SENTINEL):
        self.data = self.default_data() if data is SENTINEL else data

This latter version has the benefit that you can pass None to your function but has a few downsides (see comments by @larsmans below). If you don't forsee the need to pass None as a meaningful argument to your methods, I would advocate using that.

Lycopersicum
  • 529
  • 1
  • 6
  • 17