Search code examples
pythonpass-by-referencepass-by-value

Python NoneType object is not working as intended


I'm trying to pass by object a None value in hopes of reassigning it to an actual value. Can someone please explain why this isn't working? I thought since None is a NoneType that it would be passed in by reference and when I use the passed in variable, I would alter the original None object. Is this incorrect? I have provided a snippet below to demonstrate my case:

class MyClass:
    def __init__(self):
        self.value = None

    def assign(self, object):
        object = OtherClass()

example = MyClass()
example.assign(example.value)

The result is that example.value is still None. Why is that? I have read a some SO posts but none of them are clear in explaining this.

EDIT: Not a duplicate because I wanted to know why it was behaving like pass by value when I thought it should be pass by reference. In the end, I have concluded that NoneType objects are immutable and therefore always pass by value. I would need to use list type object or something mutable.

EDIT again: My example code is just a general case. I was trying to implement a BST and my initial root node was None and when I assigned the root node to be a Node object, it would still be NoneType, which caused me to be confused and was the cause of this question.

Last edit: answer below provides a very good tl;dr summary of pass by object. I was unable to understand just by searching/reading forums.


Solution

  • This is precisely the case where call by object (Python rules) differs from call by reference (C++ reference semantics).

    The difference is that assigning to an unqualified name rebinds the name, it has no effect whatsoever on whatever that name might previously have been bound to (aside from possibly destroying it, if no other references remain). The name has no connection to any other names that might reference the same object, so those other names aren't changed.

    So in this case, object is initially set to point to the same object example.value points to. But on the next line, you rebind object (unqualified name assignment), and it points to a whole different object.

    By contrast, if you had:

    def assign_value(self, object):
        object.value = OtherClass()
    

    and called it with:

    example.assign(example)
    

    then example and object would refer to the same object, and your assignment to a qualified name would replace value on that one object.

    All that said, your use case here doesn't need any such changes. Calling example.assign(example.value) doesn't make any sense, because self is passed implicitly and would give you access to value (qualified access no less) automatically. Seems like what you really wanted is just lazy initialization when requested with no arguments at all:

    def assign(self):
        self.value = OtherClass()
    

    called as:

    example.assign()
    

    In response to your edits: Python documents this call behavior explicitly:

    The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object).[1]

    Where footnote 1 clarifies:

    [1] Actually, call by object reference would be a better description, since if a mutable object is passed, the caller will see any changes the callee makes to it (items inserted into a list).

    This is intended; C++ reference semantics are unworkable when you don't have other calling semantics available, because there is no obvious way to opt-out, meaning every variable along a huge call chain ends up referencing the same object, causing tons of action-at-a-distance.