python list/dict property best practice - python

I have a class object that stores some properties that are lists of other objects. Each of the items in the list has an identifier that can be accessed with the id property. I'd like to be able to read and write from these lists but also be able to access a dictionary keyed by their identifier. Let me illustrate with an example:
class Child(object):
def __init__(self, id, name):
self.id = id
self.name = name
class Teacher(object):
def __init__(self, id, name):
self.id = id
self.name = name
class Classroom(object):
def __init__(self, children, teachers):
self.children = children
self.teachers = teachers
classroom = Classroom([Child('389','pete')],
[Teacher('829','bob')])
This is a silly example, but it illustrates what I'm trying to do. I'd like to be able to interact with the classroom object like this:
#access like a list
print classroom.children[0]
#append like it's a list
classroom.children.append(Child('2344','joe'))
#delete from like it's a list
classroom.children.pop(0)
But I'd also like to be able to access it like it's a dictionary, and the dictionary should be automatically updated when I modify the list:
#access like a dict
print classroom.childrenById['389']
I realize I could just make it a dict, but I want to avoid code like this:
classroom.childrendict[child.id] = child
I also might have several of these properties, so I don't want to add functions like addChild, which feels very un-pythonic anyway. Is there a way to somehow subclass dict and/or list and provide all of these functions easily with my class's properties? I'd also like to avoid as much code as possible.

An indexed list class:
class IndexedList(list):
def __init__(self, items, attrs):
super(IndexedList,self).__init__(items)
# do indexing
self._attrs = tuple(attrs)
self._index = {}
_add = self._addindex
for obj in self:
_add(obj)
def _addindex(self, obj):
_idx = self._index
for attr in self._attrs:
_idx[getattr(obj, attr)] = obj
def _delindex(self, obj):
_idx = self._index
for attr in self._attrs:
try:
del _idx[getattr(obj,attr)]
except KeyError:
pass
def __delitem__(self, ind):
try:
obj = list.__getitem__(self, ind)
except (IndexError, TypeError):
obj = self._index[ind]
ind = list.index(self, obj)
self._delindex(obj)
return list.__delitem__(self, ind)
def __delslice__(self, i, j):
for ind in xrange(i,j):
self.__delitem__(ind)
def __getitem__(self, ind):
try:
return self._index[ind]
except KeyError:
return list.__getitem__(self, ind)
def __getslice__(self, i, j):
return IndexedList(list.__getslice__(self, i, j))
def __setitem__(self, ind, new_obj):
try:
obj = list.__getitem__(self, ind)
except (IndexError, TypeError):
obj = self._index[ind]
ind = list.index(self, obj)
self._delindex(obj)
self._addindex(new_obj)
return list.__setitem__(ind, new_obj)
def __setslice__(self, i, j, newItems):
_get = self.__getitem__
_add = self._addindex
_del = self._delindex
newItems = list(newItems)
# remove indexing of items to remove
for ind in xrange(i,j):
_del(_get(ind))
# add new indexing
if isinstance(newList, IndexedList):
self._index.update(newList._index)
else:
for obj in newList:
_add(obj)
# replace items
return list.__setslice__(self, i, j, newList)
def append(self, obj):
self._addindex(obj)
return list.append(self, obj)
def extend(self, newList):
newList = list(newList)
if isinstance(newList, IndexedList):
self._index.update(newList._index)
else:
_add = self._addindex
for obj in newList:
_add(obj)
return list.extend(self, newList)
def insert(self, ind, new_obj):
# ensure that ind is a numeric index
try:
obj = list.__getitem__(self, ind)
except (IndexError, TypeError):
obj = self._index[ind]
ind = list.index(self, obj)
self._addindex(new_obj)
return list.insert(self, ind, new_obj)
def pop(self, ind=-1):
# ensure that ind is a numeric index
try:
obj = list.__getitem__(self, ind)
except (IndexError, TypeError):
obj = self._index[ind]
ind = list.index(self, obj)
self._delindex(obj)
return list.pop(self, ind)
def remove(self, ind_or_obj):
try:
obj = self._index[ind_or_obj]
ind = list.index(self, obj)
except KeyError:
ind = list.index(self, ind_or_obj)
obj = list.__getitem__(self, ind)
self._delindex(obj)
return list.remove(self, ind)
which can be used as:
class Child(object):
def __init__(self, id, name):
self.id = id
self.name = name
class Teacher(object):
def __init__(self, id, name):
self.id = id
self.name = name
class Classroom(object):
def __init__(self, children, teachers):
self.children = IndexedList(children, ('id','name'))
self.teachers = IndexedList(teachers, ('id','name'))
classroom = Classroom([Child('389','pete')], [Teacher('829','bob')])
print classroom.children[0].name # -> pete
classroom.children.append(Child('2344','joe'))
print len(classroom.children) # -> 2
print classroom.children[1].name # -> joe
print classroom.children['joe'].id # -> 2344
print classroom.children['2344'].name # -> joe
p = classroom.children.pop('pete')
print p.name # -> pete
print len(classroom.children) # -> 1
Edit: I had made a mistake in some of the exception-handling (catching KeyError instead of IndexError); it is fixed. I will add some unit-testing code. If you run into any further errors, please let me know!

You could subclass the collections.OrderedDict class. For example:
import collections
class Child(object):
def __init__(self, id, name):
self.id = id
self.name = name
def __repr__(self):
return 'Child(\'%s\', \'%s\')' % (self.id, self.name)
class MyOrderedDict(collections.OrderedDict):
def __init__(self, *args, **kwds):
super(MyOrderedDict, self).__init__()
if len(args) > 0:
for i in args[0]:
super(MyOrderedDict, self).__setitem__(i.id, i)
def __getitem__(self, key):
if isinstance(key, int):
return super(MyOrderedDict, self).__getitem__(self.keys()[key])
if isinstance(key, slice):
return [super(MyOrderedDict, self).__getitem__(k) for k in self.keys()[key]]
return super(MyOrderedDict, self).__getitem__(key)
def append(self, item):
super(MyOrderedDict, self).__setitem__(item.id, item)
def pop(self, key = None, default = object()):
if key is None:
return self.popitem()
return super(MyOrderedDict, self).pop(self.keys()[key], default = default)
class Classroom(object):
def __init__(self, children):
self.children = MyOrderedDict(children)
classroom = Classroom([Child('389', 'pete')])
print repr(classroom.children[0])
classroom.children.append(Child('2344', 'joe'))
print repr(classroom.children.pop(0))
print repr(classroom.children['2344'])
print repr(classroom.children[0:1])
This code outputs:
Child('389', 'pete')
Child('389', 'pete')
Child('2344', 'joe')
[Child('2344', 'joe')]

Maybe this is some code you wanted to avoid, but for small scale objects performance should be tolerable. I think it is at least within your constraint: I'd also like to avoid as much code as possible.
class Classroom(object):
""" Left out the teachers part for simplicity """
def __init__(self, children):
self.children = children
self._child_by_id = {}
#property
def child_by_id(self):
""" Return a dictionary with Child ids as keys and Child objects
as corresponding values.
"""
self._child_by_id.clear()
for child in self.children:
self._child_by_id[child.id] = child
return self._child_by_id
This will be always up-to-date, since it is computed on the fly.
A little more optimized version could look like this:
...
#property
def child_by_id(self):
scbi, sc = self._child_by_id, self.children
scbi.clear()
for child in sc:
scbi[child.id] = child
return scbi

Here's another variant:
class Classroom(object):
def __init__(self, objects):
for obj in objects:
self.add(obj)
def add(self, obj):
name = obj.__class__.__name__ + "ById"
if name not in self.__dict__:
self.__dict__[name] = {}
self.__dict__[name][obj.id] = obj
def remove(self, obj):
name = obj.__class__.__name__ + "ById"
del self.__dict__[name][obj.id]
def listOf(self, name):
return self.__dict__[name + "ById"].values()
classroom = Classroom([Child('389','pete'),
Teacher('829','bob')])
print classroom.ChildById['389']
classroom.ChildById['123'] = Child('123', 'gabe')
print classroom.listOf('Child')
classroom.remove(classroom.listOf('Teacher')[0])
print classroom.TeacherById
It lets you get inconsistent by allowing you to do classroom.ChildById['123'] = Teacher('456', 'gabe') but it might be good enough to do what you're looking for.

Related

Python class does not have value

I am a Javascript engineer and am switching into a JS/Python role. Working on some easy leetcodes to get some quick Python practice.
I'm looking to create a LinkedList here and perhaps I am coming at it from a JS mindset?
Error:
AttributeError: type object 'LinkedListNode' has no attribute 'value'
utils.py
# LinkedList Methods
def createLinkedList(arr):
head = createLinkedListNode(None, arr.pop(0))
def populateList(arr, prevNode):
if arr:
node = createLinkedListNode(None, arr.pop(0))
prevNode.next = node
if arr:
populateList(arr, node)
populateList(arr, head)
return head
def createLinkedListNode(next, value):
class LinkedListNode:
def __init__(self):
self.next = next
self.value = value
return LinkedListNode
deleteNode.py
from python.utils import createLinkedList, linkedListToArray
useCase1 = [4, 5, 1, 9]
linkedList = createLinkedList(useCase1)
^ linkedList.value doesn't exist?
Some misunderstandings with python classes:
The class LinkedListNode should not defined in function.
Return LinkedListNode is actually returning the class itself, but not the Instance. To return the instance, you have to call the class. return LinkedListNode()
Using next as instance variable is not ideal. next is an iteration function in python, so when you set self.next = next, you are actually assigning the function to self.next
If you want to set a variable, for example self.next_value = next_value, you should put next_value as a parameter of __init__ function, like def __init__(self, next_value)
Here is a simple demo of Linked List:
class LinkedList:
def __init__(self, value):
self.value = value
self.next_value = None
def __iter__(self):
yield self.value
if self.next_value is not None:
yield from self.next_value
# else raise StopIteration
def __getitem__(self, index):
if index == 0:
return self.value
else:
return self.next_value[index-1]
# recursively get the next value
def __str__(self):
return str(self.value) + ' -> ' + str(self.next_value)
def __len__(self):
if self.next_value is None:
return 1
else:
return 1 + len(self.next_value)
# recursively get the length
def append(self, value):
if self.next_value is None:
self.next_value = LinkedList(value, self)
else:
self.next_value.append(value)
a = LinkedList(2)
a.append(1)
a.append(3)
for num in a:
print(num, end=", ")
print()
print(a[1])
print(a)
print(len(a))
Output:
2, 1, 3,
1
2 -> 1 -> 3 -> None
3
createLinkedListNode() returns the LinkedListNode class itself, not an instance of the class.
Why are you defining classes and functions inside of other functions? That's an odd way of doing things.

how to cache object using pickle in __new__ method?

I'd like to cache an object in __new__ method so that it can load the cache when a new object is constructed, but now the following code will got an exception:
RecursionError: maximum recursion depth exceeded while calling a Python object
I have no idea about how to break the recursion
import pickle
class Cache:
def __init__(self):
self.d = {}
def __setitem__(self, obj, val):
self.d[obj] = pickle.dumps(val)
def __getitem__(self, obj):
return pickle.loads(self.d[obj])
class Car:
cache = Cache()
def __reduce__(self):
return (self.__class__, (self.name,))
def __new__(cls, name):
try:
return cls.cache[name]
except KeyError:
return cls.new(name)
#classmethod
def new(cls, name):
car = object.__new__(cls)
car.init(name)
cls.cache[name] = car
return car
def init(self, name):
self.name = name
def __repr__(self):
return self.name
a = Car('audi')
b = Car('audi')
Have a try. This may fix this, but it may not be the proper solution. If anyone have any better idea, feel free to leave comments.
Just remove the __reduce__ method.
Then implement __getnewargs__ and __getnewargs_ex__
import pickle
class Cache:
def __init__(self):
self.d = {}
def __setitem__(self, obj, val):
self.d[obj] = pickle.dumps(val)
def __getitem__(self, obj):
return pickle.loads(self.d[obj])
def __contains__(self, x):
return x in self.d
class Car:
cache = Cache()
def __new__(cls, name, extra=None, _FORCE_CREATE=False):
if _FORCE_CREATE or name not in cls.cache:
car = object.__new__(cls)
car.init(name)
car.extra = extra
cls.cache[name] = car
return car
else:
return cls.cache[name]
def init(self, name):
self.name = name
def __repr__(self):
return self.name
def __getnewargs__(self):
return (self.name, None, True)
def __getnewargs_ex__(self):
# override __getnewargs_ex__ and __getnewargs__ to provide args for __new__
return (self.name, ), {"_FORCE_CREATE": True}
a = Car('audi', extra="extra_attr")
b = Car('audi')
print(id(a), a.extra) # 1921399938016 extra_attr
print(id(b), b.extra) # 1921399937728 extra_attr

Make Class Mutable by Subscription or Implement Views

Consider the following Python class definition:
class Custom:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return Custom(items[key])
As an example for what I want to achieve is the following code snippet to print "true" instead of "false":
custom = Custom([1, 2, 3, 4])
view = custom[0:2]
view.items[0] = 0
print(custom.items[0] == 0)
This is, I want to be able to subscript my class (which basically consists of lists only) in a way that makes __getitem__() return "views" of the instances in the sense that changes to the lists of the views propagate to the lists of the original instance.
I want my class to behave with its saved lists exactly like e.g. numpy arrays behave with the values it saves. The following prints "true":
array = np.array([1, 2, 3, 4])
view = array[0:2]
view[0] = 0
array == 0
How can I achieve this? Thanks!
Borrowing from this answer https://stackoverflow.com/a/3485490/10035985 :
import collections.abc
class ListSlice(collections.abc.Sequence):
def __init__(self, alist, start, alen):
self.alist = alist
self.start = start
self.alen = alen
def __len__(self):
return self.alen
def adj(self, i):
while i < 0:
i += self.alen
if i >= self.alen:
raise IndexError
return i + self.start
def __getitem__(self, i):
return self.alist[self.adj(i)]
def __setitem__(self, i, v):
self.alist[self.adj(i)] = v
def __delitem__(self, i, v):
del self.alist[self.adj(i)]
self.alen -= 1
def insert(self, i, v):
self.alist.insert(self.adj(i), v)
self.alen += 1
class Custom:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
if isinstance(key, slice):
return ListSlice(self.items, key.start, key.stop - key.start)
return self.items[key]
custom = Custom([1, 2, 3, 4])
view = custom[1:3]
view[0] = 0
print(custom.items[1] == 0)
print(custom.items)
Prints:
True
[1, 0, 3, 4]
When we write
view[0] = 0
We're not calling __getitem__. We're calling __setitem__. That line is roughly equivalent to
view.__setitem__(0, 0)
So you need to implement that method
class Custom:
def __init__(self, items):
self.items = items
def __getitem__(self, key):
return Custom(self.items[key])
def __setitem__(self, key, value):
self.items[key] = value
# ... or whatever wrapping/unwrapping you want to do with the value

Appending a self constructed class in priority que gives TypeError: '<' not supported between instances of 'str' and 'Node'

import sys
from queue import PriorityQueue
class Node(object):
def __init__(self, data = None):
self.value = data
self.right = None
self.left = None
class Huffman():
def __init__(self, data):
self.data = data
def encoding(self):
my_dict = {}
for i in self.data:
my_dict[i] = my_dict.get(i, 0)+1
q = PriorityQueue()
for i, j in my_dict.items():
q.put((j,i))
while q.qsize() >= 2:
val_1 = q.get()
val_2 = q.get()
new_node_1 = Node(val_1[0]+val_2[0])
if isinstance(val_1[1], str):
new_node_1.left = val_1[1]
else:
new_node_1.left = val_1[1]
if isinstance(val_2[1], str):
new_node_1.right = val_2[1]
else:
new_node_1.right = val_2[1]
q.put((new_node_1.value, new_node_1))
Herein, I am getting an error in while loop, after a few operations it fails to use the method q.get(), or q.put().
For example, Huffman('AADCIDCVUSHDUSHUSAHDIADHIAD').encoding()
I do not want to change entire code, but just want to change priorities such that it priorities entries on the basis of first element (which is integer) only.
You are right, the Priority Queue doesn't know how to compare a str with a Node. You have to override the comparator functions in your Node class to let the Priority Queue know how to compare two different types.
Example (change the comparators as you want):
class Node(object):
def __init__(self, data=None):
self.value = data
self.right = None
self.left = None
def __lt__(self, obj):
"""self < obj."""
return self.value < obj
def __le__(self, obj):
"""self <= obj."""
return self.value <= obj
def __eq__(self, obj):
"""self == obj."""
return self.value == obj
def __ne__(self, obj):
"""self != obj."""
return self.value != obj
def __gt__(self, obj):
"""self > obj."""
return self.value > obj
def __ge__(self, obj):
"""self >= obj."""
return self.value >= obj

Property setter not working in Python class

I am using a class (MainClass) over which I have no control. I want to base my class on MainClass but to add extra functionality. I have added an attribute (index) to my class (SuperClass), but when I try convert index to a property, the #.setter seems to be ignored. What is wrong here?
class MainClass(object):
def __init__(self):
self.name = 'abc'
class SuperClass(object):
def __init__(self, main, *args, **kwargs):
super(SuperClass, self).__init__(*args, **kwargs)
self.__main = main
self._index = 0
def __getattr__(self, attr):
return getattr(self.__main, attr)
def __setattr__(self, attr, val):
if attr == '_SuperClass__main':
object.__setattr__(self, attr, val)
return setattr(self.__main, attr, val)
#property
def index(self):
return self._index
#index.setter
def index(self, value):
self._index = value
main_object = MainClass()
super_object = SuperClass(main_object)
print('x', super_object.index, super_object.name)
super_object.index = 3
print('y', super_object.index)
super_object.index += 2
print('z', super_object.index)
__getattr__ is only used when the normal lookup mechanism fails.
__setattr__, however, is called for all attempts to set an attribute. This means your current definition creates an attribute named index on the
MainClass instance, rather than accessing the property's setter.
>>> super_object._SuperClass__main.index
2
Because __setattr__ always calls setattr(self.__main, attr, val), += is effectively treated as =.
__setattr__ has to handle three cases:
The attribute _SuperClass__main itself, for when you assign to self.__main in __init__.
Assignments to attributes that exist on self.__main
Assignments to attributes specific to SuperClass.
With that in mind, try
def __setattr__(self, attr, val):
if attr == '_SuperClass__main':
super().__setattr__(attr, val)
elif hasattr(self.__main, attr):
setattr(self.__main, attr, val)
else:
super().__setattr__(attr, val)
The __setattr__ method you have defined is taking precedence over the #index.setter
Simplify the code and it should work:
class MainClass(object):
def __init__(self):
self.name = 'abc'
class SuperClass(object):
def __init__(self, main, *args, **kwargs):
super(SuperClass, self).__init__(*args, **kwargs)
self.__main = main
self._index = 0
#property
def name(self):
return self.__main.name
#name.setter
def name(self):
return self.__main.name
#property
def index(self):
return self._index
#index.setter
def index(self, value):
self._index = value
main_object = MainClass()
super_object = SuperClass(main_object)
print('x', super_object.index, super_object.name)
super_object.index = 3
print('y', super_object.index)
super_object.index += 2
print('z', super_object.index)
Output:
x 0 abc
y 3
z 5
I would also suggest the simpler option of just inheriting from MainClass instead of using composition and delegation:
class SuperClass(MainClass):
def __init__(self):
super().__init__()
self._index = 0
#property
def index(self):
return self._index
#index.setter
def index(self, value):
self._index = value

Categories

Resources