Skip to content

Commit 3771643

Browse files
authored
feat: metadata and __slots__ for Histogram (#403)
1 parent 98ddba8 commit 3771643

File tree

4 files changed

+55
-27
lines changed

4 files changed

+55
-27
lines changed

src/boost_histogram/_internal/axis.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def metadata(self):
8080
def metadata(self, value):
8181
self._ax.metadata = value
8282

83+
@metadata.deleter
84+
def metadata(self):
85+
self._ax.metadata = None
86+
8387
@classmethod
8488
def _convert_cpp(cls, cpp_object):
8589
nice_ax = cls.__new__(cls)

src/boost_histogram/_internal/hist.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ def _expand_ellipsis(indexes, rank):
8484
@set_family(MAIN_FAMILY)
8585
@set_module("boost_histogram")
8686
class Histogram(object):
87-
@inject_signature("self, *axes, storage=Double()", locals={"Double": Double})
87+
__slots__ = ("_hist", "axes", "metadata")
88+
89+
@inject_signature(
90+
"self, *axes, storage=Double(), metadata=None", locals={"Double": Double}
91+
)
8892
def __init__(self, *axes, **kwargs):
8993
"""
9094
Construct a new histogram.
@@ -99,22 +103,27 @@ def __init__(self, *axes, **kwargs):
99103
Provide 1 or more axis instances.
100104
storage : Storage = bh.storage.Double()
101105
Select a storage to use in the histogram
106+
metadata : Any = None
107+
Data that is passed along if a new histogram is created
102108
"""
103109

104110
# Allow construction from a raw histogram object (internal)
105111
if not kwargs and len(axes) == 1 and isinstance(axes[0], _histograms):
106112
self._hist = axes[0]
113+
self.metadata = None
107114
self.axes = self._generate_axes_()
108115
return
109116

117+
# If we construct with another Histogram, support that too
110118
if not kwargs and len(axes) == 1 and isinstance(axes[0], Histogram):
111-
self._hist = copy.copy(axes[0]._hist)
112-
self.axes = self._generate_axes_()
119+
self.__init__(axes[0]._hist)
120+
self.metadata = axes[0].metadata
113121
return
114122

115123
# Keyword only trick (change when Python2 is dropped)
116124
with KWArgs(kwargs) as k:
117125
storage = k.optional("storage", Double())
126+
self.metadata = k.optional("metadata")
118127

119128
# Check for missed parenthesis or incorrect types
120129
if not isinstance(storage, Storage):
@@ -150,6 +159,22 @@ def _generate_axes_(self):
150159

151160
return AxesTuple(self._axis(i) for i in range(self.ndim))
152161

162+
def _new_hist(self, _hist):
163+
"""
164+
Return a new histogram given a new _hist, copying metadata.
165+
"""
166+
167+
other = self.__class__(_hist)
168+
other.metadata = self.metadata
169+
return other
170+
171+
@property
172+
def ndim(self):
173+
"""
174+
Number of axes (dimensions) of histogram.
175+
"""
176+
return self._hist.rank()
177+
153178
def view(self, flow=False):
154179
"""
155180
Return a view into the data, optionally with overflow turned on.
@@ -161,7 +186,7 @@ def __array__(self):
161186

162187
def __add__(self, other):
163188
if hasattr(other, "_hist"):
164-
return self.__class__(self._hist.__add__(other._hist))
189+
return self._new_hist(self._hist.__add__(other._hist))
165190
else:
166191
retval = self.copy()
167192
retval += other
@@ -188,7 +213,7 @@ def __ne__(self, other):
188213

189214
# If these fail, the underlying object throws the correct error
190215
def __mul__(self, other):
191-
return self.__class__(self._hist.__mul__(other))
216+
return self._new_hist(self._hist.__mul__(other))
192217

193218
def __rmul__(self, other):
194219
return self * other
@@ -203,15 +228,15 @@ def __truediv__(self, other):
203228
result.__itruediv__(other)
204229
return result
205230
else:
206-
return self.__class__(self._hist.__truediv__(_hist_or_val(other)))
231+
return self._new_hist(self._hist.__truediv__(_hist_or_val(other)))
207232

208233
def __div__(self, other):
209234
if isinstance(other, Histogram):
210235
result = self.copy()
211236
result.__idiv__(other)
212237
return result
213238
else:
214-
return self.__class__(self._hist.__div__(_hist_or_val(other)))
239+
return self._new_hist(self._hist.__div__(_hist_or_val(other)))
215240

216241
def __itruediv__(self, other):
217242
if isinstance(other, Histogram):
@@ -229,12 +254,6 @@ def __idiv__(self, other):
229254
self._hist.__idiv__(_hist_or_val(other))
230255
return self
231256

232-
def __copy__(self):
233-
other = self.__class__.__new__(self.__class__)
234-
other._hist = copy.copy(self._hist)
235-
other.axes = other._generate_axes_()
236-
return other
237-
238257
# TODO: Marked as too complex by flake8. Should be factored out a bit.
239258
@inject_signature("self, *args, weight=None, sample=None, threads=None")
240259
def fill(self, *args, **kwargs): # noqa: C901
@@ -347,21 +366,26 @@ def _storage_type(self):
347366
return cast(self, self._hist._storage_type, Storage)
348367

349368
def _reduce(self, *args):
350-
return self.__class__(self._hist.reduce(*args))
369+
return self._new_hist(self._hist.reduce(*args))
370+
371+
def __copy__(self):
372+
other = self._new_hist(copy.copy(self._hist))
373+
return other
351374

352375
def __deepcopy__(self, memo):
353376
other = self.__class__.__new__(self.__class__)
354377
other._hist = copy.deepcopy(self._hist, memo)
378+
other.metadata = copy.deepcopy(self.metadata, memo)
355379
other.axes = other._generate_axes_()
356380
return other
357381

358382
def __getstate__(self):
359-
state = self.__dict__.copy()
360-
del state["axes"] # Don't save the cashe
383+
state = {"_hist": self._hist, "metadata": self.metadata}
361384
return state
362385

363386
def __setstate__(self, state):
364-
self.__dict__.update(state)
387+
self._hist = state["_hist"]
388+
self.metadata = state["metadata"]
365389
self.axes = self._generate_axes_()
366390

367391
def __repr__(self):
@@ -489,13 +513,6 @@ def rank(self):
489513
warnings.warn(msg, FutureWarning)
490514
return self._hist.rank()
491515

492-
@property
493-
def ndim(self):
494-
"""
495-
Number of axes (dimensions) of histogram.
496-
"""
497-
return self._hist.rank()
498-
499516
@property
500517
def size(self):
501518
"""
@@ -579,12 +596,12 @@ def __getitem__(self, index): # noqa: C901
579596
reduced = self._hist.reduce(*slices)
580597

581598
if not integrations:
582-
return self.__class__(reduced)
599+
return self._new_hist(reduced)
583600
else:
584601
projections = [i for i in range(self.ndim) if i not in integrations]
585602

586603
return (
587-
self.__class__(reduced.project(*projections))
604+
self._new_hist(reduced.project(*projections))
588605
if projections
589606
else reduced.sum(flow=True)
590607
)
@@ -701,4 +718,4 @@ def project(self, *args):
701718
those axes only. Flow bins are used if available.
702719
"""
703720

704-
return self.__class__(self._hist.project(*args))
721+
return self._new_hist(self._hist.project(*args))

src/boost_histogram/_internal/kwargs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def __exit__(self, *args):
1515
if self.kwargs:
1616
raise TypeError("Keyword(s) {} not expected".format(", ".join(self.kwargs)))
1717

18+
def __contains__(self, item):
19+
return item in self.kwargs
20+
1821
def required(self, name):
1922
if name in self.kwargs:
2023
self.kwargs.pop(name)

tests/test_histogram_indexing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def test_get_1D_histogram():
6060
def test_get_1D_slice():
6161
h1 = bh.Histogram(bh.axis.Regular(10, 0, 1))
6262
h2 = bh.Histogram(bh.axis.Regular(5, 0, 0.5))
63+
h1.metadata = {"that": 3}
64+
6365
h1.fill([0.25, 0.25, 0.25, 0.15])
6466
h2.fill([0.25, 0.25, 0.25, 0.15])
6567

@@ -72,6 +74,8 @@ def test_get_1D_slice():
7274
assert len(h1[2:4].view()) == 2
7375
assert len(h1[2 : 4 : bh.rebin(2)].view()) == 1
7476

77+
assert h1[2:4].metadata == {"that": 3}
78+
7579

7680
def test_ellipsis():
7781

0 commit comments

Comments
 (0)