'default' keyword argument for max(), min()
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed. (If no "default" argument is provided, and the iterable is empty, it would raise ValueError as it does currently.) I find this to be a very common need when using those functions. Of course this is already possible with a bit more code, but it depends on what type of object the iterable is -- if it supports __len__ or __nonzero__, that can be used to check if it's empty beforehand, but if it is a generator, for instance, it would have to be converted to a list first, which might be undesirable if there is the possibility that it is a very large sequence. Adding a "default" keyword argument to max() and min() would be an elegant way to centralize and simplify this common and useful behaviour. If there is support for this idea, I can submit a patch implementing it.
Adam Atlas wrote:
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed. (If no "default" argument is provided, and the iterable is empty, it would raise ValueError as it does currently.) I find this to be a very common need when using those functions. Of course this is already possible with a bit more code, but it depends on what type of object the iterable is -- if it supports __len__ or __nonzero__, that can be used to check if it's empty beforehand, but if it is a generator, for instance, it would have to be converted to a list first, which might be undesirable if there is the possibility that it is a very large sequence. Adding a "default" keyword argument to max() and min() would be an elegant way to centralize and simplify this common and useful behaviour.
If there is support for this idea, I can submit a patch implementing it.
+1, I have often wanted that. Cheers - Jacob
On Wed, Apr 15, 2009 at 1:31 PM, Jacob Holm <jh@improva.dk> wrote:
Adam Atlas wrote:
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed. (If no "default" argument is provided, and the iterable is empty, it would raise ValueError as it does currently.) I find this to be a very common need when using those functions. Of course this is already possible with a bit more code, but it depends on what type of object the iterable is -- if it supports __len__ or __nonzero__, that can be used to check if it's empty beforehand, but if it is a generator, for instance, it would have to be converted to a list first, which might be undesirable if there is the possibility that it is a very large sequence. Adding a "default" keyword argument to max() and min() would be an elegant way to centralize and simplify this common and useful behaviour.
If there is support for this idea, I can submit a patch implementing it.
+1, I have often wanted that.
Seconded; I've been bitten more than once by this. George
On Wed, Apr 15, 2009 at 8:20 PM, Adam Atlas <adam@atlas.st> wrote:
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
+1 This has precedent in reduce()'s "initial" keyword argument; note that min() and max() are really simple special cases of reduce().
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
[Tal Einat]
+1
This has precedent in reduce()'s "initial" keyword argument; note that min() and max() are really simple special cases of reduce().
Of course, what he is proposing has completely different semantics than an intial argument. If anyone finds that distinction to be confusing, then we would have a good reason not to accept the proposal. min([1,2], initial=0) --> 0 min([1,2], default=0) --> 1 Raymond
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one. Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2]) Also, can you post some snippets of real-world use cases. Is the default value always zero (even for max)? I'm wondering if there are any patterns to the use cases. I don't doubt that the use cases exist, I'm just curious what they are and what it says about how min() and max() are being used. Are the typical use cases occuring with iterables that are also sequences? If so, why would a default argument be better than a conditional expression: x = min(s) if s else 0 Raymond
...
Are the typical use cases occuring with iterables that are also sequences? If so, why would a default argument be better than a conditional expression:
x = min(s) if s else 0
Raymond
Because min(s) if s could be a generator which won't evaluate to False, even though it returns no entries. John =:->
On Thu, 16 Apr 2009 04:17:07 am Raymond Hettinger wrote:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2])
I would expect the answers should be: Yes, 1 and 1 but I'd be prepared to be talked into: No, 1 and 1.
Also, can you post some snippets of real-world use cases. Is the default value always zero (even for max)?
I don't believe there should be a default value for default. If you don't provide an explicit default, the current behaviour should remain unchanged.
I'm wondering if there are any patterns to the use cases. I don't doubt that the use cases exist, I'm just curious what they are and what it says about how min() and max() are being used.
Are the typical use cases occuring with iterables that are also sequences? If so, why would a default argument be better than a conditional expression:
x = min(s) if s else 0
If s could be either an iterable or a sequence, you would need to write that as: s = list(s) x = min(s) if s else 0 which turns a single conceptual operation into two operations. -- Steven D'Aprano
On Wed, Apr 15, 2009 at 3:11 PM, Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, 16 Apr 2009 04:17:07 am Raymond Hettinger wrote:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2])
I would expect the answers should be:
Yes, 1 and 1
but I'd be prepared to be talked into:
No, 1 and 1.
I think it would be counter-intuitive and error-prone if min(iterable, default=0) was different from min(*iterable, default=0), so I'd say no on the first one. George
2009/4/15 George Sakkis <george.sakkis@gmail.com>:
I think it would be counter-intuitive and error-prone if min(iterable, default=0) was different from min(*iterable, default=0),
It is definitely different if iterable == [[7]]. Since min(*iterable) will break if iterable has length 1, it should not be called this way at all. Values should be passed to min as individual arguments only if their number is statically known, and in this case there is no reason to use 'default'. -- Marcin Kowalczyk qrczak@knm.org.pl http://qrnik.knm.org.pl/~qrczak/
On Thu, 16 Apr 2009 05:19:05 am George Sakkis wrote:
I think it would be counter-intuitive and error-prone if min(iterable, default=0) was different from min(*iterable, default=0), so I'd say no on the first one.
That's already the case.
min( iter([1]) ) 1 min( *iter([1]) ) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable
-- Steven D'Aprano
On Wed, Apr 15, 2009 at 7:09 PM, Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, 16 Apr 2009 05:19:05 am George Sakkis wrote:
I think it would be counter-intuitive and error-prone if min(iterable, default=0) was different from min(*iterable, default=0), so I'd say no on the first one.
That's already the case.
min( iter([1]) ) 1 min( *iter([1]) ) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable
Oh right, posted too fast; the single argument case is already special, so we might as well make the zero arg case special. George
Le Wed, 15 Apr 2009 11:17:07 -0700, "Raymond Hettinger" <python@rcn.com> s'exprima ainsi:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2])
While there has been quick support for the proposal, I do not find it as obvious as it seems. I see an issue based on a possible confusion about "default". Actually, "default" is the name of a proposed optional argument for min() and max() -- but this does not mean this argument, that happens to be called "default", itself has an evident default value ;-) Even for min(), I really doubt 0 is a right choice as "default"'s default value; while for max() it's imo obviously wrong. The issue as I see it is related to the fact that python does not allow optional arguments without default values -- which in most cases is not problematic. But here I would like an hypothetical min(s, optional default) or min(s, ?default) While this is not possible, I support the proposal with None as default value for "default", instead of an often wrong choice. min(s, default=None) max(s, default=None) Maybe another word as "default" would help avoid confusion, too. Denis ------ la vita e estrany
spir wrote:
The issue as I see it is related to the fact that python does not allow optional arguments without default values -- which in most cases is not problematic. But here I would like an hypothetical min(s, optional default) or min(s, ?default)
Actually, you can have true optional arguments in Python. They're especially easy to do for functions written in C, but there a couple of common tricks for writing them in pure Python as well: 1. Extract the optional keyword-only argument manually (this most closely mimics the approach used for optional arguments in C code, and is also the only way to get keyword-only arguments in Python 2.x) def f(**kwds): try: arg = kwds.pop("optional") have_arg = True except KeyError: have_arg = False if kwds: raise TypeError("Unexpected keyword arguments") return have_arg
f() False f(optional=None) True f(fred=1) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 8, in f TypeError: Unexpected keyword arguments f(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: f() takes exactly 0 arguments (1 given)
The downside of that approach is that you have to check for unexpected keyword arguments yourself, which leads directly to the second approach. 2. Use a custom sentinel value to indicate a missing keyword-only argument (this only works in Python 3.x where keyword-only parameter syntax is available) MISSING = object() def f(*, optional=MISSING): return optional is not MISSING
f() False f(optional=1) True f(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: f() takes exactly 0 positional arguments (1 given) f(fred=1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: f() got an unexpected keyword argument 'fred'
-- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia ---------------------------------------------------------------
On 15 Apr 2009, at 11:17, Raymond Hettinger wrote:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Here's one option... I'm going to cheat a little here and just wrap the built-in min, but a quick/simple answer could be: def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2])
# Your examples min2() -> TypeError min2(default=0) -> 0 min2(1,2,default=0) -> 1 min2([1,2], default=0) -> 1 # Iterator that yields things that are not comparable min2([1, set()]) -> TypeError min2([1, set()], default=7 ) -> 7 # Iterator that raises an exception def foo(): yield 1 raise ValueError min(foo()) -> ValueError min2(foo()) -> ValueError min2(foo(), default=None) -> None Jared
On 16 Apr 2009, at 04:39, Jared Grubb wrote:
def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
Nitpick:
class Err(Exception): pass ... def it(): ... raise Err() ... yield 42 ... min(it()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in it __main__.Err min2(it()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in min2 File "<stdin>", line 2, in it __main__.Err min2(it(), default=12) 12
Shouldn't the last one raise Err as well? -- Arnaud
On Thu, 16 Apr 2009 07:10:52 pm Arnaud Delobelle wrote:
On 16 Apr 2009, at 04:39, Jared Grubb wrote:
def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
Nitpick:
I don't think pointing out that the proposed behaviour inappropriately swallows random exceptions is a nitpick. I think it's a valuable service :) I think it is vital that min() and max() don't hide bugs by swallowing all exceptions. Here's my go at a pure Python version: SENTINEL = object() def is_iterable(obj): try: iter(obj) except TypeError: return False return True def min(*vars, key=None, default=SENTINEL): if len(vars) == 1: if is_iterable(vars): vars = iter(vars[0]) else: raise TypeError try: smallest = vars.next() except StopIteration: if default is SENTINEL: raise ValueError else: return default if key is not None: smallest = key(smallest) for value in vars: if key is not None: value = key(value) if value < smallest: smallest = vars return smallest -- Steven D'Aprano
On Thu, 16 Apr 2009 10:09:12 pm Steven D'Aprano wrote:
I think it is vital that min() and max() don't hide bugs by swallowing all exceptions. Here's my go at a pure Python version:
As folks may have noticed by actually trying to run the damn thing, my first attempt hadn't been tested at all and failed miserably to work. Think of it as pseudo-code, which believe it or not I did intend to write but got distracted and forgot. *wry-grin* (Thanks to Denis Spir for the polite way he pointed that out off-list.) I agree with Raymond that in the absence of a compelling use-case, python-dev won't accept the proposal. (I'm still +1 on the idea, but I know when I'm licked.) For the record, in case future generations want to re-visit the proposal, here's a pure Python version of min() plus default which I have tested in Python 2.6.1 and I think it should match the behaviour of the built-in min(). Anyone who wants to use it should feel free to do so (credit would be nice but not required). def is_iterable(obj): try: iter(obj) except TypeError: return False return True def min(*vars, **kwargs): SENTINEL = object() kw = {'key': None, 'default': SENTINEL} kw.update(kwargs) if len(kw) != 2: raise TypeError('min() got an unexpected key word argument') key = kw['key'] default = kw['default'] if len(vars) == 1: if is_iterable(vars[0]): vars = iter(vars[0]) else: raise TypeError( "'%s' object is not iterable" % type(vars[0]).__name__) else: vars = iter(vars) try: result = vars.next() except StopIteration: if default is SENTINEL: raise ValueError("min() arg is an empty sequence") else: return default compare_result = result if key is None else key(result) for value in vars: compare_value = value if key is None else key(value) if compare_value < compare_result: result = value compare_result = compare_value return result -- Steven D'Aprano
FWIW, I ran Steven's version against "test_min" from test_builtin.py, and it passed all those tests. Jared On 16 Apr 2009, at 18:35, Steven D'Aprano wrote:
For the record, in case future generations want to re-visit the proposal, here's a pure Python version of min() plus default which I have tested in Python 2.6.1 and I think it should match the behaviour of the built-in min(). Anyone who wants to use it should feel free to do so (credit would be nice but not required).
def is_iterable(obj): try: iter(obj) except TypeError: return False return True
def min(*vars, **kwargs): SENTINEL = object() kw = {'key': None, 'default': SENTINEL} kw.update(kwargs) if len(kw) != 2: raise TypeError('min() got an unexpected key word argument') key = kw['key'] default = kw['default'] if len(vars) == 1: if is_iterable(vars[0]): vars = iter(vars[0]) else: raise TypeError( "'%s' object is not iterable" % type(vars[0]).__name__) else: vars = iter(vars) try: result = vars.next() except StopIteration: if default is SENTINEL: raise ValueError("min() arg is an empty sequence") else: return default compare_result = result if key is None else key(result) for value in vars: compare_value = value if key is None else key(value) if compare_value < compare_result: result = value compare_result = compare_value return result
-- Steven D'Aprano
On 16 Apr 2009, at 02:10, Arnaud Delobelle wrote:
On 16 Apr 2009, at 04:39, Jared Grubb wrote:
def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
Nitpick: [...]
Yes, the "except Exception" was intentional such that "default=..." gives a no-throw guarantee. (I originally had "except TypeError", but then that would swallow all TypeError, even those given by the iterator; and between THOSE two behaviors, no-throw seemed most intuitive) I'm 0 on whether that should be the semantics or not, but as some have pointed out, swallowing bugs in iterators is not always a good thing. On the other hand, a no-throw min is kinda nice too... I personally prefer some of the other posters' versions that basically do "default catches only the TypeError that min would have thrown if iterator was empty" semantics. Jared
Le Wed, 15 Apr 2009 20:39:26 -0700, Jared Grubb <jared.grubb@gmail.com> s'exprima ainsi:
On 15 Apr 2009, at 11:17, Raymond Hettinger wrote:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Here's one option... I'm going to cheat a little here and just wrap the built-in min, but a quick/simple answer could be:
def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
Is the purpose of this proposal really to return a default value in *any* case of exception?
Will min(default=0) still return a TypeError? Will min(1, 2, default=0) return 0 or 1? Will min([1,2], default=0) return 1? # different from min([0,1,2])
# Your examples min2() -> TypeError min2(default=0) -> 0 min2(1,2,default=0) -> 1 min2([1,2], default=0) -> 1
# Iterator that yields things that are not comparable min2([1, set()]) -> TypeError min2([1, set()], default=7 ) -> 7 ***1***
# Iterator that raises an exception def foo(): yield 1 raise ValueError
min(foo()) -> ValueError min2(foo()) -> ValueError min2(foo(), default=None) -> None ***2***
In the case #2 above, maybe there is some utility to get a default. I consider the case #1 as real programming error that should be properly warned with an exception.
Jared
Intuitively, I thought the proposal was rather about a possibly empty iterable, result of previous unpredictable computations; then calling: min(iterable, default=whatever)
min([]) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: min() arg is an empty sequence def min2(*vars, **kw): ... # case vars holds (only) an empty iterable and "default" is provided ... if len(vars) == 1: ... thing = vars[0] ... try: ... if len(thing) == 0 and 'default' in kw: ... return kw['default'] ... except TypeError: ... pass ... # else normal min() behaviour ... if 'default' in kw: ... del kw['default'] ... return min(*vars, **kw) min2([], default='X') 'X' min2(1,set(), default='X') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 13, in min2 TypeError: can only compare to a set min2((1,set()), default='X') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 13, in min2 TypeError: can only compare to a set min(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable min2(1, default='X') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 13, in min2 TypeError: 'int' object is not iterable min((1)) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable min2((1), default='X') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 13, in min2 TypeError: 'int' object is not iterable min([1]) 1 min2([1], default='X') 1
But, as said before, I have no clue about exceptions raised by a generator. Denis ------ la vita e estrany
Jared Grubb wrote:
On 15 Apr 2009, at 11:17, Raymond Hettinger wrote:
[Adam Atlas]
I propose adding a "default" keyword argument to max() and min(), which provides a value to return in the event that an empty iterable is passed.
Could you write your proposal out in pure python so we can see how it interacts with the key-keyword argument and how it works when the number of positional arguments is not one.
Here's one option... I'm going to cheat a little here and just wrap the built-in min, but a quick/simple answer could be:
def min2(*vars, **kw): try: if 'key' in kw: return min(*vars, key=kw['key']) return min(*vars) except Exception: if 'default' in kw: return kw['default'] raise
This will swallow exceptions that are completely unrelated to the number of values we are taking the min() of. I am -1 on that. Here is my version. It also includes an 'initial' argument (not part of Adams proposal) to illustrate the difference between the two concepts. I would be +1 to adding 'initial' as well, but that is less interesting than 'default' because the 'initial' behavior is easy to get in other ways, e.g. "min(itertools.chain((initial,), values), key=func)". _marker = object() def min(*args, **kwargs): # extract and validate kwargs initial = kwargs.pop('initial', _marker) default = kwargs.pop('default', _marker) key = kwargs.pop('key', _marker) if kwargs: raise TypeError('min() got an unexpected keyword argument') # validate args, this TypeError is needed for backwards compatibility if initial is _marker and default is _marker and not args: raise TypeError('min expected 1 arguments, got 0') # create iterator for the values if len(args) == 1: it = iter(args[0]) else: it = iter(args) # extract first value if any and handle empty sequence if initial is not _marker: result = initial else: for result in it: break else: if default is _marker: raise ValueError('min() arg is an empty sequence') return default # handle remaining values if key is _marker: for value in it: if value < result: result = value else: resultkey = key(result) for value in it: valuekey = key(value) if valuekey < resultkey: result, resultkey = value, valuekey return result And here is what I get with the examples so far:
min() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in min TypeError: min expected 1 arguments, got 0 min(default=0) 0 min(1,2,default=0) 1 min([1,2],default=0) 1 min([1,set()]) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 29, in min TypeError: can only compare to a set min([1,set()], default=7) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 29, in min TypeError: can only compare to a set def foo(): ... yield 1 ... raise ValueError ... min(foo()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 28, in min File "<stdin>", line 3, in foo ValueError min(foo(), default=None) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 28, in min File "<stdin>", line 3, in foo ValueError
Cheers - Jacob
On 16 Apr 2009, at 11:33, Jacob Holm wrote:
_marker = object()
def min(*args, **kwargs): # extract and validate kwargs initial = kwargs.pop('initial', _marker) default = kwargs.pop('default', _marker) key = kwargs.pop('key', _marker) if kwargs: raise TypeError('min() got an unexpected keyword argument') # validate args, this TypeError is needed for backwards compatibility if initial is _marker and default is _marker and not args: raise TypeError('min expected 1 arguments, got 0') # create iterator for the values if len(args) == 1: it = iter(args[0]) else: it = iter(args) # extract first value if any and handle empty sequence if initial is not _marker: result = initial else: for result in it: break else: if default is _marker: raise ValueError('min() arg is an empty sequence') return default # handle remaining values if key is _marker: for value in it: if value < result: result = value else: resultkey = key(result) for value in it: valuekey = key(value) if valuekey < resultkey: result, resultkey = value, valuekey return result
I made a similar implementation, that I post here FWIW (using positional only arguments makes it slighly more compact): _not_provided = object() def min(first, *rest, key=_not_provided, default=_not_provided): if not rest: rest = iter(first) for first in rest: break else: if default is _not_provided: raise ValueError("min() arg is an empty sequence") else: return default if key is _not_provided: for el in rest: if el < first: first = el else: fkey = key(first) for el in rest: elkey = key(el) if elkey < fkey: first, fkey = el, elkey return first
Hi Arnaud Arnaud Delobelle wrote:
On 16 Apr 2009, at 11:33, Jacob Holm wrote:
[snip my example]
_not_provided = object()
def min(first, *rest, key=_not_provided, default=_not_provided): if not rest: rest = iter(first) for first in rest: break else: if default is _not_provided: raise ValueError("min() arg is an empty sequence") else: return default if key is _not_provided: for el in rest: if el < first: first = el else: fkey = key(first) for el in rest: elkey = key(el) if elkey < fkey: first, fkey = el, elkey return first
Yes, that is indeed a bit easier on the eyes, but doesn't work on 2.6. Also your version treats the case "min(default=0)" differently from mine. That might be a good thing though :) The only reason I could see for anyone hitting that case would be a use like "min(*values, default=0)", and that is much better handled by dropping the star anyway because of the special case for sequences of length 1. Cheers - Jacob
2009/4/16 Jacob Holm <jh@improva.dk>:
# validate args, this TypeError is needed for backwards compatibility if initial is _marker and default is _marker and not args: raise TypeError('min expected 1 arguments, got 0')
I would prefer to raise this error if not args, regardless of initial and default. This makes the preconditions simpler, and the excluded cases are never useful (the result value is statically known if the length of args is statically known, which should always be true to avoid the case when it has length 1). It would even make sense to allow initial and default only if args has length 1. This would again exclude cases where the arguments contain a statically known redundancy, but this time the preconditions would be more complicated. -- Marcin Kowalczyk qrczak@knm.org.pl http://qrnik.knm.org.pl/~qrczak/
Marcin 'Qrczak' Kowalczyk wrote:
2009/4/16 Jacob Holm <jh@improva.dk>:
# validate args, this TypeError is needed for backwards compatibility if initial is _marker and default is _marker and not args: raise TypeError('min expected 1 arguments, got 0')
I would prefer to raise this error if not args, regardless of initial and default. This makes the preconditions simpler, and the excluded cases are never useful (the result value is statically known if the length of args is statically known, which should always be true to avoid the case when it has length 1).
I agree.
It would even make sense to allow initial and default only if args has length 1. This would again exclude cases where the arguments contain a statically known redundancy, but this time the preconditions would be more complicated.
It would complicate the preconditions without much gain, but I don't really care either way. - Jacob
Le Thu, 16 Apr 2009 14:14:25 +0200, "Marcin 'Qrczak' Kowalczyk" <qrczak@knm.org.pl> s'exprima ainsi:
It would even make sense to allow initial and default only if args has length 1. This would again exclude cases where the arguments contain a statically known redundancy, but this time the preconditions would be more complicated.
As I understand it, anyway, default really makes sense only when args has length 1 and args[0] is an iterable. Cannot really see the sense of initial for min()/max(). Denis ------ la vita e estrany
It would even make sense to allow initial and default only if args has length 1. This would again exclude cases where the arguments contain a statically known redundancy, but this time the preconditions would be more complicated.
As I understand it, anyway, default really makes sense only when args has length 1 and args[0] is an iterable. Cannot really see the sense of initial for min()/max().
So the motivating case for a default argument boils down to: * The input is an iterable (otherwise the number of positional arguments is already known when the call is written). * The input is not a sequence of known length (otherwise, you could just use "min(seq) if seq else default"). * The input is potentially long (otherwise you could trivially convert to a sequence with list(iterable)). * The input is potentially empty (otherwise you wouldn't need a default). * There is a semantically meaningful default case for an empty input. * You only want the min or max but no other information from the iterable (otherwise you would need to convert it to a sequence so that min/max wouldn't consume all the data). I think this is a YAGNI case. Yes, it does come up every now and then but I don't think it is worth complicating what should be a very simple function. FWIW, we recently rejected a perfectly reasonable addition to operator.attrgetter() and operator.itemgetter() that would have added a default value. The problem is that it didn't work well with the other extensions that had already been accepted (like having multiple arguments such as itemgetter(1, 4, 7) or dotted attribute chains like attrgetter("store.department.register")). The concept behind the rejection is that it isn't worthwhile to overload a function with too many alternative extensions even if the extensions make sense taken individually. I contend that min/max are already in that position. They already have some signature complexity with min()-->TypeError; min(x)-->where-x-is-iterable; min(x,y,z)-->where-args-are-unrolled; and min(*args) changing behavior based on the length of args. On top of that, we've already extended min/max with a key= argument, further adding to its complexity. I think the default arg is a bridge too far. This is evidenced by the complexity of the suggested implementations (remember the zen of python). And, it is evidenced above discussion on signature complexity resulting from a kitchen-sink full of individually simple extensions. This is also evidenced by the disagreements in this thread about what the various corner cases should do. And, at least one respondent significantly misinterpreted the default-argument as being equivalent to a initial-argument. There has been almost zero discussion on use cases. There is no doubt that they exist, but no compelling cases have been presented (i.e. real-world code that is much improved with the extension). Likewise, there has been no discussion of alternatives (it is not hard to write an itertool that wraps an iterator with something that supplies a default when the iterator is empty). one-aspect-of-language-design-is-knowing-when-quit-ly yours, Raymond
On Thu, Apr 16, 2009, Raymond Hettinger wrote:
There has been almost zero discussion on use cases. There is no doubt that they exist, but no compelling cases have been presented (i.e. real-world code that is much improved with the extension). Likewise, there has been no discussion of alternatives (it is not hard to write an itertool that wraps an iterator with something that supplies a default when the iterator is empty).
one-aspect-of-language-design-is-knowing-when-quit-ly yours,
+1 -- Aahz (aahz@pythoncraft.com) <*> http://www.pythoncraft.com/ "If you think it's expensive to hire a professional to do the job, wait until you hire an amateur." --Red Adair
On Thu, Apr 16, 2009 at 2:57 PM, Raymond Hettinger <python@rcn.com> wrote:
So the motivating case for a default argument boils down to:
* The input is an iterable (otherwise the number of positional arguments is already known when the call is written).
Yes.
* The input is not a sequence of known length (otherwise, you could just use "min(seq) if seq else default").
True, but the latter is easy to forget, less succinct and easy to miss when refactoring a function to work on iterables instead of sequences only.
* The input is potentially long (otherwise you could trivially convert to a sequence with list(iterable)).
That should be the default mentality; unless one *knows* that the input is "short" (for some definition of "short"), he should assume that it is potentially long. Regardless of the length, I don't think it's the responsibility of the iterable's consumer to convert it; if the input is always short, why it's not a sequence in the first place ?
* The input is potentially empty (otherwise you wouldn't need a default).
Yes.
* There is a semantically meaningful default case for an empty input.
Yes, typically 0 or None.
* You only want the min or max but no other information from the iterable (otherwise you would need to convert it to a sequence so that min/max wouldn't consume all the data).
Yes, I often use min/max with gen. expressions: Compare: if min(f(x) for x in iterable if x>0) > 0: with _values = [f(x) for x in iterable if x>0] if _values and min(_values) > 0:
I think this is a YAGNI case. Yes, it does come up every now and then but I don't think it is worth complicating what should be a very simple function.
The discussion has indeed sidetracked with handling the special cases, signature definition and whatnot, but I believe meeting the conditions you outlined above is not as rare as their number implies. I hope the rest of the thread focuses on this motivating case so that this proposal is not rejected due to excessive bikeshedding. George
Yes, I often use min/max with gen. expressions: Compare: if min(f(x) for x in iterable if x>0) > 0:
Do you mean: if min((f(x) for x in iterable if x>0), default=0) > 0: ... I don't find that to be a clear expression of what you're trying to do. Too much logic forced into a one-liner (also note that the inner parens are required).
with _values = [f(x) for x in iterable if x>0] if _values and min(_values) > 0:
or with: if all(f(x)>0 for x in iterable if x>0): ... I think you're focusing on just one solution, one that involves piling-up too many extensions in one function that should be dirt simple. There are many other approaches: try/except, wrap the input in a default itertool, use all(), use next(it, default) to test the first value, etc.
The discussion has indeed sidetracked with handling the special cases, signature definition and whatnot, but I believe meeting the conditions you outlined above is not as rare as their number implies.
This may be a symptom of a particular programming style. I've found zero useful examples in scans of the standard library, in my own personal code base, or third-party extensions that I use regularly.
I hope the rest of the thread focuses on this motivating case so that this proposal is not rejected due to excessive bikeshedding.
A discussion of use cases is always helpful, but the rest of the discussion wasn't bikeshedding. It revealed that the default-argument doesn't make sense with non-iterable positional arguments and that some were confusing it with an initial-argument. No one yet has produced a clean, pure-python version that only affects a single iterable argument (ignoring positional cases where a default doesn't make sense) and that doesn't wrap the existing min/max code (it is important to look at the fully spelled-out pure python code to see that the overall design, taking all features into account, isn't clean). Also, I did a couple quick checks on other languages to see any use a default for empty min() but had no luck. Do you know of any languages where a min() with default is a proven best practice?
As an aside, it would be nice If min/max start raising a more narrow ValueError subtype, say EmptyIterableError, so that hacks such as checking the exception message are not necessary.
I would support that proposal if it would end this effort to complexify min/max. Raymond FWIW, here's an itertool recipe that you may find useful. def default(iterable, default=None): '''Yield elements of the iterable or if it is empty, yield the default. default([1,2,3], default=0) --> 1 2 3 default([], default=0) --> 0 ''' it = iter(iterable) return chain([next(it, default)], it)
On Thu, Apr 16, 2009 at 7:07 PM, Raymond Hettinger <python@rcn.com> wrote:
I think you're focusing on just one solution, one that involves piling-up too many extensions in one function that should be dirt simple. There are many other approaches: try/except, wrap the input in a default itertool, use all(), use next(it, default) to test the first value, etc.
Right, I've now grown used to wrapping it in try/except in advance just to be on the safe side; in the past I had to go back and fix it after it has already bombed with a ValueError once. So yes, overall I find the proposed parameter a handy convenience, not a fix to a glaring omission. I think what it boils down to is how exceptional is the empty iterable for the overall task at hand. Does what follows the min/max computation differ substantially when the input is empty from when it's not, or does the following code can remain blissfully ignorant about how was the value determined ? In the former case, raising an exception is a feature since it forces the user to think about this case and handle it explicitly. If not, then it's just extra boilerplate we could do without. A good example of the first case is division by zero: typically the course of action is totally different if some intermediate computation involves division by zero; you can't just replace it with some default and continue with the rest computations as if nothing happened (although a default might make sense as the "final" result, depending on the application). OTOH, code that uses dict.get() or getattr() with a default doesn't really care whether the key/attribute is actually in the queried dict/object, the following logic remains the same. So the bottom line is, are most use cases of taking the min/max of an empty iterable closer to these that involve an intermediate DivisionByZero or to those that involve a missing key/attribute ? I don't have a general answer, both make sense under different circumstances. George PS: Thanks for the default() recipe, seems generally useful, not only for min/max.
George Sakkis wrote:
So the bottom line is, are most use cases of taking the min/max of an empty iterable closer to these that involve an intermediate DivisionByZero or to those that involve a missing key/attribute ?
In my experience, it's always been much more like DivisionByZero -- either it shouldn't happen in the first place or something radically different must be done. -- Greg
On 16 Apr 2009, at 16:07, Raymond Hettinger wrote:
The discussion has indeed sidetracked with handling the special cases, signature definition and whatnot, but I believe meeting the conditions you outlined above is not as rare as their number implies.
This may be a symptom of a particular programming style. I've found zero useful examples in scans of the standard library, in my own personal code base, or third-party extensions that I use regularly.
I did find one example in the stdlib in my scan (Python 2.6.1): doctest.py: def _min_indent(self, s): "Return the minimum indentation of any non-blank line in `s`" indents = [len(indent) for indent in self._INDENT_RE.findall(s)] if len(indents) > 0: return min(indents) else: return 0 Note, however, that I only found 3 examples total that used an iterable at all (almost all of the cases in the stdlib were of the form "min(x,y)"). The other two examples were in timeit and urllib2, but each knew the iterables were non-empty because it had explicitly constructed them in such a way that they could not be empty. Jared
Le Thu, 16 Apr 2009 16:07:10 -0700, "Raymond Hettinger" <python@rcn.com> s'exprima ainsi:
As an aside, it would be nice If min/max start raising a more narrow ValueError subtype, say EmptyIterableError, so that hacks such as checking the exception message are not necessary.
I would support that proposal if it would end this effort to complexify min/max.
This would be a satisfying solution for me: try: result = min(iterable) except EmptyIterableError: result = default Rather pythonic I guess; as well as much simpler and easier to implement. Denis ------ la vita e estrany
Raymond Hettinger wrote:
The discussion has indeed sidetracked with handling the special cases, signature definition and whatnot,
Does the following code emit 10 or -10?
print(min([], default=10, key=operator.neg))
10, obviously. The result is always one of the provided values. The key function is only used for selecting which one - Jacob
On Thu, Apr 16, 2009 at 2:57 PM, Raymond Hettinger <python@rcn.com> wrote:
I think the default arg is a bridge too far. This is evidenced by the complexity of the suggested implementations (remember the zen of python).
FWIW, the following works good enough for my use cases: def min2(*args, **kwds): if 'default' not in kwds: return min(*args, **kwds) default = kwds.pop('default') try: return min(*args, **kwds) except ValueError, ex: if 'arg is an empty sequence' in ex.message: return default raise As an aside, it would be nice If min/max start raising a more narrow ValueError subtype, say EmptyIterableError, so that hacks such as checking the exception message are not necessary. George
Le Thu, 16 Apr 2009 17:06:17 -0400, George Sakkis <george.sakkis@gmail.com> s'exprima ainsi:
As an aside, it would be nice If min/max start raising a more narrow ValueError subtype, say EmptyIterableError, so that hacks such as checking the exception message are not necessary.
+1 ------ la vita e estrany
participants (14)
-
Aahz
-
Adam Atlas
-
Arnaud Delobelle
-
George Sakkis
-
Greg Ewing
-
Jacob Holm
-
Jared Grubb
-
John Arbash Meinel
-
Marcin 'Qrczak' Kowalczyk
-
Nick Coghlan
-
Raymond Hettinger
-
spir
-
Steven D'Aprano
-
Tal Einat