Recursive class | can you modify self directly?

R

Russel Walker

Sorry for the vague title. Probably best to just show you the code that explains it better.

This is a simplified example of what I want to do:


# THIS DOESN'T WORK
from random import choice

class Expr(object):
"""
Expr(expr, op, val) -> an expression object.
"""

def __init__(self, expr, op='', val=''):
self.expr = expr # can be another instance of Expression.
self.op = op
self.val = val

def __str__(self):
return ("%s %s %s" % (self.expr, self.op, self.val)).strip()

def expand(self):
self = Expr(self, choice('+-*/'), choice('12345'))


Then I tried messing around with Expr.__new__() and Expr.__init__() but that didn't work.

The only way I've got it to work is by doing the expanding part outside of the object, by a method of some other class. But I want the Expr class to be responsible for itself. How can I do this and why doesn't the above work?
 
I

Ian Kelly

Sorry for the vague title. Probably best to just show you the code that explains it better.

This is a simplified example of what I want to do:


# THIS DOESN'T WORK
from random import choice

class Expr(object):
"""
Expr(expr, op, val) -> an expression object.
"""

def __init__(self, expr, op='', val=''):
self.expr = expr # can be another instance of Expression.
self.op = op
self.val = val

def __str__(self):
return ("%s %s %s" % (self.expr, self.op, self.val)).strip()

def expand(self):
self = Expr(self, choice('+-*/'), choice('12345'))

"self" is just a local binding of the object to the name self. You
can rebind the name like this as with any other local variable, but
that's all it does. It doesn't modify the object in any way, and no
other bindings of the same object are affected.

If you actually want to modify the current object, you would need to
do something like:

def expand(self):
import copy
self.expr = Expr(self.expr, self.op, self.val)
self.op = choice('+-*/')
self.val = choice('12345')
 
E

Ethan Furman

This is a simplified example of what I want to do:


# THIS DOESN'T WORK
from random import choice

class Expr(object):
"""
Expr(expr, op, val) -> an expression object.
"""

def __init__(self, expr, op='', val=''):
self.expr = expr # can be another instance of Expression.
self.op = op
self.val = val

def __str__(self):
return ("%s %s %s" % (self.expr, self.op, self.val)).strip()

def expand(self):
self = Expr(self, choice('+-*/'), choice('12345'))

`self` is just a name. In `expand()` you are rebinding the name `self` away from the object and to a new Expr instance.
If you want to change `self` the original object you have to do something like:

def expand(self):
self.op = choice('+-*/')
self.val = choice('12345')
 
I

Ian Kelly

If you actually want to modify the current object, you would need to
do something like:

def expand(self):
import copy
self.expr = Expr(self.expr, self.op, self.val)
self.op = choice('+-*/')
self.val = choice('12345')

Minus the "import copy". I modified the code before sending and
forgot to remove that.
 
D

Dave Angel

Sorry for the vague title. Probably best to just show you the code that explains it better.

This is a simplified example of what I want to do:


# THIS DOESN'T WORK
from random import choice

class Expr(object):
"""
Expr(expr, op, val) -> an expression object.
"""

def __init__(self, expr, op='', val=''):
self.expr = expr # can be another instance of Expression.
self.op = op
self.val = val

def __str__(self):
return ("%s %s %s" % (self.expr, self.op, self.val)).strip()

def expand(self):
self = Expr(self, choice('+-*/'), choice('12345'))


Then I tried messing around with Expr.__new__() and Expr.__init__() but that didn't work.

The only way I've got it to work is by doing the expanding part outside of the object, by a method of some other class. But I want the Expr class to be responsible for itself. How can I do this and why doesn't the above work?

That line in method expand() indicates that you have a fundamental
misunderstanding of the way that names and objects interact in Python.
Names are bound to objects by the assignment operator, but the object
itself is not changed. If you want to change the object that self is
bound to, then change it by mutating it.

You don't change an existing object by binding a name to a new object,
whether of the same or different type. The original object remains
unchanged. The fact that the name is 'self' is totally irrelevant.

a = 45
b = a
a = 12

This does NOT change b.



No idea what you really wanted, but perhaps it's something like:
def expand(self):
self.op = choice("+-*/")
self.val = choice("12345")

No idea what expr is supposed to represent, so I didn't try to
manipulate it here.
 
R

Russel Walker

I didn't do a good job of explaining it cos I didn't want it to be a TLDR; but I could've added a little more. To clarify:

Expr is just a way to represent simple arithmetic expressions for a calculator. Because the expression has to be modified and built over time, and evaluated from left to right and not by operator precedence, I thought it would be simpler to do it this way.

Say the calculation in the calculator is currently at:
1 + 2 * 3

Now lets expand it out to see how it gets to this point.

dial 1: (1) >>> x = Expr(1) # the base case
dial +: ((1) + ) >>> x = Expr(x, '+')
dial 2: ((1) + 2) >>> x.val = 2
dial *: (((1) + 2) + ) >>> x = Expr(x, '+')
dial 3: (((1) + 2) + 3) >>> x.val = 3

I think it's called 'binary tree' but not entirely sure if that's correct.


So I think understand what you guys are saying now.

There is the name x and the class instance (the object) which exists somewhere in memory that x points to. self is just another name that points to the same object (not self in general but the argument passed to the self parameter when a method is called). However if the code inside the method reassigns self to some other object, it doesn't change the fact that x still refers to the original object. So self is just a local variable (an argument).The name self has no relevance to to the name x other than the fact that they point to the same object. So reassigning self has no effect on x. But modifying the object that self points to, does affect x because it points tothe same object. Is this correct?


So when you call x.somemethod() it's not passing x as the self argument, it's actually passing the object that x points to as the self argument. And that object has know knowledge of the fact that x points to it, or does it?
 
T

Terry Reedy

There is the name x and the class instance (the object) which exists
somewhere in memory that x points to. self is just another name that
points to the same object (not self in general but the argument
passed to the self parameter when a method is called). However if the
code inside the method reassigns self to some other object, it
doesn't change the fact that x still refers to the original object.
So self is just a local variable (an argument).

Yes, parameters *names* are the initial local names of the function.
Calling the first parameter of instancemethods 'self' is a convention,
not a requirement.
The name self has no
relevance to to the name x other than the fact that they point to the
same object. So reassigning self has no effect on x. But modifying
the object that self points to, does affect x because it points to
the same object. Is this correct?

Yes. Multiple names for one objects are 'aliases'. Being able to modify
a object with multiple names in different namespaces is both a boon and
bug bait.
So when you call x.somemethod() it's not passing x as the self

Python does not pass'x': it is 'call-by-object', not 'call-by-name'.
argument, it's actually passing the object that x points to as the
self argument. And that object has know knowledge of the fact that x
points to it, or does it?

Some objects (modules, classes, functions) have definition names
(.__name__ attributes) that are used in their representations (as in
tracebacks). But they have no knowledge of their namespace names.
 
R

Russel Walker

Yes, parameters *names* are the initial local names of the function.

Calling the first parameter of instancemethods 'self' is a convention,

not a requirement.










Yes. Multiple names for one objects are 'aliases'. Being able to modify

a object with multiple names in different namespaces is both a boon and

bug bait.






Python does not pass'x': it is 'call-by-object', not 'call-by-name'.








Some objects (modules, classes, functions) have definition names

(.__name__ attributes) that are used in their representations (as in

tracebacks). But they have no knowledge of their namespace names.

Thanks Terry, I did read all that twice just to make sure it sunk in and it was really clear and helpful.
 
R

Russel Walker

I've been mucking around with this silly class pretty much the whole day and my eyes are about closing now so this is the solution for now I think. Please feel free to drop any suggestions. I think I mostly just ended up shaving off allot of extraneous responsibility for the class, that and inheriting from list. I wanted to try it and it seems to fit (I think).

Maybe tomorrow I'll try it inheriting from instead since that is immutable.Should've probably thought of that before right this moment, as I had quite a time with "maximum recursion depth exceeded".


class LRExpression(list):
"""
Construct for 'left to right' algebraic expressions.
"""

def __init__(self, *args):
list.__init__(self, args[:1])
for x in args[1:]:
self.append(x)

def __str__(self):
return '(%s)' % ' '.join(map(str, self))

def evaluate(self):
return eval(str(self))

def append(self, x):
if len(self) < 3:
list.append(self, x)
else:
oldself = LRExpression(*self)
self.__init__(oldself)
self.append(x)


def test():
a = LRExpression(1)
a.append('+')
a.append(2)
a.append('*')
a.append(3)
try:
print 'repr:', repr(a)
print 'str:', a
print "should be 9:", a.evaluate()
except Exception as er:
print er
 
I

Ian Kelly

def append(self, x):
if len(self) < 3:
list.append(self, x)
else:
oldself = LRExpression(*self)
self.__init__(oldself)
self.append(x)

It's probably not wise to be re-calling __init__ after the class has
been initialized, although it seems to work. I would instead use
slice assignment to replace the list contents:

self[:] = [oldself]

That said, the problem with inheriting from list is that it allows
your class to be used in a lot of ways lists can be used that are
probably not appropriate. For example, continuing on from your test
function:
3

Why 3? Because that's the length of the list. But the caller would
probably expect the length to have something to do with the size of
the expression, not the list.
False

Again, this happens because the "in" operator is only looking at the
list itself, not at sublists. But an expression container would
probably be expected to return True for both of those. These cases
can of course be easily overridden in the subclass, but what about
something harder, like slicing? How should that be expected to work
with an LRExpression?

This isn't to say that you shouldn't necessarily use a list if it
helps you implement the behavior you want, but having the LRExpression
*be* a list is probably wrong.


Finally, based on what you're doing here, I think you would be better
off using something like the OOP Builder pattern. There will be two
distinct classes: the "LRExpression" class that composes a tree
structure, and an "ExpressionBuilder" class that does the work of
actual assembly. It could look something like this:

class LRExpression(object):
def __init__(self, expr, op, value):
self.expr = expr
self.op = op
self.value = value
def __str__(self):
return '(%s %s %s)' % (self.expr, self.op, self.value)
def evaluate(self):
# Subject to the usual warning that eval() should
# never be used with untrusted input
return eval(str(self))

class ExpressionBuilder(object):
def __init__(self):
self.parts = []
def append(self, part):
self.parts.append(part)
def build(self):
expr = self.parts[0]
for i in xrange(1, len(self.parts), 2):
op, value = self.parts[i:i+2]
expr = LRExpression(expr, op, value)
return expr

def test():
a = ExpressionBuilder()
a.append(1)
a.append('+')
a.append(2)
a.append('*')
a.append(3)
expr = a.build()
print expr
print expr.evaluate()
((1 + 2) * 3)
9
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,053
Latest member
BrodieSola

Latest Threads

Top