views:

140

answers:

5

So as part of problem 17.6 in "Think Like a Computer Scientist", I've written a class called Kangaroo:

class Kangaroo(object):

    def __init__(self, pouch_contents = []):
        self.pouch_contents = pouch_contents

    def __str__(self):
        '''
        >>> kanga = Kangaroo()
        >>> kanga.put_in_pouch('olfactory')
        >>> kanga.put_in_pouch(7)
        >>> kanga.put_in_pouch(8)
        >>> kanga.put_in_pouch(9)
        >>> print kanga
        "In kanga's pouch there is: ['olfactory', 7, 8, 9]"
        '''

        return "In %s's pouch there is: %s" % (object.__str__(self), self.pouch_contents)

    def put_in_pouch(self, other):
        '''
        >>> kanga = Kangaroo()
        >>> kanga.put_in_pouch('olfactory')
        >>> kanga.put_in_pouch(7)
        >>> kanga.put_in_pouch(8)
        >>> kanga.put_in_pouch(9)
        >>> kanga.pouch_contents
        ['olfactory', 7, 8, 9]
        '''

        self.pouch_contents.append(other)

What's driving me nuts is that I'd like to be able to write a string method that would pass the unit test underneath __str__ as written. What I'm getting now instead is:

In <__main__.Kangaroo object at 0x4dd870>'s pouch there is: ['olfactory', 7, 8, 9]

Bascially, what I'm wondering if there is some function that I can perform on kanga = Kangaroo such that the output of the function is those 5 characters, i.e. function(kanga) -> "kanga".

Any ideas?

Edit: Reading the first answer has made me realize that there is a more concise way to ask my original question. Is there a way to rewrite __init__ such that the following code is valid as written?

>>> somename = Kangaroo()
>>> somename.name
'somename'
+6  A: 

To put your request into perspective, please explain what name you would like attached to the object created by this code:

marsupials = []
marsupials.append(Kangaroo()) 

This classic essay by the effbot gives an excellent explanation.

To answer the revised question in your edit: No.

Now that you've come clean in a comment and said that the whole purpose of this naming exercise was to distinguish between objects for debugging purposes associated with your mutable default argument:

In CPython implementations of Python at least, at any given time, all existing objects have a unique ID, which may be obtained by id(obj). This may be sufficient for your debugging purposes. Note that if an object is deleted, that ID (which is a memory address) can be re-used by a subsequently created object.

John Machin
What I'd like it to do in your marsupials example is to just use some default value, say, "roo". I'm not trying to assign a unique identifier to .name, just a string to put in the `__str__` method. I think that qualifies as unambiguous. If that's not possible, well, that makes me kind of sad, but workarounds are pretty obvious, I suppose.
tel
@tel: Read the article I linked to. Let your users decide what name (if any) they want.
John Machin
+2  A: 
class Kangaroo(object):

    def __init__(self, pouch_contents=None, name='roo'):
        if pouch_contents is None:
            self.pouch_contents = []  # this isn't shared with all other instances
        else:
            self.pouch_contents = pouch_contents
        self.name = name
    ...

kanga = Kangaroo(name='kanga')

Note that it's good style not to put spaces around = in the arguments

gnibbler
+1 for presenting a better solution than me yet again.
aaronasterling
This is the right way to do it. I wish I could combine your answer and John Machin's. :-)
Omnifarious
I've altered the way that pouch_contents is initialised after reading bstpierre's comment. This is one less bug for you to find
gnibbler
@gnibbler: @bstpierre didn't read the OP's original pre-edits question text, wherein the OP stated that he knew all about the mutable default arg problem. The OP's reply: "@bstpierre The whole point of the exercise was to examine that very error, so I'm aware. But you're right, that could be nasty". I canvassed this issue in an edit to my answer. Your edit is way off topic.
John Machin
+2  A: 

I wasn't going to post this but if you only want this for debugging then here you go:

import sys

class Kangaroo(object):
    def __str__(self):
        flocals = sys._getframe(1).f_locals
        for ident in flocals:
            if flocals[ident] is self:
                name = ident
                break
        else:   
            name = 'roo'
        return "in {0}'s pouch, there is {1}".format(name, self.pouch_contents)

kang = Kangaroo()
print kang

This is dependent on CPython (AFAIK) and isn't suitable for production code. It wont work if the instance is in any sort of container and may fail for any reason at any time. It should do the trick for you though.

It works by getting the f_locals dictionary out of the stack frame that represents the namespace where print kang is called. The keys of f_locals are the names of the variables in the frame so we just loop through it and test if each entry is self. If so, we break. If break is not executed, then we didn't find an entry and the loops else clause assigns the value 'roo' as requested.

If you want to get it out of a container of some sort, you need to extend this to look through any containers in f_locals. You could either return the key if it's a dictlike container or the index if it's something like a tuple or list.

aaronasterling
Wow, that's amazingly ugly and hackish. I would make it cache name. And I would never let this in production code. But wow, this is impressive in a disturbing way.
Omnifarious
I have used code like this (and even more hacky) for debugging in the past. You need to store the name during `__init__` though, otherwise for example, if a Kangaroo is passed to a function, you'll just get the name the parameter inside the function.
gnibbler
@gnibbler, I couln't figure out how to get the name in the `__init__` function because `kang` isn't entered into `f_locals` when `__init__` is running. Am I missing something?
aaronasterling
I was wondering how to implement frames. Thanks for posting this.
tel
A: 

I hadn't seen aaronasterling's hackish answer when I started working on this, but in any case here's a hackish answer of my own:

class Kangaroo(object):

    def __init__(self, pouch_contents = ()):
        self.pouch_contents = list(pouch_contents)

    def __str__(self):
        if not hasattr(self, 'name'):
            for k, v in globals().iteritems():
                if id(v) == id(self):
                    self.name = k
                    break
                else:
                    self.name = 'roo'                    
        return "In %s's pouch there is: %s" % (self.name, self.pouch_contents)

kanga = Kangaroo()
print kanga

You can break this by looking at it funny (it works as written, but it will fail as part of a doctest), but it works. I'm more concerned with what's possible than with what's practical at this point in my learning experience, so I'm glad to see that there are at least two different ways to do a thing I figured should be possible to do. Even if they're bad ways.

tel
You should really stick an `else` clause an that loop to provide a default in case it doesn't find anything ;)
aaronasterling
A: 

What you want is basically impossible in Python, even with the suggested "hacks". For example, what would the following code print?

>>> kanga1 = kanga2 = kanga3 = Kangaroo()
>>> kanga2.name 
???
>>> kanga3.name
???

or

>>> l = [Kangaroo()]
>>> l[0].name
???

If you want "named" objects, just supply a name to your object

def __init__(self, name):
    self.name = name

More explicit (which we like with Python) and consistent in all cases. Sure you can do something like

>>> foo = Kangaroo("bar")
>>> foo.name
'bar'

but foo will be just one of the possibly many labels the instance has. The name is explicit and permanent. You can even enforce unique naming if you want (while you can reuse a variable as much as you want for different objects)

Ivo van der Wijk
This is for debugging purposes. It's sorta weird to change a class interface for debugging purposes, don't you think? And the first case you gave is undefined it would print 'kanga1' or 'kanga2' or 'kanga3'. the second case would take just slightly more work but you could get it to print 'l-1' for example.
aaronasterling