Remix.run Logo
kstrauser 13 hours ago

No. It can’t. It can only destroy its own reference to foo, not the calling scope’s reference.

mrkeen 12 hours ago | parent [-]

Right, but I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages) I was asking about the foo.

kstrauser 12 hours ago | parent [-]

Right, but that reference is all the function has. It can’t destroy another scope’s reference to the foo, and the Python GC won’t destroy the foo as long as a reference to it exists.

The function could mutate foo to be empty, if foo is mutable, but it can’t make it not exist.

mrkeen 12 hours ago | parent [-]

>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!

No mention of references!

I don't care about references to foo. I don't care about facades to foo. I don't care about decorators of foo. I don't care about memory segments of foo.

"Did someone eat my lunch in the work fridge?"

"Well at least you wrote your name in permanent marker on your lunchbox, so that should help narrow it down"

kstrauser 10 hours ago | parent [-]

Then I don’t know what you mean. If you have:

  foo = open(‘bar.txt’)
  answer = frobnicate(foo)
  print(foo)
then frobnicate may call foo.close(), or it may read foo’s contents so that you’d have to seek back to the beginning before you could read them a second time. There’s literally nothing you can do in frobnicate that can make it such that the 3rd raises a NameError because foo no longer exists.
AngriestLettuce 2 hours ago | parent [-]

  #!/usr/bin/env python3
  import inspect
  
  def frobnicate(unfrobbed: any) -> None:
      frame = inspect.currentframe().f_back
      for name in [name for name, value in frame.f_locals.items() if value is unfrobbed]:
          del frame.f_locals[name]
      for name in [name for name, value in frame.f_globals.items() if value is unfrobbed]:
          del frame.f_globals[name]
  
  foo = open("bar.txt")
  answer = frobnicate(foo)
  print(foo)

  
  Traceback (most recent call last):
    File "hackers.py", line 20, in <module>
      print(foo)
            ^^^
  NameError: name 'foo' is not defined
Be careful with the absolutes now :)

Not that this is is reasonable code to encounter in the wild, but you certainly can do this. You could even make it work properly when called from inside functions that use `fastlocals` if you're willing to commit even more reprehensible crimes and rewrite the `f_code` object.

Anyway, it's not really accurate to say that Python passes by reference, because Python has no concept of references. It passes by assignment. This is perfectly analogous to passing by pointer in C, which also can be used to implement reference semantics, but it ISN'T reference semantics. The difference comes in assignment, like in the following C++ program:

  #include <print>
  
  struct Object
  {
      char member{'a'};
  };
  
  void assign_pointer(Object *ptr)
  {
      Object replacement{'b'};
      ptr = &replacement;
  }
  
  void assign_reference(Object &ref)
  {
      Object replacement{'b'};
      ref = replacement;
  }
  
  int main()
  {
      Object obj{};
      std::println("Original value: {}", obj.member);
      assign_pointer(&obj);
      std::println("After assign_pointer: {}", obj.member);
      assign_reference(obj);
      std::println("After assign_reference: {}", obj.member);
      return 0;
  }

  $ ./a.out
  Original value: a
  After assign_pointer: a
  After assign_reference: b

Just like in Python, you can modify the underlying object in the pointer example by dereferencing it, but if you just assign the name to a new value, that doesn't rebind the original object. So it isn't an actual reference, it's a name that's assigned to the same thing.

ANYWAY, irrelevant nitpicking aside, I do think Python has a problem here, but its reference semantics are kind of a red herring. Python's concept of `const` is simply far too coarse. Constness is applied and enforced at the class level, not the object, function, or function call level. This, in combination with the pass-by-assignment semantics does indeed mean that functions can freely modify their arguments the vast majority of the time, with no real contract for making sure they don't do that.

In practice, I think this is handled well enough at a culture level that it's not the worst thing in the world, and I understand Python's general reluctance to introduce new technical concepts when it doesn't strictly have to, but it's definitely a bit of a footgun. Can be hard to wrap your head around too.