def depth(obj : Union[dict,list]):
'''
Recursively sort the number of layers of a nested
list or dictionary `x`.
'''
if type(obj) is dict and obj:
return 1 + max(depth(obj[a]) for a in obj)
if type(obj) is list and obj:
return 1 + max(depth(a) for a in obj)
return 0
depth([0,[1,[2]]])
depth({0:{0:{0:0}}})
flatten([0,[1,[2,[3]]]])
i=flatten({'a':0,'b':{'c':1,'d':{'e':2}}})
print(i)
unflatten(i)
a=np.array([[0,1],[2,3]])
get(a,0)
get(a,'doesnt exist')
get(a,'doesnt exist',retnone=False)
get(a,'mean')
get(a,'mean',-1)
get(a,'mean',call=True,axis=-1)
get(a,'mean',call=True)
get(a,'mean','doesnt exist',call=True)
This function is extremely powerful. Because it tries to access different data structures, and skips any it can't access, get
can be used in very flexible and exploratory programming styles in order to extract data and perform transformations.
Next, we will use a similar method to insert arbitrary methods and data into objects with give
.
a=[0]
give(a,0,1) #give the 0th element of a the value 1
print(a)
give
only works on indexable objects:
a=0
give(a,0)
a=np.array([[0,1],[2,3]])
give(a,(1,1),100)
a #arrays must be accessed using tuples
a=[[0,1],[2,3]]
give(a,1,1,100)
a #lists must be accessed using sequences
give
can be used to replace loops:
a=[0]
[give(a,0,a[0]+1) for i in range(10)]
print(a)
Let's use a more complex example. We'll use a networkx graph object to create a simple ring of nodes 0->1->2->0 and apply some attributes.
import networkx as nx
g=nx.DiGraph()
g.add_edges_from([(0,1),(1,2),(2,0)])
[get(g.edges,e,['weight']) for e in g.edges] #normally this would produce an error!
[give(g.edges,e,weight=np.random.random(1)) for e in g.edges] #give returns none
for e in g.edges: #newly applied values
print(e,g.edges[e]['weight'])
[get(g.nodes,n,'a') for n in g.nodes] #normally would give error!
[give(g.nodes,n,a=np.random.random(1)) for n in g.nodes]
[get(g.nodes,n,'a') for n in g.nodes]
These functions can be used in many more ways than those shown here, and encourage exploratory programming.
sort([9,0,1,8,7])
sort([9,0,1,8,7],by=lambda t:t%3)
sort(g.nodes,'a')
sort(g.nodes,'a',by=g.nodes)
sort(g.edges,'weight')
sort(g.edges,'weight',by=g.edges)
g.add_edges_from([(0,0)]) #add another edge
sort(g.nodes,by=g.edges,key=lambda t:len(sort(list(t[-1])))) #then sort nodes over edges by in-degree
sort(g.nodes,by=g.in_degree) #same as above but relies on 'in_degree' attribute of networkx
In conclusion, sort
is capable of handling an immense number of possible data structures, and it's best understood by playing around with it and seeing what works!
A word of caution though: the more complex and custom the python object, the more difficult it is to typecast. Remember to transform your data so that the key
lambda performs a valid comparison - since it's a lambda function, it's still up to you to make sure the data types are actually comparable in a way that admits a binary operation.
pipe(lambda x,y:x+y,otype=int,ftype=int,y=1.9)(1.9) #convert the input to int, and the output to int
Pipe is useful for converting function datatypes and passing them as arguments to other functions, as follows:
def maps(obj,
*funcs,
depth=0,
zipit=False,
to=None,
squeeze=True):
'''
Sequentially map `funcs` over the elements of `obj`, "o".
The first `depth` number of funcs are mapped sequentially f(g(h(...(o)..)))=x.
The remaining number of funcs are mapped separately (u(x),v(x),...).
Use partial `funcs` to fill in all args but `obj` if other parameters needed.
If `keys`, return tuples of the object elements "o" along with map outputs.
'''
if not funcs:
return obj
else:
obj=obj if hasattr(obj,'__iter__') and type(obj)!='str' else [obj] #asiter(obj)
r=[o for o in obj]
[[give(r,i,get(f,r[i])) for f in funcs[:depth]] for i in range(len(r))]
[give(r,i,[get(f,r[i]) for f in funcs[depth:]]) for i in range(len(r))]
if squeeze:
r=np.ndarray.tolist(np.squeeze(np.array(r,dtype=object)))
if zipit:
r=list(zip(obj,r))
return cast(r,to)
maps(0,lambda t:t+1,lambda t:t+1,lambda t:t+1,depth=0) #add 1 separately to 0, three times
maps(0,lambda t:t+1,lambda t:t+1,lambda t:t+1,depth=1) #add 1 to 0, then add 1 to that separately twice
maps(0,lambda t:t+1,lambda t:t+1,lambda t:t+1,depth=-1) #sequantially apply all functions
maps([0,1],lambda t:t+1,lambda t:t+1,lambda t:t+1) #apply functions over elements of iterable
maps([0,1],lambda t:t+1,lambda t:t+1,lambda t:t+1,depth=-1)
maps([0,1],lambda t:t+1,lambda t:t+1,lambda t:t+1,depth=-1,zipit=True) #zip the arguments with their func outputs
maps(g.nodes,pipe(g.predecessors,None,list),pipe(g.successors,None,list),zipit=True) #use a pipeline
maps(g.nodes,pipe(g.predecessors,None,list),pipe(g.successors,None,list),zipit=True,to=dict) #convert the output