Why a range_iterator when a range is reversed? - python-3.x

I can subscript a range object:
>>> r = range(4)
>>> r
range(0, 4)
>>> r[3]
3
>>> for i in r:
print(i)
0
1
2
3
>>> list(r)
[0, 1, 2, 3]
But, if I call reversed on the same range object:
>>> r = reversed(range(4))
>>> r
<range_iterator object at memaddr>
>>> for i in r:
print(i)
3
2
1
0
>>> r[3]
TypeError: 'range_iterator' object is not subscriptable # ?
>>> range(r)
TypeError: 'range_iterator' cannot be interpreted as an integer # ?
>>> list(r)
[] # ? uhmm
Hmm... Acting kinda like a generator but less useful.
Is there a reason a reversed range object isn't like a normal generator / iterator in how it quacks?

The reversed function returns an iterator, not a sequence. That's just how it's designed. The range_iterator you're seeing is essentially iter called on the reversed range you seem to want.
To get the reversed sequence rather than a reverse iterator, use the "alien smiley" slice: r[::-1] (where r is the value you got from range). This works both in Python 2 (where range returns a list) and in Python 3 (where range returns a sequence-like range object).

You need to change r back to a list type. For example:
reversed([1,2]) #prints <listreverseiterator object at 0x10a0039d0>
list(reversed([1,2])) #prints [2,1]
Edit
To clarify what you are asking, here is some sample I/O:
>>> r = range(5)
>>> x = reversed(r)
>>> print x
<listreverseiterator object at 0x10b6cea90>
>>> x[2]
Traceback (most recent call last):
File "<pyshell#24>", line 1, in <module>
x[2]
TypeError: 'listreverseiterator' object has no attribute '__getitem__'
>>> x = list(x)
>>> x[2] #it works here
2

Related

Error when removing nodes that do not have a certain attrubute from the graph

I've generated a graph with networkx. Then I add attribute 'commu_per' to some nodes. Then would like to remove those nodes that do not have attribute 'commu_per'.
import urllib3
import io
import networkx as nx
from networkx.algorithms import community
## Import dataset
http = urllib3.PoolManager()
url = 'https://raw.githubusercontent.com/leanhdung1994/WebMining/main/lesmis.gml'
f = http.request('GET', url)
data = io.BytesIO(f.data)
g = nx.read_gml(data)
## Define a function to add attributes
def add_att(g, att, att_name):
att = list(att)
for i in g.nodes():
for j in range(len(att)):
if i in list(att[j]):
nx.set_node_attributes(g, {i: j}, name = att_name)
break
## Add attributes
commu_per = community.k_clique_communities(g, 3)
add_att(g, commu_per, 'commu_per')
g_1 = g
## Remove nodes which do not have attribute 'commu_per'
for i in g.nodes:
if 'commu_per' not in g.nodes[i]:
g_1.remove_node(i)
Then it returns an error
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-6-7339f3f2ea6a> in <module>
26 g_1 = g
27 ## Remove nodes which do not have attribute 'commu_per'
---> 28 for i in g.nodes:
29 if 'commu_per' not in g.nodes[i]:
30 g_1.remove_node(i)
RuntimeError: dictionary changed size during iteration
Could you please elaborate on how to solve this error?
You g and g1 dict objects are same. So getting an iterator on 1 and using that to try and delete the other will not work.
>>> a = {1:10, 2:20}
>>> b = a
>>> id(b) == id(a)
True
>>> b[4] = 40
>>> id(b) == id(a)
True
>>> b
{1: 10, 2: 20, 4: 40}
>>> a
{1: 10, 2: 20, 4: 40}
>>>
Use copy() method to get a new copy so that you can remove the keys while iterating on same object.
>>> c = b.copy()
>>> id(b) == id(c)
False
>>> c[5] = 50
>>> c
{1: 10, 2: 20, 4: 40, 5: 50}
>>>
>>> b
{1: 10, 2: 20, 4: 40}
>>>
Another method is to use for i in list(g.nodes)
Your problem is caused by the fact that networkx stores the graph in a data structure based on dictionaries.
When you do a for loop to step through a dictionary, python will run into trouble if the dictionary itself changes.
So what you'll need to do is somehow create a list or some other collection of the nodes that don't have the attribute and then delete them.
If you aren't yet comfortable with "list comprehensions" you can do it like this:
nodes_to_delete = []
for node in g.nodes:
if 'commu_per' not in g.nodes[node]:
nodes_to_delete.append(node)
g.remove_nodes_from(nodes_to_delete)
To do it with a list comprehension (which eliminates the for loop) you can do
nodes_to_delete = [node for node in g.nodes if 'commu_per' not in g.nodes[node]]
g.remove_nodes_from(nodes_to_delete)
delete = [i for i in g.nodes if 'commu_per' not in g.nodes[node]]
g.remove_nodes_from(delete)

How to reassign multiple variables (if possible) with an iterator in Python 3

update Adding a usage example to better explain why I'm trying to elicit this behavior. See update at end of post. Added better syntax for links and added a link to article suggested as an answer.
I am trying to use an iterator to alter the value of a list of variables.
>>> a = 1
>>> b = 2
>>> c = 3
>>> for i in [a,b,c]:
i += 1
>>> a,b,c #Expecting (2, 3, 4)
(1, 2, 3)
This doesn't appear to work, I've tried some other ways (see below) without success.
1. Will someone please tell me a better way to approach this problem?
2. Will someone explain why the example above doesn't work as I expected?
3. Will someone tell me how/where I could have found #2 in python help documentation?
Places I've previously looked for answers...
StackOverflow question "reassign variable to original value defined prior to the loop at start of each"
StackOverflow question on reassigning variables (but not a list of variables)
Python docs: Objects and Value Types
Python docs section 3.1.3
I feel like the last reference to the python docs might have the answer, but I was overwhelmed with the amount of info, my brain is tired, and I'm hoping someone on s.o. can help.
also tried...
>>> help(__name__)
Help on module __main__:
NAME
__main__
DATA
__annotations__ = {}
a = 1
b = 2
c = 3
i = 4
FILE
(built-in)
but if anything, this only confused me further.
Lastly I tried...
>>> a = 1
>>> b = 2
>>> c = 3
>>> R = [a, b, c]
>>> for i in range(3):
R[i] += 1
>>> a, b, c #Hoping for (2, 3, 4)
(1, 2, 3)
>>> R #Kind of expected (1, 2, 3) based on the above behavior
[2, 3, 4]
update
I used the list for convenience, since I could iterate through its members. The part I'm not understanding is that when I say...
>>> x = [a, b, c]
I am creating a list such that
x = [the value assigned to a,
the value assigned to b,
the value assigned to c]
rather than
x = [the variable a, the variable b, the variable c]
so when I am trying to use the syntax
>>> x[0] += 1 #take the current ITEM in place 0 and increment its value by 1.
instead of
>>> a += 1
it is instead interpreted as...
take the current VALUE of the ITEM in place 0,
increment that VALUE by 1,
this is the new VALUE of ITEM in place 0 ...
and I lose the reference to the original ITEM... the variable a.
Here is a usage example of why I am trying to elicit this behavior...
>>> class breakfast():
>>> def __init__(self, eggs=None, bacon=None, spam=None):
>>> self.eggs = eggs
>>> self.bacon = bacon
>>> self.spam = spam
>>> updateOrder = False
>>> updateItems = []
>>> for i in [self.eggs, self.bacon, self.spam]:
>>> if i == None:
>>> updateOrder = True
>>> updateItems.append(i)
>>> else:
>>> pass
>>> if updateOrder:
>>> self.updateOrder(itemsToUpdate = updateItems)
>>>
>>> def updateOrder(self, itemsToUpdate):
>>> for i in itemsToUpdate: ###also tried...
>>> ###for i in range(len(itemsToUpdate)):
>>> print('You can\'t order None, so I am updating your order to 0 for some of your items.')
>>> i = 0
>>> ###itemsToUpdate[i] = 0
>>>
>>> #Finally, the good part, behavior I'm after...
>>> myBreakfast = breakfast(eggs=2,bacon=3)
You can't order None, so I am updating your order to 0 for some of your items.
>>> myBreakfast.spam == 0 #hoping for True....
False
The only way I know would work to get this behavior is to instead say...
>>> ...
>>> def updateOrder(self):
>>> orderUpdated=False
>>> if self.eggs == None:
>>> self.eggs = 0
>>> orderUpdated = True
>>> if self.bacon == None:
>>> self.bacon = 0
>>> orderUpdated = True
>>> if self.spam == None:
>>> self.spam = 0
>>> orderUpdated = True
>>> if orderUpdated:
>>> print('You can\'t order None, so I am updating your order')
However, if there are (lots) more than just 3 items on the menu the code for updateOrder would become very long and worse repetitive.
You have to use a loop and change the each value of the list during the loop
a=[1, 2, 3]
for i in range(len(a)):
a[i] += 1
Output will be
[2, 3, 4]
To access and change values in a list, you need to select items based on their location (you can also use slices). So, a[0] = 1, a[1] = 2 and so on. To change the value of a[0], you need to assign a new value to it (as done in the for loop).
Your example does not work because you are just changing the value of i (which is 1, then 2, then 3), instead of actually changing the list. You are not selecting any item from the list itself.
The documentation is given here (see section 3.1.3)
EDIT
Based on your clarification: Creating a list of variables that have been defined elsewhere:
a, b, c = 5, 6, 7
letters = [a, b, c]
# See id of variable a
id(a)
# 94619952681856
# See id of letters[0]
id(letters[0]
# 94619952681856
# Add 1 to letters[0]
letters[0] += 1
# See id of letters[0]
# 94619952681888
As you can see, when the list is first created, the item in the list points to the same variable However, as soon as the value in the list is changed, python creates a new item, so that the original value is unchanged.
This works the other way around also. Any change in the variable a will not affect the list, as once the variable is modified, its id will change, while the id of the item in the list will not.
And in the breakfast example, why don't you just assign 0 as the default value of each dish, instead of None? It would be a lot easier, unless there is some other reason for it.
EDIT 2:
If you need to update your attributes in the way that you have given, you would need to use the setattr method
class breakfast():
def __init__(self, eggs=None, bacon=None, spam=None):
self.eggs = eggs
self.bacon = bacon
self.spam = spam
# Create a list of attributes. This will return [self, eggs,bacon,spam]
attributes = list(locals().keys())
attributes.remove('self') # Remove self from the list
updateItems = []
for i in attributes:
# Check if the attribute is None or not
if getattr(self, i) == None:
updateItems.append(i)
else:
pass
for i in updateItems:
setattr(self, i, 0) # Set the attributes that are None to 0

Iteration over a list in a Pandas DataFrame column

I have a dataframe df as this one:
my_list
Index
0 [81310, 81800]
1 [82160]
2 [75001, 75002, 75003, 75004, 75005, 75006, 750...
3 [95190]
4 [38170, 38180]
5 [95240]
6 [71150]
7 [62520]
I have a list named code with at least one element.
code = ['75008', '75015']
I want to create another column in my DataFrame named my_min, containing the minimum absolute difference between each element of the list code and the list from df.my_list.
Here are the commands I tried :
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in df.loc[:, 'my_list'].str[:]])
>>> TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
#or
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in df.loc[:, 'my_list']])
>>> TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
#or
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in df.loc[:, 'my_list'].tolist()])
>>> TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
#or
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in z for z in df.loc[:, 'my_list'].str[:]])
>>> UnboundLocalError: local variable 'z' referenced before assignment
#or
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in z for z in df.loc[:, 'my_list']])
>>> UnboundLocalError: local variable 'z' referenced before assignment
#or
df.loc[:, 'my_list'] = min([abs(int(x)-int(y)) for x in code for y in z for z in df.loc[:, 'my_list'].tolist()])
>>> UnboundLocalError: local variable 'z' referenced before assignment
you could do this with a list comprehension:
import pandas as pd
import numpy as np
df = pd.DataFrame({'my_list':[[81310, 81800],[82160]]})
code = ['75008', '75015']
pd.DataFrame({'my_min':[min([abs(int(i) - j) for i in code for j in x])
for x in df.my_list]})
returns
my_min
0 6295
1 7145
You could also use pd.Series.apply instead of the outer list, for example:
df.my_list.apply(lambda x: min([abs(int(i) - j) for i in code for j in x]) )
Write a helper: def find_min(lst): -- it is clear you know how to do that. The helper will consult a global named code.
Then apply it:
df['my_min'] = df.my_list.apply(find_min)
The advantage of breaking out a helper
is you can write separate unit tests for it.
If you prefer to avoid globals,
you will find partial quite helpful.
https://docs.python.org/3/library/functools.html#functools.partial
If you have pandas 0.25+ you can use explode and combine with np.min:
# sample data
df = pd.DataFrame({'my_list':
[[81310, 81800], [82160], [75001,75002]]})
code = ['75008', '75015']
# concatenate the lists into one series
s = df.my_list.explode()
# convert `code` into np.array
code = np.array(code, dtype=int)
# this is the output series
pd.Series(np.min(np.abs(s.values[:,None] - code),axis=1),
index=s.index).min(level=0)
Output:
0 6295
1 7145
2 6
dtype: int64

Why is an assignment of a list at [1:1] only possible with an iterable?

a = [1,2]
a[1:1] = 3
raises the Exception:
TypeError: can only assign an iterable
If I change the variable to an iterable, no exception is raised.
a = ['a','c']
a[1:1] = 'b'
Why does the assignment of the slice [1:1] raise an exception, if the variable is not an iterable?
Because is expecting to match positions, since for him a[1:1] is a slice of size n even if n == 1 it will expect an iterable of size at least 1. For example that is why this works:
>>> a[1:1] = [3]
>>> a
[1, 3, 2]

How to pass a map to a function in Python 3?

I get a map of numbers from user and I want to pass the map to a function. I am able to display the map, but I can't find its length. I understand that in Python 3, maps have no length and they have to be converted in lists, but I had no success with that. I also noticed that if I attempt to display the length of the map before calling function info, then the info() will print an empty map.
def info(phys):
print("phys =",list(phys)) # it displays correctly only if I comment line 10
print("len =",len(list(phys))) # it always displays 0 and I expect 3
for i in phys:
print(str(i))
return
phys = map(int, input().strip().split()) # I pass "1 2 3"
print("len(phys) =",len(list(phys))) # if this command executes before next, line 2 will print "phys = []"
info(phys)
The result of a map() call is a generator which will yield resulting values only once. See relevant documentation about map.
>>> phys = map(int, input().strip().split())
1 2 3 4
>>> list(phys)
[1, 2, 3, 4]
>>> list(phys)
[] # Second attempt to iterate through "phys" does not return anything anymore
Apparently you want to materialize the values and work with them later. Then store them:
>>> phys = map(int, input().strip().split())
1 2 3 4
>>> result = list(phys)
>>> len(result)
4
>>> result[1]
2
>>> result[-2:]
[3, 4]

Resources