Update dynamically dependent object attributes after mutation - python

Let's take as example this classical solution to the problem of updating dependent object attributes:
class SomeClass(object):
def __init__(self, n):
self.list = range(0, n)
#property
def list(self):
return self._list
#list.setter
def list(self, val):
self._list = val
self._listsquare = [x**2 for x in self._list ]
#property
def listsquare(self):
return self._listsquare
#listsquare.setter
def listsquare(self, val):
self.list = [int(pow(x, 0.5)) for x in val]
It works as required: when a new value is set for one attribute, the other attribute is updated:
>>> c = SomeClass(5)
>>> c.listsquare
[0, 1, 4, 9, 16]
>>> c.list
[0, 1, 2, 3, 4]
>>> c.list = range(0,6)
>>> c.list
[0, 1, 2, 3, 4, 5]
>>> c.listsquare
[0, 1, 4, 9, 16, 25]
>>> c.listsquare = [x**2 for x in range(0,10)]
>>> c.list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
But, what if we mutate the attribute list instead of setting it to a new value?:
>>> c.list[0] = 10
>>> c.list
[10, 1, 2, 3, 4, 5, 6, 7, 8, 9] # this is ok
>>> c.listsquare
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # we would like 100 as first element
We would like listsquare attribute to be updated accordingly, but it's not the case because the setters are not invoked when we mutate the list attribute.
Of course we could force the update by explicitly invoking the setter after we modify the attribute, for example by doing:
>>> c.list[0] = 10
>>> c.list = c.list. # invoke setter
>>> c.listsquare
[100, 1, 4, 9, 16, 25, 36, 49, 64, 81]
but it looks somewhat cumbersome and error prone for the user, we would prefer that it occurs implicitly.
What would be the most pythonic way for having the attributes updated when another mutable attribute is modified. How the object can know that one of his attributes has been modified?

So, as Davis Herring was saying in the comments, this is eminently possible but not nearly as clean. You essentially have to build your own custom data structure that maintains two lists in parallel, each one aware of the other, so that if one is updated, the other is also updated. Below is my shot at doing that, which took, um, a little longer than expected. Seems to work, but I haven't comprehensively tested it.
I've chosen to inherit from collections.UserList here. The other option would be to inherit from collections.abc.MutableSequence, which has various pros and cons compared to UserList.
from __future__ import annotations
from collections import UserList
from abc import abstractmethod
from typing import (
Sequence,
TypeVar,
Generic,
Optional,
Union,
Any,
Iterable,
overload,
cast
)
### ABSTRACT CLASSES ###
# Initial type
I = TypeVar('I')
# Transformed type
T = TypeVar('T')
# Return type for methods that return self
C = TypeVar('C', bound="AbstractListPairItem[Any, Any]")
class AbstractListPairItem(UserList[I], Generic[I, T]):
"""Base class for AbstractListPairParent and AbstractListPairChild"""
__slots__ = '_other_list'
_other_list: AbstractListPairItem[T, I]
# UserList inherits from `collections.abc.MutableSequence`,
# which has `abc.ABCMeta` as its metaclass,
# so the #abstractmethod decorator works fine.
#abstractmethod
def __init__(self, initlist: Optional[Iterable[I]] = None) -> None:
# We inherit from UserList, which stores the sequence as a `list`
# in a `data` instance attribute
super().__init__(initlist)
#staticmethod
#abstractmethod
def transform(value: I) -> T: ...
#overload
def __setitem__(self, index: int, value: I) -> None: ...
#overload
def __setitem__(self, index: slice, value: Iterable[I]) -> None: ...
def __setitem__(
self,
index: Union[int, slice],
value: Union[I, Iterable[I]]
) -> None:
super().__setitem__(index, value) # type: ignore[index, assignment]
if isinstance(index, int):
value = cast(I, value)
self._other_list.data[index] = self.transform(value)
elif isinstance(index, slice):
value = cast(Iterable[I], value)
for i, val in zip(range(index.start, index.stop, index.step), value):
self._other_list.data[i] = self.transform(val)
else:
raise NotImplementedError
# __getitem__ doesn't need to be altered
def __delitem__(self, index: Union[int, slice]) -> None:
super().__delitem__(index)
del self._other_list.data[index]
def __add__(self, other: Iterable[I]) -> list[I]: # type: ignore[override]
# Return a normal list rather than an instance of this class
return self.data + list(other)
def __radd__(self, other: Iterable[I]) -> list[I]:
# Return a normal list rather than an instance of this class
return list(other) + self.data
def __iadd__(self: C, other: Union[C, Iterable[I]]) -> C:
if isinstance(other, type(self)):
self.data += other.data
self._other_list.data += other._other_list.data
else:
new = list(other)
self.data += new
self._other_list.data += [self.transform(x) for x in new]
return self
def __mul__(self, n: int) -> list[I]: # type: ignore[override]
# Return a normal list rather than an instance of this class
return self.data * n
__rmul__ = __mul__
def __imul__(self: C, n: int) -> C:
self.data *= n
self._other_list.data *= n
return self
def append(self, item: I) -> None:
super().append(item)
self._other_list.data.append(self.transform(item))
def insert(self, i: int, item: I) -> None:
super().insert(i, item)
self._other_list.data.insert(i, self.transform(item))
def pop(self, i: int = -1) -> I:
del self._other_list.data[i]
return self.data.pop(i)
def remove(self, item: I) -> None:
i = self.data.index(item)
del self.data[i]
del self._other_list.data[i]
def clear(self) -> None:
super().clear()
self._other_list.data.clear()
def copy(self) -> list[I]: # type: ignore[override]
# Return a copy of the underlying data, NOT a new instance of this class
return self.data.copy()
def reverse(self) -> None:
super().reverse()
self._other_list.reverse()
def sort(self, /, *args: Any, **kwds: Any) -> None:
super().sort(*args, **kwds)
for i, elem in enumerate(self):
self._other_list.data[i] = self.transform(elem)
def extend(self: C, other: Union[C, Iterable[I]]) -> None:
self.__iadd__(other)
# Initial type for the parent, transformed type for the child.
X = TypeVar('X')
# Transformed type for the parent, initial type for the child.
Y = TypeVar('Y')
# Return type for methods returning self
P = TypeVar('P', bound='AbstractListPairParent[Any, Any]')
class AbstractListPairParent(AbstractListPairItem[X, Y]):
__slots__: Sequence[str] = tuple()
child_cls: type[AbstractListPairChild[Y, X]] = NotImplemented
def __new__(cls: type[P], initlist: Optional[Iterable[X]] = None) -> P:
if not hasattr(cls, 'child_cls'):
raise NotImplementedError(
"'ListPairParent' subclasses must have a 'child_cls' attribute"
)
return super().__new__(cls) # type: ignore[no-any-return]
def __init__(self, initlist: Optional[Iterable[X]] = None) -> None:
super().__init__(initlist)
self._other_list = self.child_cls(
self,
[self.transform(x) for x in self.data]
)
class AbstractListPairChild(AbstractListPairItem[Y, X]):
__slots__: Sequence[str] = tuple()
def __init__(
self,
parent: AbstractListPairParent[X, Y],
initlist: Optional[Iterable[Y]] = None
) -> None:
super().__init__(initlist)
self._other_list = parent
### CONCRETE IMPLEMENTATION ###
# Return type for methods returning self
L = TypeVar('L', bound='ListKeepingTrackOfSquares')
# We have to define the child before we define the parent,
# since the parent creates the child
class SquaresList(AbstractListPairChild[int, int]):
__slots__: Sequence[str] = tuple()
_other_list: ListKeepingTrackOfSquares
#staticmethod
def transform(value: int) -> int:
return int(pow(value, 0.5))
#property
def sqrt(self) -> ListKeepingTrackOfSquares:
return self._other_list
class ListKeepingTrackOfSquares(AbstractListPairParent[int, int]):
__slots__: Sequence[str] = tuple()
_other_list: SquaresList
child_cls = SquaresList
#classmethod
def from_squares(cls: type[L], child_list: Iterable[int]) -> L:
return cls([cls.child_cls.transform(x) for x in child_list])
#staticmethod
def transform(value: int) -> int:
return value ** 2
#property
def squared(self) -> SquaresList:
return self._other_list
class SomeClass:
def __init__(self, n: int) -> None:
self.list = range(0, n) # type: ignore[assignment]
#property
def list(self) -> ListKeepingTrackOfSquares:
return self._list
#list.setter
def list(self, val: Iterable[int]) -> None:
self._list = ListKeepingTrackOfSquares(val)
#property
def listsquare(self) -> SquaresList:
return self.list.squared
#listsquare.setter
def listsquare(self, val: Iterable[int]) -> None:
self.list = ListKeepingTrackOfSquares.from_squares(val)
s = SomeClass(10)

Related

Python type hints on mapping to mutable Set

I have written a mapping to a mutable Set in Python. I've used it as a datastructure dealing with file hashes and map the hashes to duplicate files.
I want to apply type hints and have read a lot about Generics and that stuff. Finally, I am not able to solve it well and I ask help from the community:
How does one type annotate the code below correctly?
I know, I could inherit from collections.abc thereby taking advantage of some type hints. However, in the mixin of Mapping and MutableSet I would end up in ducktyped methods such as __iter__ that are difficut to trace (at least for me). Therefore I chose to write all necessary methods by hand.
I want to supply all possible types, so kind of
K = typing.TypeVar('K', bound = typing.Hashable)
should be involved. This is necessary for keys in a dict and members of a set.
The code below just deals with Integers.
Thank you very much for your comments!
class MtoS():
def __init__(self, x = None):
self._hashlu= {}
if isinstance(x, list):
for t in x:
self.add(t)
elif x:
self.add(x)
def __ior__(self, other):
for i in other:
self.add(i)
return self
def __len__(self):
return sum([len(x) for x in self._hashlu.values()])
def add(self, item):
if item[0] not in self._hashlu:
self._hashlu[item[0]] = set()
self._hashlu[item[0]].add(item[1])
def __iter__(self):
for hash in self._hashlu.keys():
for file in sorted(self._hashlu[hash]):
yield hash, file
def __str__(self):
return(str(self._hashlu))
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self))
def __getitem__(self, key):
return self._hashlu[key]
if __name__ == "__main__":
m = MtoS([(1,2),(3,4)])
o = MtoS([(3,4),(3,5),(4,5),(4,6)])
print(m) # {1: {2}, 3: {4}}
print(m[1]) # {2}
m |= o
print(m) # {1: {2}, 3: {4, 5}, 4: {5, 6}}
print(len(m)) # 5
Generally, a data structure that can hold many types of items should use typing.Generic to let its users specify exactly which types it contains. If there are limits on the types that are valid (such as requiring them to be hashable), you can use bound types and typing.Protocols to specify the requirements.
So I'd annotate your code like this:
import typing
K = typing.TypeVar('K', bound=typing.Hashable) # typevar for keys
V = typing.TypeVar('V', bound="HashableAndSortable") # typevar for values
class HashableAndSortable(typing.Protocol, typing.Hashable):
def __lt__(self: V, other:V) -> bool: ...
KV = tuple[K, V] # alias for (key, value)
class MtoS(typing.Generic[K, V]):
_hashlu: dict[K, set[V]]
def __init__(self, x:typing.Optional[KV]|list[KV]=None) -> None: ...
def __ior__(self, other: "MtoS[K, V]") -> "MtoS[K, V]": ...
def __len__(self) -> int: ...
def add(self, item: KV) -> None: ...
def __iter__(self) -> typing.Iterator[KV]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __getitem__(self, key: K) -> set[V]: ...
The hints for __ior__ are pretty ugly because we need to use strings for forward references. A nicer approach will be possible in Python 3.11, which implements PEP 673, allowing typing.Self to be used to annotate a type that's the same as the type of the self argument.
Finally, thanks to Blckknght, I've come up with this specialized solution:
import typing as tp
import pathlib as pl
K = tp.TypeVar('K', bound = tp.Hashable)
V = tp.TypeVar('V', bound = pl.PurePath)
class MtoS(tp.Generic[K,V]):
_hashlu: dict[K, set[V]]
def __init__(self, x: tp.Union[None, tuple[K,V], list[tuple[K,V]]] = None) -> None:
self._hashlu= {}
if isinstance(x, list):
for t in x:
self.add(t)
elif x:
self.add(x)
def __ior__(self, other: "MtoS[K,V]") -> "MtoS[K,V]":
for i in other:
self.add(i)
return self
def __len__(self) -> int:
return sum([len(x) for x in self._hashlu.values()])
def add(self, item: tuple[K,V]) -> None:
if item[0] not in self._hashlu:
self._hashlu[item[0]] = set()
self._hashlu[item[0]].add(item[1])
def __iter__(self) -> tp.Iterator[tuple[K,V]]:
for hash in self._hashlu.keys():
for file in sorted(self._hashlu[hash]):
yield hash, file
def __str__(self) -> str:
return(str(self._hashlu))
def __repr__(self) -> str:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self))
def __getitem__(self, key: K) -> set[V]:
return self._hashlu[key]
if __name__ == "__main__":
m = MtoS([(1,pl.Path.home() / "dummy2"),(3,pl.Path.home() / "dummy4")])
o = MtoS([
(3,pl.Path.home() / "dummy4"),
(3,pl.Path.home() / "dummy5"),
(4,pl.Path.home() / "dummy5"),
(4,pl.Path.home() / "dummy6")])
print(m)
print(m[1])
m |= o
print(m)
print(len(m))

How do I use the python __get__ descriptor to output values of list elements?

How can I use the get descriptor to output values of list elements?
class X(object):
def __init__(self,value):
self.value = value
def __get__(self,obj,objtype):
return self.value
class Y(object):
a = X(1)
b = X(2)
c = [X(3),X(4)]
y = Y()
print(y.a)
print(y.b)
print(y.c[0])
Output:
1
2
<__main__.X object at ...>
Desired Output:
1
2
3
This snippet could bring you closer, but it's not the same. Z subclasses a list and defines __get__ for acting as a descriptor.
class X(object):
def __init__(self, value):
self.value = value
def __get__(self, obj, objtype):
return self.value
def __repr__(self):
return "X(%r)" % self.value
class Z(list):
def __get__(self, obj, objtype):
return self
def __getitem__(self, index):
"""override brackets operator, suggested by Azat Ibrakov"""
list_item = super(Z, self).__getitem__(index)
try:
return list_item.value
except AttributeError:
return list_item
class _LiteralForContainerDescriptorZ(object):
def __getitem__(self, keys):
"""override brackets operator, basing on https://stackoverflow.com/a/37259917/2823074"""
if not isinstance(keys, tuple):
keys = (keys,)
assert not any(isinstance(key, slice) for key in keys) # avoid e.g. ZL[11:value, key:23, key2:value2]
return Z(keys)
ZL = _LiteralForContainerDescriptorZ()
Using _LiteralForContainerDescriptorZ is optional, it gives a bit nicer syntax.
class Y(object):
a = X(1)
b = X(2)
c = Z([X(3.14), X(4)]) # define 'c' using constructor of Z class inherited from list
d = ZL[X(3.14), X(4)] # define 'd' using custom literal
y = Y()
for statement_to_print in [
"y.a", "y.b", "y.c","y.d", "y.c[0]", "y.c[1]", "y.d[0]",
]:
value = eval(statement_to_print)
print("{st:9} = {ev:<16} # type: {tp}".format(
st=statement_to_print, ev=value, tp=type(value).__name__))
Calling it, the prints are:
y.a = 1 # type: int
y.b = 2 # type: int
y.c = [X(3.14), X(4)] # type: Z
y.d = [X(3.14), X(4)] # type: Z
y.c[0] = 3.14 # type: float
y.c[1] = 4 # type: int
y.d[0] = 3.14 # type: float

Python: proper way to implement `l[s:e] += v`

I'm implementing for fun and profit a data-structure allowing fast additive range updates:
class RAUQ:
""" Allow 'l[s:e] += v' update and 'a[i]' query in O(log n)
>>> l = RAUQ([0, 10, 20]) ; l
[0, 10, 20]
>>> l[1]
10
>>> l[2] += 10 ; l
[0, 10, 30]
>>> l[0:2] += 3 ; l
[3, 13, 30]
>>> l[1:10] -= 4 ; l # Support usual out of bounds slices
[3, 9, 26]
"""
According to disassembled bytecode, the l[i] += v expression is translated to:
l.__setitem__(i, l.__getitem__(i).__iadd__(v))
which I find pretty weird (inplace add, and set anyway?).
So, S.O., what would be a nice and pythonic way to implement this?
Here is what I came up with. Does the job, but feels hackish.
class RAUQ:
def __init__(self, iterable):
# Stripped down example,
# actual implementation use segment tree.
self.l = list(iterable)
def __getitem__(self, i):
if isinstance(i, slice):
return _view(self, i)
return self.l[i]
def __setitem__(self, i, val):
if isinstance(i, slice):
""" No-op: work already done in view"""
return self
self.l[i] = val
return self
def __str__(self):
return str(_view(self, slice(None)))
__repr__ = __str__
class _view:
def __init__(self, parent, i):
# generic implementation non designed for single index.
assert isinstance(i, slice)
self.l = parent.l
self.i = i
def __iter__(self):
return iter(self.l[self.i])
def update(self, val):
""" Add val to all element of the view """
self.l[self.i] = [x+val for x in self]
def __iadd__(self, val):
self.update(val)
return self
def __isub__(self, val):
self.update(-val)
return self
def __str__(self):
return str(list(self))
__repr__ = __str__

Python is adding values to multiple instances

I have created a descriptor for lists.
After testing it seems that every time I append a value to a list of one instance, it is being added to another instance as well.
Even weirder, in the unittests it keeps appending to the list, and not resetting on every test.
My descriptor main class:
class Field(object):
def __init__(self, type_, name, value=None, required=False):
self.type = type_
self.name = "_" + name
self.required = required
self._value = value
def __get__(self, instance, owner):
return getattr(instance, self.name, self.value)
def __set__(self, instance, value):
raise NotImplementedError
def __delete__(self, instance):
raise AttributeError("Can't delete attribute")
#property
def value(self):
return self._value
#value.setter
def value(self, value):
self._value = value if value else self.type()
Descriptor list class:
class ListField(Field):
def __init__(self, name, value_type):
super(ListField, self).__init__(list, name, value=[])
self.value_type = value_type
def __set__(self, instance, value):
if not isinstance(value, list):
raise TypeError("{} must be a list".format(self.name))
setattr(instance, self.name, value)
def __iter__(self):
for item in self.value:
yield item
def __len__(self):
return len(self.value)
def __getitem__(self, item):
return self.value[item]
def append(self, value):
if not isinstance(value, self.value_type):
raise TypeError("Value is list {} must be of type {}".format(self.name, self.value_type))
self.value.append(value)
Unittests:
# Class I created solely for testing purposes
class ListTestClass(object):
l = ListField("l", int)
class TestListFieldClass(unittest.TestCase):
def setUp(self):
self.listobject = ListTestClass()
def test_add(self):
# The first number is added to the list
self.listobject.l.append(2)
def test_multiple_instances(self):
# This test works just fine
l1 = ListField("l1", int)
l2 = ListField("l2", int)
l1.append(1)
l2.append(2)
self.assertEqual(l1[0], 1)
self.assertEqual(l2[0], 2)
def test_add_multiple(self):
# This test works just fine
l1 = ListField("l1", int)
l1.append(1)
l1.append(2)
self.assertEqual(l1[0], 1)
self.assertEqual(l1[1], 2)
def test_add_error(self):
# This test works just fine
with self.assertRaises(TypeError):
l1 = ListField("l1", int)
l1.append("1")
def test_overwrite_list(self):
# This test works just fine
l1 = ListField("l1", int)
l1 = []
l1.append(1)
def test_overwrite_error(self):
# This test works just fine
l1 = ListTestClass()
l1.l.append(1)
with self.assertRaises(TypeError):
l1.l = "foo"
def test_multiple_model_instances(self):
# I create 2 more instances of ListTestClass
l1 = ListTestClass()
l2 = ListTestClass()
l1.l.append(1)
l2.l.append(2)
self.assertEqual(l1.l[0], 1)
self.assertEqual(l2.l[0], 2)
The last test fails
Failure
Traceback (most recent call last):
File "/home/user/project/tests/test_fields.py", line 211, in test_multiple_model_instances
self.assertEqual(l1.l[0], 1)
AssertionError: 2 != 1
When I look at the values for l1.1 and l2.l, they both have a list containing [2, 1, 2]
What am I missing here?
I looked to the memory addresses and it seems that the lists all point to the same object.
class ListFieldTest(object):
lf1 = ListField("lf1", int)
class TestClass(object):
def __init__(self):
l1 = ListFieldTest()
l2 = ListFieldTest()
l1.lf1.append(1)
l2.lf1.append(2)
print(l1.lf1)
print(l2.lf1)
print(hex(id(l1)))
print(hex(id(l2)))
print(hex(id(l1.lf1)))
print(hex(id(l2.lf1)))
This prints
[1, 2]
[1, 2]
0x7f987da018d0 --> Address for l1
0x7f987da01910 --> Address for l2
0x7f987d9c4bd8 --> Address for l1.lf1
0x7f987d9c4bd8 --> Address for l2.lf1
ListTestClass.l is a class attribute, so it is shared by all instances of the class. Instead, you should create an instance attribute, eg in the __init__ method:
class ListTestClass(object):
def __init__(self):
self.l = ListField("l", int)
Similar remarks apply to ListFieldTest. There may be other similar problems elsewhere in your code, I haven't examined it closely.
According to this source, the proper form is
class ListTestClass(object):
l_attrib = ListField("l", int)
def __init__(self)
self.l = l_attrib
Thanks to both #PM 2Ring and volcano I found the answer.
In the end this works great for value types:
class IntTestClass(object):
i = IntegerField("i")
However for a reference type (like a list) that won't work and you have to add a new list
class ListTestClass(object):
l = ListField("l", int)
def __init__(self):
self.l = []

Is there a clever way to pass the key to defaultdict's default_factory?

A class has a constructor which takes one parameter:
class C(object):
def __init__(self, v):
self.v = v
...
Somewhere in the code, it is useful for values in a dict to know their keys.
I want to use a defaultdict with the key passed to newborn default values:
d = defaultdict(lambda : C(here_i_wish_the_key_to_be))
Any suggestions?
It hardly qualifies as clever - but subclassing is your friend:
class keydefaultdict(defaultdict):
def __missing__(self, key):
if self.default_factory is None:
raise KeyError( key )
else:
ret = self[key] = self.default_factory(key)
return ret
d = keydefaultdict(C)
d[x] # returns C(x)
No, there is not.
The defaultdict implementation can not be configured to pass missing key to the default_factory out-of-the-box. Your only option is to implement your own defaultdict subclass, as suggested by #JochenRitzel, above.
But that isn't "clever" or nearly as clean as a standard library solution would be (if it existed). Thus the answer to your succinct, yes/no question is clearly "No".
It's too bad the standard library is missing such a frequently needed tool.
I don't think you need defaultdict here at all. Why not just use dict.setdefault method?
>>> d = {}
>>> d.setdefault('p', C('p')).v
'p'
That will of course would create many instances of C. In case it's an issue, I think the simpler approach will do:
>>> d = {}
>>> if 'e' not in d: d['e'] = C('e')
It would be quicker than the defaultdict or any other alternative as far as I can see.
ETA regarding the speed of in test vs. using try-except clause:
>>> def g():
d = {}
if 'a' in d:
return d['a']
>>> timeit.timeit(g)
0.19638929363557622
>>> def f():
d = {}
try:
return d['a']
except KeyError:
return
>>> timeit.timeit(f)
0.6167065411074759
>>> def k():
d = {'a': 2}
if 'a' in d:
return d['a']
>>> timeit.timeit(k)
0.30074866358404506
>>> def p():
d = {'a': 2}
try:
return d['a']
except KeyError:
return
>>> timeit.timeit(p)
0.28588609450770264
I just want to expand on Jochen Ritzel's answer with a version that makes typecheckers happy:
from typing import Callable, TypeVar
K = TypeVar("K")
V = TypeVar("V")
class keydefaultdict(dict[K, V]):
def __init__(self, default_factory: Callable[[K], V]):
super().__init__()
self.default_factory = default_factory
def __missing__(self, key: K) -> V:
if self.default_factory is None:
raise KeyError(key)
else:
ret = self[key] = self.default_factory(key)
return ret
Here's a working example of a dictionary that automatically adds a value. The demonstration task in finding duplicate files in /usr/include. Note customizing dictionary PathDict only requires four lines:
class FullPaths:
def __init__(self,filename):
self.filename = filename
self.paths = set()
def record_path(self,path):
self.paths.add(path)
class PathDict(dict):
def __missing__(self, key):
ret = self[key] = FullPaths(key)
return ret
if __name__ == "__main__":
pathdict = PathDict()
for root, _, files in os.walk('/usr/include'):
for f in files:
path = os.path.join(root,f)
pathdict[f].record_path(path)
for fullpath in pathdict.values():
if len(fullpath.paths) > 1:
print("{} located in {}".format(fullpath.filename,','.join(fullpath.paths)))
Another way that you can potentially achieve the desired functionality is by using decorators
def initializer(cls: type):
def argument_wrapper(
*args: Tuple[Any], **kwargs: Dict[str, Any]
) -> Callable[[], 'X']:
def wrapper():
return cls(*args, **kwargs)
return wrapper
return argument_wrapper
#initializer
class X:
def __init__(self, *, some_key: int, foo: int = 10, bar: int = 20) -> None:
self._some_key = some_key
self._foo = foo
self._bar = bar
#property
def key(self) -> int:
return self._some_key
#property
def foo(self) -> int:
return self._foo
#property
def bar(self) -> int:
return self._bar
def __str__(self) -> str:
return f'[Key: {self.key}, Foo: {self.foo}, Bar: {self.bar}]'
Then you can create a defaultdict as so:
>>> d = defaultdict(X(some_key=10, foo=15, bar=20))
>>> d['baz']
[Key: 10, Foo: 15, Bar: 20]
>>> d['qux']
[Key: 10, Foo: 15, Bar: 20]
The default_factory will create new instances of X with the specified
arguments.
Of course, this would only be useful if you know that the class will be used in a default_factory. Otherwise, in-order to instantiate an individual class you would need to do something like:
x = X(some_key=10, foo=15)()
Which is kind of ugly... If you wanted to avoid this however, and introduce a degree of complexity, you could also add a keyword parameter like factory to the argument_wrapper which would allow for generic behaviour:
def initializer(cls: type):
def argument_wrapper(
*args: Tuple[Any], factory: bool = False, **kwargs: Dict[str, Any]
) -> Callable[[], 'X']:
def wrapper():
return cls(*args, **kwargs)
if factory:
return wrapper
return cls(*args, **kwargs)
return argument_wrapper
Where you could then use the class as so:
>>> X(some_key=10, foo=15)
[Key: 10, Foo: 15, Bar: 20]
>>> d = defaultdict(X(some_key=15, foo=15, bar=25, factory=True))
>>> d['baz']
[Key: 15, Foo: 15, Bar: 25]

Categories