Last active
July 26, 2017 03:37
-
-
Save ncoghlan/a79e7a1b3f7dac11c6cfbbf59b189621 to your computer and use it in GitHub Desktop.
Auto-defined named tuples in Python 3.6+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Licensed under BSD 2-clause license | |
from collections import namedtuple | |
_AUTO_NTUPLE_FIELD_SEPARATOR = "__" | |
_AUTO_NTUPLE_PREFIX = "_ntuple" + _AUTO_NTUPLE_FIELD_SEPARATOR | |
def _fields_to_class_name(fields): | |
"""Generate a class name based on the given field names""" | |
return _AUTO_NTUPLE_PREFIX + _AUTO_NTUPLE_FIELD_SEPARATOR.join(fields) | |
def _class_name_to_fields(cls_name): | |
"""Extract field names from an auto-generated class name""" | |
parts = cls_name.split(_AUTO_NTUPLE_FIELD_SEPARATOR) | |
return tuple(parts[1:]) | |
class _AutoNamedTupleTypeCache(dict): | |
"""Pickle compatibility helper for autogenerated collections.namedtuple type definitions""" | |
def __new__(cls): | |
# Ensure that unpickling reuses the existing cache instance | |
self = globals().get("_AUTO_NTUPLE_TYPE_CACHE") | |
if self is None: | |
maybe_self = super().__new__(cls) | |
self = globals().setdefault("_AUTO_NTUPLE_TYPE_CACHE", maybe_self) | |
return self | |
def __missing__(self, fields): | |
if any(_AUTO_NTUPLE_FIELD_SEPARATOR in field for field in fields): | |
msg = "Field names {!r} cannot include field separator {!r}" | |
raise ValueError(msg.format(fields, _AUTO_NTUPLE_FIELD_SEPARATOR)) | |
cls_name = _fields_to_class_name(fields) | |
return self._define_new_type(cls_name, fields) | |
def __getattr__(self, cls_name): | |
if not cls_name.startswith(_AUTO_NTUPLE_PREFIX): | |
raise AttributeError(cls_name) | |
fields = _class_name_to_fields(cls_name) | |
return self._define_new_type(cls_name, fields) | |
def _define_new_type(self, cls_name, fields): | |
cls = namedtuple(cls_name, fields) | |
cls.__name__ = "auto_ntuple" | |
cls.__module__ = __name__ | |
cls.__qualname__ = "_AUTO_NTUPLE_TYPE_CACHE." + cls_name | |
# Rely on setdefault to handle race conditions between threads | |
return self.setdefault(fields, cls) | |
_AUTO_NTUPLE_TYPE_CACHE = _AutoNamedTupleTypeCache() | |
def auto_ntuple(**items): | |
"""Create a named tuple instance from ordered keyword arguments | |
Automatically defines and caches a new type if necessary. | |
""" | |
cls = _AUTO_NTUPLE_TYPE_CACHE[tuple(items)] | |
return cls(*items.values()) | |
# The following two APIs allow particular sequences of fields to be | |
# assigned symbolic names in place of the auto-generated ones | |
# They are definitely *NOT* thread safe in their current form. | |
def set_ntuple_name(cls_name, fields): | |
"""Assigns a meaningful symbolic name to a sequence of field names | |
Adjusts __name__ and __qualname__ on the type to use the symbolic | |
name instead of the auto-generated name. | |
""" | |
fields = tuple(fields) | |
cls_by_name = globals().get(cls_name) | |
if cls_by_name is not None: | |
assigned_fields = cls_by_name._fields | |
if assigned_fields == fields: | |
# Name is already assigned as requested | |
return | |
msg = "{!r} already used for {!r} ntuples, can't assign to {!r}" | |
raise RuntimeError(msg.format(cls_name, assigned_fields, fields)) | |
cls_by_fields = _AUTO_NTUPLE_TYPE_CACHE[fields] | |
assigned_name = cls_by_fields.__name__ | |
if assigned_name != "auto_ntuple": | |
msg = "{!r} ntuples already named as {!r}, can't rename as {!r}" | |
raise RuntimeError(msg.format(fields, assigned_name, cls_name)) | |
cls_by_fields.__name__ = cls_by_fields.__qualname__ = cls_name | |
globals()[cls_name] = cls_by_fields | |
def reset_ntuple_name(fields): | |
"""Resets an ntuple's name back to the default auto-generated name. | |
This allows it to be renamed.""" | |
fields = tuple(fields) | |
auto_cls_name = _fields_to_class_name(fields) | |
cls = _AUTO_NTUPLE_TYPE_CACHE[fields] | |
cls.__name__ = "auto_ntuple" | |
cls.__qualname__ = "_AUTO_NTUPLE_TYPE_CACHE." + auto_cls_name | |
# For pickle compatibility, any entry in the module globals is retained |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> p1 = auto_ntuple(x=1, y=2) | |
>>> p2 = auto_ntuple(x=4, y=5) | |
>>> type(p1) is type(p2) | |
True | |
>>> | |
>>> import pickle | |
>>> p3 = pickle.loads(pickle.dumps(p1)) | |
>>> p1 == p3 | |
True | |
>>> type(p1) is type(p3) | |
True | |
>>> | |
>>> p1, p2, p3 | |
(auto_ntuple(x=1, y=2), auto_ntuple(x=4, y=5), auto_ntuple(x=1, y=2)) | |
>>> type(p1) | |
<class '__main__._AUTO_NTUPLE_TYPE_CACHE._ntuple__x__y'> | |
>>> | |
>>> p4 = auto_ntuple(with_underscores="ok") | |
>>> p4 | |
auto_ntuple(with_underscores='ok') | |
>>> | |
>>> auto_ntuple(with__double__underscores="not ok") | |
Traceback (most recent call last): | |
... | |
ValueError: Field names ('with__double__underscores',) cannot include field separator '__' | |
>>> | |
>>> set_ntuple_name("Point2D", "x y".split()) | |
>>> p1, p2, p3, p4 | |
(Point2D(x=1, y=2), Point2D(x=4, y=5), Point2D(x=1, y=2), auto_ntuple(with_underscores='ok')) | |
>>> set_ntuple_name("CartesianCoordinates", "x y".split()) | |
Traceback (most recent call last): | |
... | |
RuntimeError: ('x', 'y') ntuples already named as 'Point2D', can't rename as 'CartesianCoordinates' | |
>>> reset_ntuple_name("x y".split()) | |
>>> p1, p2, p3 | |
(auto_ntuple(x=1, y=2), auto_ntuple(x=4, y=5), auto_ntuple(x=1, y=2)) | |
>>> set_ntuple_name("CartesianCoordinates", "x y".split()) | |
>>> p1, p2, p3 | |
(CartesianCoordinates(x=1, y=2), CartesianCoordinates(x=4, y=5), CartesianCoordinates(x=1, y=2)) | |
>>> type(p1) | |
<class '__main__.CartesianCoordinates'> | |
>>> | |
>>> p5 = pickle.loads(pickle.dumps(p1)) | |
>>> p5 == p1 | |
True | |
>>> type(p5) is type(p1) | |
True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Copyright 2017 Nicholas Coghlan. All rights reserved. | |
Redistribution and use in source and binary forms, with or without modification, are | |
permitted provided that the following conditions are met: | |
1. Redistributions of source code must retain the above copyright notice, this list of | |
conditions and the following disclaimer. | |
2. Redistributions in binary form must reproduce the above copyright notice, this list | |
of conditions and the following disclaimer in the documentation and/or other materials | |
provided with the distribution. | |
THIS SOFTWARE IS PROVIDED BY NICHOLAS COGHLAN ''AS IS'' AND ANY EXPRESS OR IMPLIED | |
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR | |
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
p1 = auto_ntuple(x=1, y=2) | |
p2 = auto_ntuple(x=4, y=5) | |
type(p1) is type(p2) | |
import pickle | |
p3 = pickle.loads(pickle.dumps(p1)) | |
p1 == p3 | |
type(p1) is type(p3) | |
p1, p2, p3 | |
type(p1) | |
p4 = auto_ntuple(with_underscores="ok") | |
p4 | |
auto_ntuple(with__double__underscores="not ok") | |
set_ntuple_name("Point2D", "x y".split()) | |
p1, p2, p3, p4 | |
set_ntuple_name("CartesianCoordinates", "x y".split()) | |
reset_ntuple_name("x y".split()) | |
p1, p2, p3 | |
set_ntuple_name("CartesianCoordinates", "x y".split()) | |
p1, p2, p3 | |
type(p1) | |
p5 = pickle.loads(pickle.dumps(p1)) | |
p5 == p1 | |
type(p5) is type(p1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Added
set_ntuple_name
andreset_ntuple_name
to illustrate how to give the autogenerated types prettier runtime aliases without breaking pickle compatibility.