소스 검색

Improved collections

tags/1.0.1
Rocketsoup 4 년 전
부모
커밋
a1cf5b13e2
2개의 변경된 파일514개의 추가작업 그리고 82개의 파일을 삭제
  1. 3
    3
      cogs/crosspostcog.py
  2. 511
    79
      rscollections.py

+ 3
- 3
cogs/crosspostcog.py 파일 보기

@@ -1,4 +1,4 @@
1
-from rscollections import BoundList
1
+from rscollections import SizeBoundList
2 2
 from discord import Guild, Message
3 3
 from cogs.basecog import BaseCog
4 4
 
@@ -21,7 +21,7 @@ class CrossPostCog(BaseCog):
21 21
 
22 22
 	def __record_message(self, message: Message) -> None:
23 23
 		recent_messages = Storage.get_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES) \
24
-			or BoundList(self.max_recent_messages, lambda index, message : message.created_at)
24
+			or SizeBoundList(self.max_recent_messages, lambda index, message : message.created_at)
25 25
 		recent_messages.append(message)
26 26
 		Storage.set_state_value(message.guild, self.STATE_KEY_RECENT_MESSAGES, recent_messages)
27 27
 
@@ -53,7 +53,7 @@ class CrossPostCog(BaseCog):
53 53
 			member: Member,
54 54
 			create_if_missing: bool = False) -> SpamContext:
55 55
 		spam_lookup = Storage.get_state_value(member.guild, self.STATE_KEY_SPAM_CONTEXT) \
56
-			or BoundDict(self.max_spam_contexts, lambda key, context : context.age)
56
+			or SizeBoundDict(self.max_spam_contexts, lambda key, context : context.age)
57 57
 		context = spam_lookup.get(member.id)
58 58
 		if context:
59 59
 			return context

+ 511
- 79
rscollections.py 파일 보기

@@ -1,138 +1,570 @@
1
+from abc import ABC, abstractmethod
2
+
1 3
 """
2
-Subclasses of list, set, and dict with maximum sizes.
4
+Subclasses of list, set, and dict with special behaviors.
3 5
 """
4 6
 
5
-class BoundList(list):
7
+# Abstract collections
8
+
9
+class AbstractMutableList(list, ABC):
10
+	"""
11
+	Abstract list with hooks for custom logic before and after mutation
12
+	operations.
13
+	"""
14
+	@abstractmethod
15
+	def pre_mutate(self):
16
+		"""
17
+		Called just prior to any mutation operation.
18
+		"""
19
+		raise NotImplementedError('subclass must implement pre_mutate')
20
+	@abstractmethod
21
+	def post_mutate(self):
22
+		"""
23
+		Called just after any mutating operation before returning the result
24
+		to the caller.
25
+		"""
26
+		raise NotImplementedError('subclass must implement post_mutate')
27
+	# Mutating method hooks
28
+	def __delitem__(self, *args):
29
+		self.pre_mutate()
30
+		ret_val = super().__delitem__(*args)
31
+		self.post_mutate()
32
+		return ret_val
33
+	def __iadd__(self, *args):
34
+		self.pre_mutate()
35
+		ret_val = super().__iadd__(*args)
36
+		self.post_mutate()
37
+		return ret_val
38
+	def __imul__(self, *args):
39
+		self.pre_mutate()
40
+		ret_val = super().__imul__(*args)
41
+		self.post_mutate()
42
+		return ret_val
43
+	def __setitem__(self, *args):
44
+		self.pre_mutate()
45
+		ret_val = super().__setitem__(*args)
46
+		self.post_mutate()
47
+		return ret_val
48
+	def append(self, *args):
49
+		self.pre_mutate()
50
+		ret_val = super().append(*args)
51
+		self.post_mutate()
52
+		return ret_val
53
+	def clear(self, *args):
54
+		self.pre_mutate()
55
+		ret_val = super().clear(*args)
56
+		self.post_mutate()
57
+		return ret_val
58
+	def extend(self, *args):
59
+		self.pre_mutate()
60
+		ret_val = super().extend(*args)
61
+		self.post_mutate()
62
+		return ret_val
63
+	def insert(self, *args):
64
+		self.pre_mutate()
65
+		ret_val = super().insert(*args)
66
+		self.post_mutate()
67
+		return ret_val
68
+	def pop(self, *args):
69
+		self.pre_mutate()
70
+		ret_val = super().pop(*args)
71
+		self.post_mutate()
72
+		return ret_val
73
+	def remove(self, *args):
74
+		self.pre_mutate()
75
+		ret_val = super().remove(*args)
76
+		self.post_mutate()
77
+		return ret_val
78
+	def reverse(self, *args):
79
+		self.pre_mutate()
80
+		ret_val = super().reverse(*args)
81
+		self.post_mutate()
82
+		return ret_val
83
+	def sort(self, *args):
84
+		self.pre_mutate()
85
+		ret_val = super().sort(*args)
86
+		self.post_mutate()
87
+		return ret_val
88
+
89
+class AbstractMutableSet(set, ABC):
90
+	"""
91
+	Abstract set with hooks for custom logic before and after mutation
92
+	operations.
93
+	"""
94
+	@abstractmethod
95
+	def pre_mutate(self):
96
+		"""
97
+		Called just prior to any mutation operation.
98
+		"""
99
+		raise NotImplementedError('subclass must implement pre_mutate')
100
+	@abstractmethod
101
+	def post_mutate(self):
102
+		"""
103
+		Called just after any mutating operation before returning the result
104
+		to the caller.
105
+		"""
106
+		raise NotImplementedError('subclass must implement post_mutate')
107
+	# Mutating method hooks
108
+	def __iand__(self, *args):
109
+		self.pre_mutate()
110
+		ret_val = super().__iand__(*args)
111
+		self.post_mutate()
112
+		return ret_val
113
+	def __ior__(self, *args):
114
+		self.pre_mutate()
115
+		ret_val = super().__ior__(*args)
116
+		self.post_mutate()
117
+		return ret_val
118
+	def __isub__(self, *args):
119
+		self.pre_mutate()
120
+		ret_val = super().__isub__(*args)
121
+		self.post_mutate()
122
+		return ret_val
123
+	def __ixor__(self, *args):
124
+		self.pre_mutate()
125
+		ret_val = super().__ixor__(*args)
126
+		self.post_mutate()
127
+		return ret_val
128
+	def add(self, *args):
129
+		self.pre_mutate()
130
+		ret_val = super().add(*args)
131
+		self.post_mutate()
132
+		return ret_val
133
+	def clear(self, *args):
134
+		self.pre_mutate()
135
+		ret_val = super().clear(*args)
136
+		self.post_mutate()
137
+		return ret_val
138
+	def difference_update(self, *args):
139
+		self.pre_mutate()
140
+		ret_val = super().difference_update(*args)
141
+		self.post_mutate()
142
+		return ret_val
143
+	def discard(self, *args):
144
+		self.pre_mutate()
145
+		ret_val = super().discard(*args)
146
+		self.post_mutate()
147
+		return ret_val
148
+	def intersection_update(self, *args):
149
+		self.pre_mutate()
150
+		ret_val = super().intersection_update(*args)
151
+		self.post_mutate()
152
+		return ret_val
153
+	def pop(self, *args):
154
+		self.pre_mutate()
155
+		ret_val = super().pop(*args)
156
+		self.post_mutate()
157
+		return ret_val
158
+	def remove(self, *args):
159
+		self.pre_mutate()
160
+		ret_val = super().remove(*args)
161
+		self.post_mutate()
162
+		return ret_val
163
+	def symmetric_difference_update(self, *args):
164
+		self.pre_mutate()
165
+		ret_val = super().symmetric_difference_update(*args)
166
+		self.post_mutate()
167
+		return ret_val
168
+	def update(self, *args):
169
+		self.pre_mutate()
170
+		ret_val = super().update(*args)
171
+		self.post_mutate()
172
+		return ret_val
173
+
174
+class AbstractMutableDict(dict, ABC):
175
+	"""
176
+	Abstract dict with hooks for custom logic before and after mutation
177
+	operations.
178
+	"""
179
+	@abstractmethod
180
+	def pre_mutate(self):
181
+		"""
182
+		Called just prior to any mutation operation.
183
+		"""
184
+		raise NotImplementedError('subclass must implement pre_mutate')
185
+	@abstractmethod
186
+	def post_mutate(self):
187
+		"""
188
+		Called just after any mutating operation before returning the result
189
+		to the caller.
190
+		"""
191
+		raise NotImplementedError('subclass must implement post_mutate')
192
+	# Mutating method hooks
193
+	def __delitem__(self, *args):
194
+		self.pre_mutate()
195
+		ret_val = super().__delitem__(*args)
196
+		self.post_mutate()
197
+		return ret_val
198
+	def __ior__(self, *args):
199
+		self.pre_mutate()
200
+		ret_val = super().__ior__(*args)
201
+		self.post_mutate()
202
+		return ret_val
203
+	def __setitem__(self, *args):
204
+		self.pre_mutate()
205
+		ret_val = super().__setitem__(*args)
206
+		self.post_mutate()
207
+		return ret_val
208
+	def clear(self, *args):
209
+		self.pre_mutate()
210
+		ret_val = super().clear(*args)
211
+		self.post_mutate()
212
+		return ret_val
213
+	def pop(self, *args):
214
+		self.pre_mutate()
215
+		ret_val = super().pop(*args)
216
+		self.post_mutate()
217
+		return ret_val
218
+	def popitem(self, *args):
219
+		self.pre_mutate()
220
+		ret_val = super().popitem(*args)
221
+		self.post_mutate()
222
+		return ret_val
223
+	def update(self, *args):
224
+		self.pre_mutate()
225
+		ret_val = super().update(*args)
226
+		self.post_mutate()
227
+		return ret_val
228
+
229
+# Collections with limited number of elements
230
+
231
+class SizeBoundList(AbstractMutableList):
6 232
 	"""
7
-	Subclass of `list` that enforces a maximum size.
233
+	Subclass of `list` that enforces a maximum number of elements.
8 234
 
9 235
 	If the number of elements following a mutating operation exceeds
10 236
 	`self.max_element_count`, then each element will be tested for its "age,"
11 237
 	and the "oldest" elements will be removed until the total size is back
12 238
 	within the limit. "Age" is determined via a provided lambda function. It is
13 239
 	a value with arbitrary numeric value that is used comparitively to find
14
-	the element with the smallest value. If no `age_test` is provided, the
15
-	default is to remove elem 0.
240
+	the element with the smallest value.
16 241
 
17
-	The age lambda takes two arguments: the element index and the element value.
18
-	It must return an int or float.
242
+	The `element_age` lambda takes two arguments: the element index and the
243
+	element value. It must return values that can be compared to one another
244
+	(e.g. int, float, datetime).
19 245
 
20
-	`self.age_test` and `self.max_element_count` can be modified at runtime,
21
-	however elements will only be discarded following the next mutating operation.
246
+	`self.element_age` and `self.max_element_count` can be modified at runtime,
247
+	however elements will only be discarded following the next mutating
248
+	operation. Call `self.purge_old_elements()` to force resizing.
22 249
 	"""
23
-	def __init__(self, max_element_count = 10, age_test = (lambda index, elem: index), *args, **kwargs):
250
+	def __init__(self, max_element_count: int, element_age, *args, **kwargs):
24 251
 		super().__init__(*args, **kwargs)
25
-		self.age_test = age_test
252
+		self.element_age = element_age
26 253
 		self.max_element_count = max_element_count
254
+		self.is_culling = False
255
+		self.post_mutate()
27 256
 
28
-	def __cull_old_elements(self):
257
+	def purge_old_elements(self):
258
+		"""
259
+		Manually purges elements. Purging usually occurs automatically, but
260
+		this can be called if `self.max_element_count` or `self.element_age`
261
+		are modified at runtime.
262
+		"""
263
+		self.post_mutate()
264
+
265
+	def pre_mutate(self):
266
+		pass
267
+
268
+	def post_mutate(self):
269
+		if self.is_culling:
270
+			return
271
+		self.is_culling = True
29 272
 		while len(self) > self.max_element_count:
30 273
 			oldest_age = None
31 274
 			oldest_index = -1
32 275
 			for i in range(len(self)):
33 276
 				elem = self[i]
34
-				age = self.age_test(i, elem)
277
+				age = self.element_age(i, elem)
35 278
 				if oldest_age is None or age < oldest_age:
36 279
 					oldest_age = age
37 280
 					oldest_index = i
38 281
 			self.pop(oldest_index)
282
+		self.is_culling = False
39 283
 
40
-	# Passthrough methods
41
-
42
-	def __add__(self, *args):
43
-		ret_val = super().__add__(*args)
44
-		self.__cull_old_elements()
45
-		return ret_val
46
-	def __iadd__(self, *args):
47
-		ret_val = super().__iadd__(*args)
48
-		self.__cull_old_elements()
49
-		return ret_val
50
-	def append(self, *args):
51
-		ret_val = super().append(*args)
52
-		self.__cull_old_elements()
53
-		return ret_val
54
-	def copy(self):
55
-		return BoundList(self.max_element_count, self.age_test, super())
56
-	def extend(self, *args):
57
-		ret_val = super().extend(*args)
58
-		self.__cull_old_elements()
59
-		return ret_val
60
-	def insert(self, *args):
61
-		ret_val = super().insert(*args)
62
-		self.__cull_old_elements()
63
-		return ret_val
284
+	def copy():
285
+		return SizeBoundList(max_element_count, element_age, super())
64 286
 
65
-class BoundSet(set):
287
+class SizeBoundSet(AbstractMutableSet):
66 288
 	"""
67
-	Subclass of `set` that enforces a maximum size.
289
+	Subclass of `set` that enforces a maximum number of elements.
290
+
291
+	If the number of elements following a mutating operation exceeds
292
+	`self.max_element_count`, then each element will be tested for its "age,"
293
+	and the "oldest" elements will be removed until the total size is back
294
+	within the limit. "Age" is determined via a provided lambda function. It is
295
+	a value with arbitrary numeric value that is used comparitively to find
296
+	the element with the smallest value.
68 297
 
69
-	Same behavior as `BoundList`.
298
+	The `element_age` lambda takes one argument: the element value. It must
299
+	return values that can be compared to one another (e.g. int, float,
300
+	datetime).
70 301
 
71
-	`age_test` lambda function takes one argument, the element value.
302
+	`self.element_age` and `self.max_element_count` can be modified at runtime,
303
+	however elements will only be discarded following the next mutating
304
+	operation. Call `self.purge_old_elements()` to force resizing.
72 305
 	"""
73
-	def __init__(self, max_element_count = 10, age_test = (lambda elem: float(elem)), *args, **kwargs):
306
+	def __init__(self, max_element_count: int, element_age, *args, **kwargs):
74 307
 		super().__init__(*args, **kwargs)
75
-		self.age_test = age_test
308
+		self.element_age = element_age
76 309
 		self.max_element_count = max_element_count
310
+		self.is_culling = False
311
+		self.post_mutate()
312
+
313
+	def purge_old_elements(self):
314
+		"""
315
+		Manually purges elements. Purging usually occurs automatically, but
316
+		this can be called if `self.max_element_count` or `self.element_age`
317
+		are modified at runtime.
318
+		"""
319
+		self.post_mutate()
77 320
 
78
-	def __cull_old_elements(self):
321
+	def pre_mutate(self):
322
+		pass
323
+	def post_mutate(self):
324
+		if self.is_culling:
325
+			return
326
+		self.is_culling = True
79 327
 		while len(self) > self.max_element_count:
80 328
 			oldest_age = None
81 329
 			oldest_elem = None
82 330
 			for elem in self:
83
-				age = self.age_test(elem)
331
+				age = self.element_age(elem)
84 332
 				if oldest_age is None or age < oldest_age:
85 333
 					oldest_age = age
86 334
 					oldest_elem = elem
87 335
 			self.remove(oldest_elem)
336
+		self.is_culling = False
88 337
 
89
-	def add(self, *args):
90
-		ret_val = super().add(*args)
91
-		self.__cull_old_elements()
92
-		return ret_val
93
-	def copy(self):
94
-		return BoundSet(self.max_element_count, self.age_test, super())
95
-	def update(self, *args):
96
-		ret_val = super().update(*args)
97
-		self.__cull_old_elements()
98
-		return ret_val
338
+	def copy():
339
+		return SizeBoundSet(max_element_count, element_age, super())
99 340
 
100
-class BoundDict(dict):
341
+class SizeBoundDict(AbstractMutableDict):
101 342
 	"""
102
-	Subclass of `dict` that enforces a maximum size.
343
+	Subclass of `dict` that enforces a maximum number of elements.
103 344
 
104
-	Same behavior as `BoundList`.
345
+	If the number of elements following a mutating operation exceeds
346
+	`self.max_element_count`, then each element will be tested for its "age,"
347
+	and the "oldest" elements will be removed until the total size is back
348
+	within the limit. "Age" is determined via a provided lambda function. It is
349
+	a value with arbitrary numeric value that is used comparitively to find
350
+	the element with the smallest value.
105 351
 
106
-	`age_test` lambda takes two arguments: the key and the value of a pair.
352
+	The `element_age` lambda takes two arguments: the key and the value of a
353
+	dict pair. It must return values that can be compared to one another (e.g.
354
+	int, float, datetime).
355
+
356
+	`self.element_age` and `self.max_element_count` can be modified at runtime,
357
+	however elements will only be discarded following the next mutating
358
+	operation. Call `self.purge_old_elements()` to force resizing.
107 359
 	"""
108
-	def __init__(self, max_element_count = 10, age_test = (lambda key, value: float(key)), *args, **kwargs):
360
+	def __init__(self, max_element_count = 10, element_age = (lambda key, value: float(key)), *args, **kwargs):
109 361
 		super().__init__(*args, **kwargs)
110
-		self.age_test = age_test
362
+		self.element_age = element_age
111 363
 		self.max_element_count = max_element_count
364
+		self.is_culling = False
365
+		self.post_mutate()
366
+
367
+	def purge_old_elements(self):
368
+		"""
369
+		Manually purges elements. Purging usually occurs automatically, but
370
+		this can be called if `self.max_element_count` or `self.element_age`
371
+		are modified at runtime.
372
+		"""
373
+		self.post_mutate()
112 374
 
113
-	def __cull_old_elements(self):
375
+	def pre_mutate(self):
376
+		pass
377
+	def post_mutate(self):
378
+		if self.is_culling:
379
+			return
380
+		self.is_culling = True
114 381
 		while len(self) > self.max_element_count:
115 382
 			oldest_age = None
116 383
 			oldest_key = None
117 384
 			for key, value in self.items():
118
-				age = self.age_test(key, value)
385
+				age = self.element_age(key, value)
119 386
 				if oldest_age is None or age < oldest_age:
120 387
 					oldest_age = age
121 388
 					oldest_key = key
122 389
 			del self[oldest_key]
123
-	def __ior__(self, *args):
124
-		ret_val = super().__ior__(*args)
125
-		self.__cull_old_elements()
126
-		return ret_val
127
-	def __setitem__(self, *args):
128
-		super().__setitem__(*args)
129
-		self.__cull_old_elements()
130
-	def copy(self):
131
-		return BoundDict(self.max_element_count, self.age_test, super())
132
-	@classmethod
133
-	def fromkeys(cls, iter, value=None):
134
-		raise RuntimeError('fromkeys not available')
135
-	def update(self, *args):
136
-		ret_val = super().update(*args)
137
-		self.__cull_old_elements()
138
-		return ret_val
390
+		self.is_culling = False
391
+
392
+	def copy():
393
+		return SizeBoundDict(max_element_count, element_age, super())
394
+
395
+# Collections with limited age of elements
396
+
397
+class AgeBoundList(AbstractMutableList):
398
+	"""
399
+	Subclass of `list` that enforces a maximum "age" of elements.
400
+
401
+	After each mutating operation, the minimum and maximum "age" of the elements
402
+	are determined. If the span between the newest and oldest exceeds
403
+	`self.max_age` then then the oldest elements will be purged until that is
404
+	no longer the case. "Age" is determined via a provided lambda function. It
405
+	is a value with arbitrary numeric value that is used comparitively.
406
+
407
+	The `element_age` lambda takes two arguments: the element index and the
408
+	element value. It must return values that can be compared to one another
409
+	and be added and subtracted (e.g. int, float, datetime). If the lambda
410
+	returns `datetime`s, the `max_age` should be a `timespan`.
411
+
412
+	`self.element_age` and `self.max_age` can be modified at runtime,
413
+	however elements will only be discarded following the next mutating
414
+	operation. Call `self.purge_old_elements()` to force resizing.
415
+	"""
416
+	def __init__(self, max_age, element_age, *args, **kwargs):
417
+		super().__init__(*args, **kwargs)
418
+		self.max_age = max_age
419
+		self.element_age = element_age
420
+		self.is_culling = False
421
+
422
+	def purge_old_elements(self):
423
+		"""
424
+		Manually purges elements. Purging usually occurs automatically, but
425
+		this can be called if `self.max_age` or `self.element_age` are modified
426
+		at runtime.
427
+		"""
428
+		self.post_mutate()
429
+
430
+	def pre_mutate(self):
431
+		pass
432
+	def post_mutate(self):
433
+		if self.is_culling or len(self) <= 1:
434
+			return
435
+		self.is_culling = True
436
+		min_age = None
437
+		max_age = None
438
+		ages = {}
439
+		for i in range(len(self)):
440
+			elem = self[i]
441
+			age = self.element_age(i, elem)
442
+			ages[i] = age
443
+			if min_age is None or age < min_age:
444
+				min_age = age
445
+			if max_age is None or age > max_age:
446
+				max_age = age
447
+		cutoff = max_age - self.max_age
448
+		if min_age >= cutoff:
449
+			self.is_culling = False
450
+			return
451
+		for i in reversed(range(len(self))):
452
+			if ages[i] < cutoff:
453
+				del self[i]
454
+		self.is_culling = False
455
+
456
+class AgeBoundSet(AbstractMutableSet):
457
+	"""
458
+	Subclass of `set` that enforces a maximum "age" of elements.
459
+
460
+	After each mutating operation, the minimum and maximum "age" of the elements
461
+	are determined. If the span between the newest and oldest exceeds
462
+	`self.max_age` then then the oldest elements will be purged until that is
463
+	no longer the case. "Age" is determined via a provided lambda function. It
464
+	is a value with arbitrary numeric value that is used comparitively.
465
+
466
+	The `element_age` lambda takes one argument: the element value. It must
467
+	return values that can be compared to one another and be added and
468
+	subtracted (e.g. int, float, datetime). If the lambda returns `datetime`s,
469
+	the `max_age` should be a `timespan`.
470
+
471
+	`self.element_age` and `self.max_age` can be modified at runtime,
472
+	however elements will only be discarded following the next mutating
473
+	operation. Call `self.purge_old_elements()` to force resizing.
474
+	"""
475
+	def __init__(self, max_age, element_age, *args, **kwargs):
476
+		super().__init__(*args, **kwargs)
477
+		self.max_age = max_age
478
+		self.element_age = element_age
479
+		self.is_culling = False
480
+
481
+	def purge_old_elements(self):
482
+		"""
483
+		Manually purges elements. Purging usually occurs automatically, but
484
+		this can be called if `self.max_age` or `self.element_age` are modified
485
+		at runtime.
486
+		"""
487
+		self.post_mutate()
488
+
489
+	def pre_mutate(self):
490
+		pass
491
+	def post_mutate(self):
492
+		if self.is_culling or len(self) <= 1:
493
+			return
494
+		self.is_culling = True
495
+		min_age = None
496
+		max_age = None
497
+		ages = {}
498
+		for elem in self:
499
+			age = self.element_age(elem)
500
+			ages[elem] = age
501
+			if min_age is None or age < min_age:
502
+				min_age = age
503
+			if max_age is None or age > max_age:
504
+				max_age = age
505
+		cutoff = max_age - self.max_age
506
+		if min_age >= cutoff:
507
+			self.is_culling = False
508
+			return
509
+		for elem, age in ages.items():
510
+			if age < cutoff:
511
+				self.remove(elem)
512
+		self.is_culling = False
513
+
514
+class AgeBoundDict(AbstractMutableDict):
515
+	"""
516
+	Subclass of `dict` that enforces a maximum "age" of elements.
517
+
518
+	After each mutating operation, the minimum and maximum "age" of the elements
519
+	are determined. If the span between the newest and oldest exceeds
520
+	`self.max_age` then then the oldest elements will be purged until that is
521
+	no longer the case. "Age" is determined via a provided lambda function. It
522
+	is a value with arbitrary numeric value that is used comparitively.
523
+
524
+	The `element_age` lambda takes two arguments: the key and value of a pair.
525
+	It must return values that can be compared to one another and be added and
526
+	subtracted (e.g. int, float, datetime). If the lambda returns `datetime`s,
527
+	the `max_age` should be a `timespan`.
528
+
529
+	`self.element_age` and `self.max_age` can be modified at runtime,
530
+	however elements will only be discarded following the next mutating
531
+	operation. Call `self.purge_old_elements()` to force resizing.
532
+	"""
533
+	def __init__(self, max_age, element_age, *args, **kwargs):
534
+		super().__init__(*args, **kwargs)
535
+		self.max_age = max_age
536
+		self.element_age = element_age
537
+		self.is_culling = False
538
+
539
+	def purge_old_elements(self):
540
+		"""
541
+		Manually purges elements. Purging usually occurs automatically, but
542
+		this can be called if `self.max_age` or `self.element_age` are modified
543
+		at runtime.
544
+		"""
545
+		self.post_mutate()
546
+
547
+	def pre_mutate(self):
548
+		pass
549
+	def post_mutate(self):
550
+		if self.is_culling or len(self) <= 1:
551
+			return
552
+		self.is_culling = True
553
+		min_age = None
554
+		max_age = None
555
+		ages = {}
556
+		for key, value in self.items():
557
+			age = self.element_age(key, value)
558
+			ages[key] = age
559
+			if min_age is None or age < min_age:
560
+				min_age = age
561
+			if max_age is None or age > max_age:
562
+				max_age = age
563
+		cutoff = max_age - self.max_age
564
+		if min_age >= cutoff:
565
+			self.is_culling = False
566
+			return
567
+		for key, age in ages.items():
568
+			if age < cutoff:
569
+				del self[key]
570
+		self.is_culling = False

Loading…
취소
저장