ssb-sgis 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sgis/maps/map.py CHANGED
@@ -119,53 +119,68 @@ class Map:
119
119
  list(self._gdf.loc[~self._nan_idx, self._column].unique())
120
120
  )
121
121
 
122
- def _get_unique_floats(self) -> list[int | float]:
123
- """Converting floats to large integers, then getting unique values.
122
+ def _get_unique_floats(self) -> np.array:
123
+ """Get unique floats by multiplying, then converting to integer.
124
124
 
125
- Also making a column of the large integers to use in the bin classifying later.
125
+ Find a multiplier that makes the max value greater than +- 1_000_000.
126
+ Because floats don't always equal each other. This will make very
127
+ similar values count as the same value in the color classification.
126
128
  """
127
129
  array = self._gdf.loc[~self._nan_idx, self._column]
128
130
 
129
- self._gdf["col_as_int"] = self._array_to_large_int(array)
131
+ self._min = np.min(array)
132
+ self._max = np.max(array)
133
+ self._get_multiplier(array)
130
134
 
131
135
  unique = array.reset_index(drop=True).drop_duplicates()
132
-
133
136
  as_int = self._array_to_large_int(unique)
134
137
  no_duplicates = as_int.drop_duplicates()
135
- return list(sorted(unique.loc[no_duplicates.index]))
136
138
 
137
- @staticmethod
138
- def _array_to_large_int(array: np.ndarray):
139
+ return np.sort(np.array(unique.loc[no_duplicates.index]))
140
+
141
+ def _array_to_large_int(self, array: np.ndarray):
139
142
  """Multiply values in float array, then convert to integer."""
140
- max_ = np.max(array)
141
- min_ = np.min(array)
142
143
 
143
- if max_ > 1 or min_ < -1:
144
- unique_multiplied = array * np.emath.logn(1.25, np.abs(np.mean(array)) + 1)
145
- else:
146
- unique_multiplied = array
147
- while max_ < 1_000_000:
148
- unique_multiplied = unique_multiplied * 10
149
- max_ = np.max(unique_multiplied)
144
+ unique_multiplied = array * self._multiplier
150
145
 
151
146
  return unique_multiplied.astype(np.int64)
152
147
 
148
+ def _get_multiplier(self, array: np.ndarray):
149
+ """Find the number of zeros needed to push the max value of the array above
150
+ +-1_000_000.
151
+
152
+ Adding this as an attribute to use later in _classify_from_bins.
153
+ """
154
+ multiplier = 10
155
+ max_ = np.max(array * multiplier)
156
+
157
+ if self._max > 0:
158
+ while max_ < 1_000_000:
159
+ multiplier *= 10
160
+ max_ = np.max(array * multiplier)
161
+ else:
162
+ while max_ > -1_000_000:
163
+ multiplier *= 10
164
+ max_ = np.max(array * multiplier)
165
+
166
+ self._multiplier: int = multiplier
167
+
153
168
  def _add_minmax_to_bins(self, bins: list[float | int]) -> list[float | int]:
154
169
  """If values are outside the bin range, add max and/or min values of array."""
155
170
  # make sure they are lists
156
171
  bins = [bin for bin in bins]
157
172
 
158
- if min(bins) > 0 and min(self._gdf[self._column]) < min(bins) * 0.999:
159
- bins = [min(self._gdf[self._column]) * 0.9999] + bins
173
+ if min(bins) > 0 and min(self._gdf[self._column]) < min(bins):
174
+ bins = [min(self._gdf[self._column])] + bins
160
175
 
161
- if min(bins) < 0 and min(self._gdf[self._column]) < min(bins) * 1.0001:
162
- bins = [min(self._gdf[self._column]) * 1.0001] + bins
176
+ if min(bins) < 0 and min(self._gdf[self._column]) < min(bins):
177
+ bins = [min(self._gdf[self._column])] + bins
163
178
 
164
- if max(bins) > 0 and max(self._gdf[self._column]) > max(bins) * 1.0001:
165
- bins = bins + [max(self._gdf[self._column]) * 1.0001]
179
+ if max(bins) > 0 and max(self._gdf[self._column]) > max(bins):
180
+ bins = bins + [max(self._gdf[self._column])]
166
181
 
167
- if max(bins) < 0 and max(self._gdf[self._column]) < max(bins) * 1.0001:
168
- bins = bins + [max(self._gdf[self._column]) * 1.0001]
182
+ if max(bins) < 0 and max(self._gdf[self._column]) < max(bins):
183
+ bins = bins + [max(self._gdf[self._column])]
169
184
 
170
185
  return bins
171
186
 
@@ -194,7 +209,7 @@ class Map:
194
209
  """Create bins if not already done and adjust k if needed."""
195
210
 
196
211
  if not hasattr(self, "scheme"):
197
- self.scheme = self.kwargs.get("scheme", "fisherjenks")
212
+ self.scheme = self.kwargs.pop("scheme", "fisherjenks")
198
213
 
199
214
  if self.scheme is None:
200
215
  return
@@ -204,7 +219,6 @@ class Map:
204
219
  if len(self.bins) <= self._k and len(self.bins) != len(self._unique_values):
205
220
  warnings.warn(f"Could not create {self._k} classes.")
206
221
  self._k = len(self.bins)
207
- self.bins = self._add_minmax_to_bins(self.bins)
208
222
  else:
209
223
  self.bins = self._add_minmax_to_bins(self.bins)
210
224
  if len(self._unique_values) > len(self.bins):
@@ -285,10 +299,10 @@ class Map:
285
299
  return False
286
300
 
287
301
  if all_nan == len(self._gdfs):
288
- raise ValueError(f"All values are NaN in column {self.kwargs['column']!r}.")
302
+ raise ValueError(f"All values are NaN in column {self.column!r}.")
289
303
 
290
304
  if col_not_present == len(self._gdfs):
291
- raise ValueError(f"{self.kwargs['column']} not found.")
305
+ raise ValueError(f"{self.column} not found.")
292
306
 
293
307
  return False
294
308
 
@@ -323,7 +337,7 @@ class Map:
323
337
 
324
338
  self._gdf["color"] = self._gdf[self._column].map(self._categories_colors_dict)
325
339
 
326
- def _create_bins(self, gdf, column) -> np.ndarray:
340
+ def _create_bins(self, gdf: GeoDataFrame, column: str) -> np.ndarray:
327
341
  """Make bin list of length k + 1, or length of unique values.
328
342
 
329
343
  The returned bins sometimes have two almost identical
@@ -332,14 +346,6 @@ class Map:
332
346
  much faster than the one from Mapclassifier.
333
347
  """
334
348
 
335
- if hasattr(self, "scheme"):
336
- scheme = self.scheme
337
- else:
338
- scheme = self.kwargs.get("scheme", "fisherjenks")
339
-
340
- if scheme is None:
341
- return
342
-
343
349
  n_classes = (
344
350
  self._k if len(self._unique_values) > self._k else len(self._unique_values)
345
351
  )
@@ -347,12 +353,12 @@ class Map:
347
353
  if self._k == len(self._unique_values) - 1:
348
354
  n_classes = self._k - 1
349
355
 
350
- if scheme == "fisherjenks":
356
+ if self.scheme == "fisherjenks":
351
357
  bins = jenks_breaks(gdf.loc[~self._nan_idx, column], n_classes=n_classes)
352
358
  else:
353
359
  binning = classify(
354
360
  np.asarray(gdf.loc[~self._nan_idx, column]),
355
- scheme=scheme,
361
+ scheme=self.scheme,
356
362
  k=self._k,
357
363
  )
358
364
  bins = binning.bins
@@ -367,13 +373,7 @@ class Map:
367
373
  if len(unique_bins) == len(self._unique_values):
368
374
  return np.array(unique_bins)
369
375
 
370
- binarray = np.array(bins)
371
- binarray = np.where(
372
- binarray > 0,
373
- binarray + binarray / 100_000,
374
- binarray - binarray / 100_000,
375
- )
376
- return binarray
376
+ return np.array(bins)
377
377
 
378
378
  def change_cmap(self, cmap: str, start: int = 0, stop: int = 256):
379
379
  """Change the color palette of the plot.
@@ -391,47 +391,42 @@ class Map:
391
391
  self._cmap_has_been_set = True
392
392
  return self
393
393
 
394
- def _get_continous_colors(self) -> list[str]:
394
+ def _get_continous_colors(self, n: int) -> np.ndarray:
395
395
  cmap = matplotlib.colormaps.get_cmap(self._cmap)
396
396
  colors_ = [
397
397
  colors.to_hex(cmap(int(i)))
398
- for i in np.linspace(self.cmap_start, self.cmap_stop, num=self._k)
398
+ # for i in np.linspace(self.cmap_start, self.cmap_stop, num=self._k)
399
+ for i in np.linspace(self.cmap_start, self.cmap_stop, num=n)
399
400
  ]
400
401
  if any(self._nan_idx):
401
402
  colors_ = colors_ + [self.nan_color]
402
- return colors_
403
-
404
- def _classify_from_bins(self, gdf: GeoDataFrame) -> np.ndarray:
405
- """Place the values of the column into groups."""
406
- # if equal lenght, use integer column to check for equality
407
- # since long floats are unpredictable
408
- if len(self.bins) == len(self._unique_values):
409
- if "col_as_int" not in gdf.columns:
410
- gdf["col_as_int"] = self._array_to_large_int(gdf[self._column])
411
- bins = np.array(sorted(gdf["col_as_int"].unique()))
403
+ return np.array(colors_)
404
+
405
+ def _classify_from_bins(self, gdf: GeoDataFrame, bins: np.ndarray) -> np.ndarray:
406
+ """Place the column values into groups."""
407
+ if len(bins) == len(self._unique_values):
408
+ # if equal lenght, convert to integer and check for equality
409
+ gdf["col_as_int"] = self._array_to_large_int(gdf[self._column])
410
+ bins = self._array_to_large_int(self._unique_values)
412
411
  classified = np.searchsorted(bins, gdf["col_as_int"])
413
412
  else:
414
- if any(self._nan_idx) and len(self.bins) == len(self.colorlist):
415
- bins = self.bins[1:]
416
- elif not any(self._nan_idx) and len(self.bins) == len(self.colorlist) + 1:
417
- bins = self.bins[1:]
418
- else:
419
- bins = self.bins
413
+ if len(bins) == self._k + 1:
414
+ bins = bins[1:]
420
415
 
421
416
  classified = np.searchsorted(bins, gdf[self._column])
422
417
 
423
- # storing unique values to use in legend labels
424
- self._bins_unique_values = {
425
- i: list(set(gdf.loc[classified == i, self._column]))
426
- for i, _ in enumerate(bins)
427
- }
418
+ return classified
428
419
 
429
- colors_ = np.array(self.colorlist)
420
+ def _push_classification(self, classified: np.ndarray) -> np.ndarray:
421
+ """Push classes downwards if gaps in classification sequence.
430
422
 
431
- # nans are sorted to the end, so nans will get NAN_COLOR
432
- colors_classified = colors_[classified]
423
+ So from e.g. [0,2,4] to [0,1,2].
424
+
425
+ Otherwise, will get index error when classifying colors.
426
+ """
427
+ rank_dict = {val: rank for rank, val in enumerate(np.unique(classified))}
433
428
 
434
- return colors_classified
429
+ return np.array([rank_dict[val] for val in classified])
435
430
 
436
431
  @property
437
432
  def k(self):
sgis/maps/maps.py CHANGED
@@ -7,8 +7,6 @@ interactive map with layers that can be toggled on and off. The 'samplemap' and
7
7
  The 'qtm' function shows a static map of one or more GeoDataFrames.
8
8
  """
9
9
  from geopandas import GeoDataFrame, GeoSeries
10
- from matplotlib.axes._axes import Axes
11
- from matplotlib.figure import Figure
12
10
  from shapely import Geometry
13
11
 
14
12
  from ..exceptions import NotInJupyterError
@@ -39,12 +37,16 @@ def explore(
39
37
  show_in_browser: bool = False,
40
38
  **kwargs,
41
39
  ) -> None:
42
- """Interactive map of GeoDataFrames with layers that can be toggles on/off.
40
+ """Interactive map of GeoDataFrames with layers that can be toggled on/off.
43
41
 
44
42
  It takes all the given GeoDataFrames and displays them together in an
45
43
  interactive map with a common legend. If 'column' is not specified, each
46
44
  GeoDataFrame is given a unique color.
47
45
 
46
+ If the column is of type string and only one GeoDataFrame is given, the unique
47
+ values will be split into separate GeoDataFrames so that each value can be toggled
48
+ on/off.
49
+
48
50
  The coloring can be changed with the 'cmap' parameter. The default colormap is a
49
51
  custom, strongly colored palette. If a numerical colum is given, the 'viridis'
50
52
  palette is used.
@@ -299,6 +301,7 @@ def qtm(
299
301
  legend: bool = True,
300
302
  cmap: str | None = None,
301
303
  k: int = 5,
304
+ **kwargs,
302
305
  ) -> None:
303
306
  """Quick, thematic map of one or more GeoDataFrames.
304
307
 
@@ -320,6 +323,7 @@ def qtm(
320
323
  cmap: Color palette of the map. See:
321
324
  https://matplotlib.org/stable/tutorials/colors/colormaps.html
322
325
  k: Number of color groups.
326
+ **kwargs: Additional keyword arguments taken by the geopandas plot method.
323
327
 
324
328
  See also:
325
329
  ThematicMap: Class with more options for customising the plot.
@@ -329,7 +333,7 @@ def qtm(
329
333
 
330
334
  m.title = title
331
335
 
332
- if k and len(m._unique_values) >= 6:
336
+ if k and len(m._unique_values) >= k:
333
337
  m.k = k
334
338
 
335
339
  if cmap:
@@ -338,4 +342,4 @@ def qtm(
338
342
  if not legend:
339
343
  m.legend = None
340
344
 
341
- m.plot()
345
+ m.plot(**kwargs)
sgis/maps/thematicmap.py CHANGED
@@ -3,10 +3,11 @@ import warnings
3
3
 
4
4
  import matplotlib
5
5
  import matplotlib.pyplot as plt
6
+ import numpy as np
6
7
  import pandas as pd
7
8
  from geopandas import GeoDataFrame
8
9
 
9
- from .legend import Legend
10
+ from .legend import ContinousLegend, Legend
10
11
  from .map import Map
11
12
 
12
13
 
@@ -103,7 +104,7 @@ class ThematicMap(Map):
103
104
  if not self._is_categorical:
104
105
  self._choose_cmap()
105
106
 
106
- self._add_legend()
107
+ self._create_legend()
107
108
 
108
109
  def change_cmap(self, cmap: str, start: int = 0, stop: int = 256):
109
110
  """Change the color palette of the plot.
@@ -139,12 +140,90 @@ class ThematicMap(Map):
139
140
  self.diffy = self.maxy - self.miny
140
141
  return self
141
142
 
142
- def plot(self) -> None:
143
+ def plot(self, **kwargs) -> None:
143
144
  """Creates the final plot.
144
145
 
145
146
  This method should be run after customising the map, but before saving.
146
147
  """
147
148
 
149
+ __test = kwargs.pop("__test", False)
150
+ include_legend = bool(kwargs.pop("legend", self.legend))
151
+
152
+ if "color" in kwargs:
153
+ kwargs.pop("column", None)
154
+ self.legend = None
155
+ include_legend = False
156
+ elif hasattr(self, "color"):
157
+ kwargs.pop("column", None)
158
+ kwargs["color"] = self.color
159
+ self.legend = None
160
+ include_legend = False
161
+
162
+ elif self._is_categorical:
163
+ kwargs = self._prepare_categorical_plot(kwargs)
164
+ self.legend._prepare_categorical_legend(
165
+ categories_colors=self._categories_colors_dict,
166
+ nan_label=self.nan_label,
167
+ )
168
+
169
+ else:
170
+ kwargs = self._prepare_continous_plot(kwargs)
171
+ if self.legend:
172
+ if not self.legend._rounding_has_been_set:
173
+ self.legend._rounding = self.legend._get_rounding(
174
+ array=self._gdf.loc[~self._nan_idx, self._column]
175
+ )
176
+
177
+ self.legend._prepare_continous_legend(
178
+ bins=self.bins,
179
+ colors=self._unique_colors,
180
+ nan_label=self.nan_label,
181
+ bin_values=self._bins_unique_values,
182
+ )
183
+
184
+ if self.legend and not self.legend._position_has_been_set:
185
+ self.legend._position = self.legend._get_best_legend_position(
186
+ self._gdf, k=self._k + bool(len(self._nan_idx))
187
+ )
188
+
189
+ if __test:
190
+ return
191
+
192
+ self._prepare_plot(**kwargs)
193
+
194
+ if self.legend:
195
+ self.ax = self.legend._actually_add_legend(ax=self.ax)
196
+
197
+ # if self.legend:
198
+ # self._actually_add_legend()
199
+
200
+ self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
201
+
202
+ def save(self, path: str) -> None:
203
+ """Save figure as image file.
204
+
205
+ To be run after the plot method.
206
+
207
+ Args:
208
+ path: File path.
209
+ """
210
+ try:
211
+ plt.savefig(path)
212
+ except FileNotFoundError:
213
+ from dapla import FileClient
214
+
215
+ fs = FileClient.get_gcs_file_system()
216
+ with fs.open(path, "wb") as file:
217
+ plt.savefig(file)
218
+
219
+ def _prepare_plot(self, **kwargs):
220
+ """Add figure and axis, title and background gdf."""
221
+ for attr in self.__dict__.keys():
222
+ if attr in self.kwargs:
223
+ self[attr] = self.kwargs.pop(attr)
224
+ if attr in kwargs:
225
+ self[attr] = kwargs.pop(attr)
226
+
148
227
  self.fig, self.ax = self._get_matplotlib_figure_and_axix(
149
228
  figsize=(self._size, self._size)
150
229
  )
@@ -159,85 +238,82 @@ class ThematicMap(Map):
159
238
  self.title, fontsize=self.title_fontsize, color=self.title_color
160
239
  )
161
240
 
162
- if not self._is_categorical:
163
- self._prepare_continous_map()
164
- if self.scheme:
165
- self.colorlist = self._get_continous_colors()
166
- self.colors = self._classify_from_bins(self._gdf)
241
+ def _prepare_continous_plot(self, kwargs) -> dict:
242
+ """Create bins and colors."""
243
+ self._prepare_continous_map()
244
+
245
+ if self.scheme is None:
246
+ self.legend = None
247
+ kwargs["column"] = self.column
248
+ return kwargs
249
+
250
+ elif self.bins is None:
251
+ kwargs["column"] = self.column
252
+ return kwargs
253
+
167
254
  else:
168
- self._get_categorical_colors()
169
- self.colors = self._gdf["color"]
255
+ classified = self._classify_from_bins(self._gdf, bins=self.bins)
256
+ classified_sequential = self._push_classification(classified)
257
+ n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
258
+ self._unique_colors = self._get_continous_colors(n=n_colors)
259
+ self._bins_unique_values = self._make_bin_value_dict(
260
+ self._gdf, classified_sequential
261
+ )
262
+ colorarray = self._unique_colors[classified_sequential]
263
+ kwargs["color"] = colorarray
170
264
 
171
- if self.legend:
172
- if not self.legend._position_has_been_set:
173
- self.legend._position = self.legend._get_best_legend_position(
174
- self._gdf, k=self._k + bool(len(self._nan_idx))
175
- )
265
+ if self.legend and not self.legend._rounding_has_been_set:
266
+ self.bins = self.legend._set_rounding(
267
+ bins=self.bins, rounding=self.legend._rounding
268
+ )
176
269
 
177
- if not self._is_categorical and not self.legend._rounding_has_been_set:
178
- self.legend._rounding = self.legend._get_rounding(
179
- array=self._gdf.loc[~self._nan_idx, self._column]
180
- )
270
+ if any(self._nan_idx):
271
+ self.bins = self.bins + [self.nan_label]
181
272
 
182
- if not self.legend:
183
- self._include_legend = False
184
- elif self._is_categorical:
273
+ return kwargs
274
+
275
+ def _prepare_categorical_plot(self, kwargs) -> dict:
276
+ """Map values to colors."""
277
+ self._get_categorical_colors()
278
+ colorarray = self._gdf["color"]
279
+
280
+ kwargs["color"] = colorarray
281
+ return kwargs
282
+
283
+ def _actually_add_legend(self) -> None:
284
+ """Add legend to the axis and fill it with colors and labels."""
285
+ if not self.legend._position_has_been_set:
286
+ self.legend._position = self.legend._get_best_legend_position(
287
+ self._gdf, k=self._k + bool(len(self._nan_idx))
288
+ )
289
+
290
+ if self._is_categorical:
185
291
  self.ax = self.legend._actually_add_categorical_legend(
186
292
  ax=self.ax,
187
293
  categories_colors=self._categories_colors_dict,
188
294
  nan_label=self.nan_label,
189
295
  )
190
- self._include_legend = True
191
- elif self.scheme is None:
192
- self._include_legend = True
193
296
  else:
194
- self._include_legend = True
195
- if not self.legend._rounding_has_been_set:
196
- self.bins = self.legend._set_rounding(
197
- bins=self.bins, rounding=self.legend._rounding
198
- )
199
-
200
- if any(self._nan_idx):
201
- self.bins = self.bins + [self.nan_label]
202
-
203
297
  self.ax = self.legend._actually_add_continous_legend(
204
298
  ax=self.ax,
205
299
  bins=self.bins,
206
- colors=self.colorlist,
300
+ colors=self._unique_colors,
207
301
  nan_label=self.nan_label,
208
302
  bin_values=self._bins_unique_values,
209
303
  )
210
304
 
211
- if self.bins or self._is_categorical:
212
- self._gdf.plot(color=self.colors, legend=self._include_legend, ax=self.ax)
213
- else:
214
- self._gdf.plot(column=self.column, legend=self._include_legend, ax=self.ax)
215
-
216
- def save(self, path: str) -> None:
217
- """Save figure as image file.
218
-
219
- To be run after the plot method.
220
-
221
- Args:
222
- path: File path.
223
- """
224
- try:
225
- plt.savefig(path)
226
- except FileNotFoundError:
227
- from dapla import FileClient
228
-
229
- fs = FileClient.get_gcs_file_system()
230
- with fs.open(path, "wb") as file:
231
- plt.savefig(file)
232
-
233
- def _add_legend(self):
305
+ def _create_legend(self):
306
+ """Instantiate the Legend class."""
234
307
  kwargs = {}
235
308
  if self._black:
236
309
  kwargs["facecolor"] = "#0f0f0f"
237
310
  kwargs["labelcolor"] = "#fefefe"
238
311
  kwargs["title_color"] = "#fefefe"
239
312
 
240
- self.legend = Legend(title=self._column, size=self._size, **kwargs)
313
+ if self._is_categorical:
314
+ self.legend = Legend(title=self._column, size=self._size, **kwargs)
315
+ else:
316
+ self.legend = ContinousLegend(title=self._column, size=self._size, **kwargs)
241
317
 
242
318
  def _choose_cmap(self):
243
319
  """kwargs is to catch start and stop points for the cmap in __init__."""
@@ -250,6 +326,14 @@ class ThematicMap(Map):
250
326
  self.cmap_start = 23
251
327
  self.cmap_stop = 256
252
328
 
329
+ def _make_bin_value_dict(self, gdf, classified) -> dict:
330
+ """Dict with unique values of all bins. Used in labels in ContinousLegend."""
331
+ bins_unique_values = {
332
+ i: list(set(gdf.loc[classified == i, self._column]))
333
+ for i, _ in enumerate(np.unique(classified))
334
+ }
335
+ return bins_unique_values
336
+
253
337
  def _actually_add_background(self):
254
338
  self.ax.set_xlim([self.minx - self.diffx * 0.03, self.maxx + self.diffx * 0.03])
255
339
  self.ax.set_ylim([self.miny - self.diffy * 0.03, self.maxy + self.diffy * 0.03])
@@ -271,7 +355,6 @@ class ThematicMap(Map):
271
355
  self.nan_color = "#666666"
272
356
  if not self._is_categorical:
273
357
  self.change_cmap("viridis")
274
- self._add_legend()
275
358
 
276
359
  else:
277
360
  self.facecolor, self.title_color, self.bg_gdf_color = (
@@ -282,7 +365,8 @@ class ThematicMap(Map):
282
365
  self.nan_color = "#c2c2c2"
283
366
  if not self._is_categorical:
284
367
  self.change_cmap("RdPu", start=23)
285
- self._add_legend()
368
+
369
+ self._create_legend()
286
370
 
287
371
  def __getitem__(self, item):
288
372
  return getattr(self, item)