The Question
Is there a straightforward algorithm for figuring out if a variable is "used" within a given scope?
In a Python AST, I want to remove all assignments to variables that are not otherwise used anywhere, within a given scope.
Details
Motivating example
In the following code, it is obvious to me (a human), that _hy_anon_var_1 is unused, and therefore the _hy_anon_var_1 = None statements can be removed without changing the result:
# Before
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
_hy_anon_var_1 = None
else:
n = 3 * n + 1
_hy_anon_var_1 = None
yield n
# After
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
else:
n = 3 * n + 1
yield n
Bonus version
Extend this to []-lookups with string literals as keys.
In this example, I would expect _hyx_letXUffffX25['x'] to be eliminated as unused, because _hyx_letXUffffX25 is local to h, so _hyx_letXUffffX25['x'] is essentially the same thing as a local variable. I would then expect _hyx_letXUffffX25 itself to be eliminated once there are no more references to it.
# Before
def h():
_hyx_letXUffffX25 = {}
_hyx_letXUffffX25['x'] = 5
return 3
# After
def h():
return 3
From what I can tell, this is somewhat of an edge case, and I think the basic algorithmic problem is the same.
Definition of "used"
Assume that no dynamic name lookups are used in the code.
A name is used if any of these are true in a given scope:
It is referenced anywhere in an expression. Examples include: an expression in a return statement, an expression on the right-hand side of an assignment statement, a default argument in a function definition, being referenced inside a local function definition, etc.
It is referenced on the left-hand side of an "augmented assignment" statement, i.e. it is an augtarget therein. This might represent "useless work" in a lot of programs, but for the purpose of this task that's OK and distinct from being an entirely unused name.
It is nonlocal or global. These might be useless nonlocals or globals, but because they reach beyond the given scope, it is OK for my purposes to assume that they are "used".
Please let me know in the comments if this seems incorrect, or if you think I am missing something.
Examples of "used" and "unused"
Example 1: unused
Variable i in f is unused:
def f():
i = 0
return 5
Example 2: unused
Variable x in f is unused:
def f():
def g(x):
return x/5
x = 10
return g(100)
The name x does appear in g, but the variable x in g is local to g. It shadows the variable x created in f, but the two x names are not the same variable.
Variation
If g has no parameter x, then x is in fact used:
def f():
x = 10
def g():
return x/5
return g(100)
Example 3: used
Variable i in f is used:
def f():
i = 0
return i
Example 4: used
Variable accum in silly_map and silly_sum is used in both examples:
def silly_map(func, data):
data = iter(data)
accum = []
def _impl():
try:
value = next(data)
except StopIteration:
return accum
else:
accum.append(value)
return _impl()
return _impl()
def silly_any(func, data):
data = iter(data)
accum = False
def _impl():
nonlocal accum, data
try:
value = next(data)
except StopIteration:
return accum
else:
if value:
data = []
accum = True
else:
return _impl()
return _impl()
The solution below works in two parts. First, the syntax tree of the source is traversed and all unused target assignment statements are discovered. Second, the tree is traversed again via a custom ast.NodeTransformer class, which removes these offending assignment statements. The process is repeated until all unused assignment statements are removed. Once this is finished, the final source is written out.
The ast traverser class:
import ast, itertools, collections as cl
class AssgnCheck:
def __init__(self, scopes = None):
self.scopes = scopes or cl.defaultdict(list)
#classmethod
def eq_ast(cls, a1, a2):
#check that two `ast`s are the same
if type(a1) != type(a2):
return False
if isinstance(a1, list):
return all(cls.eq_ast(*i) for i in itertools.zip_longest(a1, a2))
if not isinstance(a1, ast.AST):
return a1 == a2
return all(cls.eq_ast(getattr(a1, i, None), getattr(a2, i, None))
for i in set(a1._fields)|set(a2._fields) if i != 'ctx')
def check_exist(self, t_ast, s_path):
#traverse the scope stack and remove scope assignments that are discovered in the `ast`
s_scopes = []
for _ast in t_ast:
for sid in s_path[::-1]:
s_scopes.extend(found:=[b for _, b in self.scopes[sid] if AssgnCheck.eq_ast(_ast, b) and \
all(not AssgnCheck.eq_ast(j, b) for j in s_scopes)])
self.scopes[sid] = [(a, b) for a, b in self.scopes[sid] if b not in found]
def traverse(self, _ast, s_path = [1]):
#walk the ast object itself
_t_ast = None
if isinstance(_ast, ast.Assign): #if assignment statement, add ast object to current scope
self.traverse(_ast.targets[0], s_path)
self.scopes[s_path[-1]].append((True, _ast.targets[0]))
_ast = _ast.value
if isinstance(_ast, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
s_path = [*s_path, (nid:=(1 if not self.scopes else max(self.scopes)+1))]
if isinstance(_ast, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.scopes[nid].extend([(False, ast.Name(i.arg)) for i in _ast.args.args])
_t_ast = [*_ast.args.defaults, *_ast.body]
self.check_exist(_t_ast if _t_ast is not None else [_ast], s_path) #determine if any assignment statement targets have previously defined names
if _t_ast is None:
for _b in _ast._fields:
if isinstance((b:=getattr(_ast, _b)), list):
for i in b:
self.traverse(i, s_path)
elif isinstance(b, ast.AST):
self.traverse(b, s_path)
else:
for _ast in _t_ast:
self.traverse(_ast, s_path)
Putting it all together:
class Visit(ast.NodeTransformer):
def __init__(self, asgn):
super().__init__()
self.asgn = asgn
def visit_Assign(self, node):
#remove assignment nodes marked as unused
if any(node.targets[0] == i for i in self.asgn):
return None
return node
def remove_assgn(f_name):
tree = ast.parse(open(f_name).read())
while True:
r = AssgnCheck()
r.traverse(tree)
if not (k:=[j for b in r.scopes.values() for k, j in b if k]):
break
v = Visit(k)
tree = v.visit(tree)
return ast.unparse(tree)
print(remove_assgn('test_name_assign.py'))
Output Samples
Contents of test_name_assign.py:
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
_hy_anon_var_1 = None
else:
n = 3 * n + 1
_hy_anon_var_1 = None
yield n
Output:
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
else:
n = 3 * n + 1
yield n
Contents of test_name_assign.py:
def h():
_hyx_letXUffffX25 = {}
_hyx_letXUffffX25['x'] = 5
return 3
Output:
def h():
return 3
Contents of test_name_assign.py:
def f():
i = 0
return 5
Output:
def f():
return 5
Contents of test_name_assign.py:
def f():
x = 10
def g():
return x/5
return g(100)
Ouptut:
def f():
x = 10
def g():
return x / 5
return g(100)
Related
I was trying to solve a codewars problem here, and I got a bit stuck. I believe I should be using nested currying in Python.
Let us just take the case of add. Let us constrain the problem even more, and just get nested add working on the right hand side, i.e. write an add function such that
print((add)(3)(add)(5)(4))
prints 12.
It should be possible to nest it as deep as required, for e.g. I want
print((add)(add)(3)(4)(add)(5)(6))
should give me 18.
What I have done so far -
My initial attempt is to use the following nested function -
def add_helper():
current_sum = 0
def inner(inp):
if isinstance(inp, int):
nonlocal current_sum
current_sum += inp
print(f"current_sum = {current_sum}")
return inner
return inner
add = add_helper()
However, this does not do the trick. Instead, I get the following output, for when I do something like print((add)(add)(3)(4)(add)(5)(6))
current_sum = 3
current_sum = 7
current_sum = 12
current_sum = 18
<function add_helper.<locals>.inner at 0x...>
Does anyone know how I have to change my function so that I just return 18, because the function will know it is "done"?
Any help will be appreciated!
UPDATE
After looking at Bharel's comments, I have the following so far -
def add_helper():
val = 0
ops_so_far = []
def inner(inp):
if isinstance(inp, int):
nonlocal val
val += inp
return inner
else:
ops_so_far.append(("+", val))
inp.set_ops_so_far(ops_so_far)
return inp
def set_ops_so_far(inp_list):
nonlocal ops_so_far
ops_so_far = inp_list
def get_val():
nonlocal val
return val
def get_ops_so_far():
nonlocal ops_so_far
return ops_so_far
inner.get_ops_so_far = get_ops_so_far
inner.set_ops_so_far = set_ops_so_far
inner.get_val = get_val
return inner
def mul_helper():
val = 1
ops_so_far = []
def inner(inp):
if isinstance(inp, int):
nonlocal val
val *= inp
return inner
else:
ops_so_far.append(("*", val))
inp.set_ops_so_far(ops_so_far)
return inp
def get_ops_so_far():
nonlocal ops_so_far
return ops_so_far
def set_ops_so_far(inp_list):
nonlocal ops_so_far
ops_so_far = inp_list
def get_val():
nonlocal val
return val
inner.get_ops_so_far = get_ops_so_far
inner.get_val = get_val
inner.set_ops_so_far = set_ops_so_far
return inner
add = add_helper()
mul = mul_helper()
and now when I do
res = (add)(add)(3)(4)(mul)(5)(6)
print(res.get_ops_so_far())
print(res.get_val())
I get
[('+', 0), ('+', 7)]
30
Still not sure if this is the correct direction to be following?
This is how I solved it for anyone still looking in the future -
from copy import deepcopy
def start(arg):
def start_evalutaion(_arg, eval_stack, variables):
new_eval_stack = deepcopy(eval_stack)
new_variables = deepcopy(variables)
to_ret = evaluate_stack(_arg, new_eval_stack, new_variables)
if to_ret is not None:
return to_ret
def inner(inner_arg):
return start_evalutaion(
inner_arg, new_eval_stack, new_variables
)
return inner
return start_evalutaion(arg, [], dict())
add = lambda a, b, variables: variables.get(a, a) + variables.get(b, b)
sub = lambda a, b, variables: variables.get(a, a) - variables.get(b, b)
mul = lambda a, b, variables: variables.get(a, a) * variables.get(b, b)
div = lambda a, b, variables: variables.get(a, a) // variables.get(b, b)
def let(name, val, variables):
variables[name] = val
return
def return_(val, variables):
return variables.get(val, val)
def evaluate_stack(_arg, eval_stack, variables):
if callable(_arg):
if _arg.__name__ == "return_":
req_args = 1
else:
req_args = 2
eval_stack.append((_arg, req_args, []))
else:
while True:
func_to_eval, req_args, args_so_far = eval_stack[-1]
args_so_far.append(_arg)
if len(args_so_far) == req_args:
eval_stack.pop()
_arg = func_to_eval(*args_so_far, variables)
if func_to_eval.__name__ == "return_":
return _arg
elif _arg is None:
break
else:
break
Passes all testcases
I have a class and some functions. In the 'check_reflexive()' function, there is a variable called reflexive_list. I want to use this variable also in the 'antisymmetric' function.
I checked some examples about class but didn't find a specific example to solve this problem.
I'll be waiting for your advice. Hope you have a nice day
class MyClass():
def __init__(self):
def checkif_pair(k):
for a in k:
if a%2 == 0:
None
else:
return False
return True
def check_reflexive(k):
j = 0
z = 0
reflexive_list = []
while j < len(k):
i = 0
while i < len(k):
if k[i] == k[j]:
tup = k[j],k[i]
reflexive_list.append(tup)
i += 1
else:
None
j = j + 1
else:
None
print(reflexive_list)
if len(reflexive_list) == len(self.list1):
return True
else:
return False
def antisymmetric(k):
antisymettric_list = []
for b in k:
swap1 = b[0]
swap2 = b[1]
newtuple = (swap2, swap1)
antisymettric_list.append(newtuple)
for ü in reflexive_list:
if ü in antisymettric_list:
antisymettric_list.remove(ü)
else:
None
print(antisymettric_list)
for q in antisymettric_list:
if q in k:
print("The system is not Anti-Symmetric.")
break
print("The system is Anti-Symmetric.")
def transitive(k):
result = {}
for first, second in k:
result.setdefault(first, []).append(second)
print(result)
for a, b in k:
for x in result[b]:
if x in result[a]:
None
else:
print("There is no {} in the {}".format(x, result[a]))
return False
return True
You can just use reflexive_list as an instance variable. Just add a constructor where the variable is defined:
class MyClass():
def __init__(self):
self.reflexive_list = []
And everytime you want to use it inside the function, you use self.reflexive_list
NOTE: This post is heavy edited. I found a solution.
My question was, how I can apply all possible combinations of a truth table for an arbitary number of variables to a dictionary. E.g. for 3 vars I have a dictionary like 'lib = {"x":0, "y":0, "z":0}' I wanted to generate all 8 possible combinations.
I need the n-tuples in this format to evaluate a given expression.
Because some wanted to know why I needed it. I was working on an exercise where I created a class with subclasses to handle boolean expressions.
While brooding over the problem I looked over the previous chapters in the exercise book and realized, that I might solve it with recursion.
For those interested here is my implementation and with my solution:
# classes to handle boolean expressions
class ExprBoolean:
def __str__(self): # necessary for considering precedence: not > and > or > equal
return self.str_aux(0)
def init_Lib_Keys(self): # if not existent, generate class var: lib - dictionary with all
try: # vars, keys - list with all vars, tt - empty list for truth table
self.lib
except:
self.lib = self.getVar({})
self.keys = list(self.lib.keys())
self.tt = []
# ---- Solution I was looking for --------------------------------
def TruthTable(self,keys): # generates truth table considering all vars and entered expression
if keys == []: # condition to insert n-tupel as row in lib-string
dummy = []
for key in self.lib:
dummy += [self.lib[key]] + ["\t| "]
dummy += [self.eval(self.lib)] + ["\n"]
self.tt += [dummy]
return
for keyN in range(len(keys)):
for i in [True,False]:
self.lib[keys[keyN]] = i
self.TruthTable(keys[1:])
break
# ----------------------------------------------------------------
def make_tt(self): # prints a truth table
self.init_Lib_Keys()
for key in self.keys:
print("{}\t\t| ".format(key),end="") # header of table with
print(self) # expression in last column
if not self.tt:
self.TruthTable(self.keys) # generates truth table
for row in self.tt: # prints row after row
for col in row:
print(col,end="")
def isTauto(self): # checks if expression is tautology (always True)
self.init_Lib_Keys()
if not self.tt:
self.TruthTable(self.keys)
for row in self.tt:
if not row[-2]: # in row[-2] is evaluated expression
print(row[-2])
return
print(True)
class Not(ExprBoolean):
prec = 3 # precedence rank for brackets
def __init__(self,arg):
self.arg = arg
def str_aux(self,prec):
return "!" + self.arg.str_aux(self.prec)
def getVar(self,env):
return self.arg.getVar(env)
def eval(self,env):
return not self.arg.eval(env)
class BooOp(ExprBoolean):
def __init__(self,x,y):
self.x = x
self.y = y
def str_aux(self,prec):
s = self.x.str_aux(self.prec) + self.op + self.y.str_aux(self.prec)
if self.prec < prec:
return "(" + s + ")"
else:
return s
def getVar(self,env):
new_env = self.x.getVar(env)
new_env = self.y.getVar(new_env)
return new_env
def eval(self,env):
return self.fun(self.x.eval(env),self.y.eval(env))
class And(BooOp):
prec = 2
op = "&"
def fun(self,x,y):
return x & y
class Or(BooOp):
prec = 1
op = "|"
def fun(self,x,y):
return x | y
class Eq(BooOp):
prec = 0
op = "=="
def fun(self,x,y):
return x == y
class Var(ExprBoolean):
def __init__(self,name):
self.name = name
def str_aux(self,prec):
return self.name
def getVar(self,env):
env[self.name] = 0
return env
def eval(self,env):
return env[self.name]
# examples to test class
lib = {"x":True, "y":False, "z":True}
e1 = Or(Var("x"),Not(Var("x")))
e2 = Eq(Var("x"),Not(Not(Var("x"))))
e3 = Eq(Not(And(Var("x"),Var("y"))),Or(Not(Var("x")),Not(Var("y"))))
e4 = Eq(Not(And(Var("x"),Var("y"))),And(Not(Var("x")),Not(Var("y"))))
e5 = Eq(Eq(Eq(Var("p"),Var("q")),Var("r")),Eq(Var("p"),Eq(Var("q"),Var("r"))))
e6 = And(Or(Var("x"),Var("y")),Eq(Var("x"),Var("y")))
e4.make_tt()
e4.isTauto()
If I understand the problem, this is one way to help solve it without using eval() or having to parse the expression yourself. (You will have to implement the other Boolean functions yourself.)
def AND(a, b):
return a and b
def OR(a, b):
return a or b
def NOT(a):
return not a
x = True
y = False
z = True
print(AND(x, y))
# False
print(NOT(AND(x, y)))
# True
print(OR(x, y))
# True
print(AND(AND(x, z), NOT(y)))
# True
Even though python has classes, that doesn't mean you should treat every problem as an invitation to write a class hierarchy.
Evaluating expression is a verb, not a noun.
First off, you don't always have to write an expression evaluator. Python has eval and exec, and boolean and arithmetic expressions in Python are pretty much standard:
>>> eval("3+5")
8
>>> eval("x=3+5")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
x=3+5
^
SyntaxError: invalid syntax
>>> exec("x=3+5")
>>> x
8
If that is enough, look at the ast module in the standard library.
Even if you want to write it yourself, programming is often more about creating smart data that writing lots of code.
Below is a small implementation of a postfix expression parser (from one of my github repos).
import operator
import math
# Global constants {{{1
_add, _sub, _mul = operator.add, operator.sub, operator.mul
_truediv, _pow, _sqrt = operator.truediv, operator.pow, math.sqrt
_sin, _cos, _tan, _radians = math.sin, math.cos, math.tan, math.radians
_asin, _acos, _atan = math.asin, math.acos, math.atan
_degrees, _log, _log10 = math.degrees, math.log, math.log10
_e, _pi = math.e, math.pi
_ops = {
"+": (2, _add),
"-": (2, _sub),
"*": (2, _mul),
"/": (2, _truediv),
"**": (2, _pow),
"sin": (1, _sin),
"cos": (1, _cos),
"tan": (1, _tan),
"asin": (1, _asin),
"acos": (1, _acos),
"atan": (1, _atan),
"sqrt": (1, _sqrt),
"rad": (1, _radians),
"deg": (1, _degrees),
"ln": (1, _log),
"log": (1, _log10),
}
_okeys = tuple(_ops.keys())
_consts = {"e": _e, "pi": _pi}
_ckeys = tuple(_consts.keys())
def postfix(expression): # {{{1
"""
Evaluate a postfix expression.
Arguments:
expression: The expression to evaluate. Should be a string or a
sequence of strings. In a string numbers and operators
should be separated by whitespace
Returns:
The result of the expression.
"""
if isinstance(expression, str):
expression = expression.split()
stack = []
for val in expression:
if val in _okeys:
n, op = _ops[val]
if n > len(stack):
raise ValueError("not enough data on the stack")
args = stack[-n:]
stack[-n:] = [op(*args)]
elif val in _ckeys:
stack.append(_consts[val])
else:
stack.append(float(val))
return stack[-1]
The "intelligence" of this is in the _ops dictionary, which links an operator to a two-tuple of a number of arguments and an operator function.
Because of that, the evaluator itself is only 14 lines of code.
Basically, if I were to write a function with variable return elements, like so:
def func(elem1=True, elem2=True, elem3=True, elem4=False):
x = MyClass()
ret = []
if elem1:
ret.extend([x.func1()])
if elem2:
ret.extend([x.obj1])
if elem3:
ret.extend([x.func2().attr1])
if elem4:
ret.extend(x.list_obj3)
return ret
Things get rather long and windy. Is it possible to do something like this perhaps:
def func(elem1=True, elem2=True, elem3=True, elem4=False):
x = MyClass()
return [x.func1() if elem1,
x.obj1 if elem2,
x.func2().attr1 if elem3,
x.list_obj3 if elem4]
How neat is that!?
I know this can be done:
def func(elem1=True, elem2=True, elem3=True, elem4=False):
x = MyClass()
ret = [x.func1(), x.obj1, x.func2().attr1, x.list_obj3]
choices = [elem1, elem2, elem3, elem4]
return [r for i, r in enumerate(ret) if choices[i]]
but I would like to not calculate the elements if the user does not want them; it is a little expensive to calculate some of them.
If you hide your operations in lambdas then you can use lazy evaluation:
def func(elem1=True, elem2=True, elem3=True, elem4=False):
x = MyClass()
return [L() for inc,L in (
(elem1, lambda: x.func1()),
(elem2, lambda: x.obj1),
(elem3, lambda: x.func2().attr1),
(elem4, lambda: x.list_obj3),
) if inc]
Asking a slightly different question, can you get behaviour like matlab/octave, where you only calculate the first two results if you are assigning to two variables, without computing results 3 and 4?
For example:
a, b = func()
Python can't quite do it since func() doesn't know how many return values it wants, but you can get close using:
from itertools import islice
def func():
x = MyClass()
yield x.fun c1()
yield x.obj1
yield x.func2().attr1
yield x.list_obj3
a, b = islice(func(), 2)
I'm not sure it is better, but you could add array indexing semantics using a decorator, which would allow you to write:
#sliceable
def func():
...
a, b = func()[:2]
This is easy enough to implement:
from itertools import islice
class SlicedIterator(object):
def __init__(self, it):
self.it = it
def __iter__(self):
return self.it
def __getitem__(self, idx):
if not isinstance(idx, slice):
for _ in range(idx): next(self.it)
return next(self.it)
return list(islice(self.it, idx.start, idx.stop, idx.step))
def sliceable(f):
def wraps(*args, **kw):
return SlicedIterator(f(*args, **kw))
return wraps
Testing:
#sliceable
def f():
print("compute 1")
yield 1
print("compute 2")
yield 2
print("compute 3")
yield 3
print("compute 4")
yield 4
print("== compute all four")
a, b, c, d = f()
print("== compute first two")
a, b = f()[:2]
print("== compute one only")
a = f()[0]
print("== all as a list")
a = f()[:]
gives:
== compute all four
compute 1
compute 2
compute 3
compute 4
== compute first two
compute 1
compute 2
== compute one only
compute 1
== all as a list
compute 1
compute 2
compute 3
compute 4
I am writing a vector class in python (just to see if i can). i ran into a problem with the subtract method and i have no idea what could be causing this.
this is the class (i omitted "class Vector:").
def __init__(self, p):
print self
self.p = p
def __str__(self):
return str(list(self.p))
def equals(self, v):
if type(self) == type(v):
return str(self) == str(v)
return false
def size(self):
return len(self.p)
def add(self, v):
a = self.p
b = v.p
if self.size() == v.size():
for i in range(0, self.size()):
a[i] += b[i]
return Vector(a)
raise Exception()
def subtract(self, v):
a = self.p
b = v.p
if self.size() == v.size():
for i in range(0, self.size()):
a[i] -= b[i]
return Vector(a)
raise Exception()
def dot(self, v):
total = 0
if self.size() == v.size():
for i in range(0, len(self.p)):
total += self.p[i] * v.p[i]
return total
raise Exception()
def norm(self):
total = 1
if self.size() == v.size():
for i in range(0, len(self.p)):
total += self.p[i]^2
return total
raise Exception()
when i try to do:
a = Vector([1,1])
a.subtract(Vector[1,1])
print a
my thought says i should get [1,1] as output because i do not change any values of Vector a when i do the subtraction, i return a new vector with the values it should have. when i print the object it shows me that it is in a different space in memory but my output from 'print a' is [0,0]
also if i do
a = Vector(1,1)
b = a
a.subtract(Vector([1,1])
print a,b
my output is [0,0][0,0], what i want is [0,0][1,1]
why does b change with a ?
First question:
(1, 1) - (1, 1) == (0, 0)
The output of your program is correct. You change the values of a in your function with a[i] -= b[i] where a is the list of coordinates (not a copy of the list) in self and b the list of coordinates (again, not a copy) in v.
Second question:
b = a
a and b are now the same object (not different objects with the same value), so they change simultaneously.
Think of a and b as addresses for your computer.
a = Vector(1,1) # There is a Vector object somewhere in memory, e.g. 12345. Then a = 12345
b = a # b = a = 12345 (still the address of the same Vector object)
a.subtract(Vector([1,1])) # Change whatever is stored at the address of a = 12345
print a,b # both are still the address of the same object you modified in the previous step!