Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-32320: Add default value support to collections.namedtuple() #4859

Merged
merged 4 commits into from
Jan 11, 2018

Conversation

rhettinger
Copy link
Contributor

@rhettinger rhettinger commented Dec 14, 2017

@@ -805,6 +805,13 @@ they add the ability to access fields by name instead of position index.
converted to ``['abc', '_1', 'ghi', '_3']``, eliminating the keyword
``def`` and the duplicate fieldname ``abc``.

*defaults* can be ``None`` or an :term:`iterable` of default values.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the interface be simpler if make the default value of defaults an empty tuple?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler but that isn't the norm of Python standard library APIs and I don't really like the way it looks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the norm. Run git grep '=[(][)]' and you will get numerous examples.

.. doctest::

>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>> Account._fields_defaults
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to demonstrate the purposed effect of having default values:

>>> Account('premium')
Account(type='premium', balance=0)

_fields_defaults is for advanced users, which are able to read the documentation and help carefully. The ability to omit arguments in the constructor is the purpose of using the defaults argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I've expanded the example

defaults = tuple(defaults)
if len(defaults) > len(field_names):
raise TypeError('Got more default values than field names')
field_defaults = dict(reversed(list(zip(reversed(field_names),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the following code be more clear?

if defaults:
    field_defaults = dict(zip(field_names[-len(defaults)], defaults))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that the zero-length case has to be guarded against. It may be a matter of taste, but I prefer the current code over negative indexing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But your code already contains special guards for None. You could just change the one of guards.

@@ -332,7 +332,8 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
if isinstance(field_names, str):
field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names))
typename = str(typename)
typename = _sys.intern(str(typename))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed.

If typename is not valid name, ValueError will be raised below. But it already have seat in the interned string repository.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typename does not become automatically interned with type(typename), (tuple,), namespace):

>>> import sys
>>> tn = 'Po'
>>> tn1 = tn + 'int'
>>> tn2 = tn + 'i' + 'nt'
>>> t1 = type(tn1, (), {})
>>> t2 = type(tn2, (), {})
>>> t1.__name__ is t2.__name__
False

So, I do think it it wise to intern the typename.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

with self.assertRaises(TypeError): # non-iterable defaults
Point = namedtuple('Point', 'x y', defaults=10)

Point = namedtuple('Point', 'x y', defaults=None) # default is None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for empty defaults.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

with self.assertRaises(TypeError): # catch too few args
Point(10)

Point = namedtuple('Point', 'x y', defaults=[10, 20]) # allow non-tuple iterable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth to add a test for non-reiterable value, e.g. iter([10, 20])?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

with self.assertRaises(TypeError): # too many defaults
Point = namedtuple('Point', 'x y', defaults=(10, 20, 30))
with self.assertRaises(TypeError): # non-iterable defaults
Point = namedtuple('Point', 'x y', defaults=10)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for a false non-iterable value like defaults=False.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Added another test.

Copy link
Contributor

@asottile asottile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, I've been using (abusing) this approach for quite a while with success:

https://github.com/pre-commit/pre-commit/blob/a543190f7b40fb7e2e783eb6a09669360b133669/pre_commit/envcontext.py#L12-L13

exec(s, namespace)
__new__ = namespace['__new__']
__new__.__doc__ = f'Create new instance of {typename}({arg_list})'
if defaults is not None:
__new__.__defaults__ = defaults
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could make the default defaults=() and then avoid this branch here and always assign __new__.__defaults__

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would work. I prefer the current approach though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-feature A feature request or enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants