Remix.run Logo
AngriestLettuce 2 hours ago

  #!/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.