This has nothing to do with encapsulation, and everything with a Python implementation detail as to when string literals produce a string object.
What is specifically happening here is that Python uses constants to store literal values used in code. Your
'the juice bar' value is such a constant.
The compiler has stored
'the juice bar' with the code block object for the module, and is reusing that object. You are not creating new string values in the
__name__ == '__main__' block.
Functions get their own code blocks, which includes their own constants, and thus their own string objects.
>>> import dis
>>> code = compile('''\
... fr = "the juice bar"
... print('%r is %r: %r' % (fr, 'the juice bar', (fr is 'the juice bar')))
... print('%r == %r: %r' % (fr, 'the juice bar', (fr == 'the juice bar')))
... ''', '<stdin>', 'exec')
('the juice bar', '%r is %r: %r', '%r == %r: %r', None)
1 0 LOAD_CONST 0 ('the juice bar')
3 STORE_NAME 0 (fr)
2 6 LOAD_NAME 1 (print)
9 LOAD_CONST 1 ('%r is %r: %r')
12 LOAD_NAME 0 (fr)
15 LOAD_CONST 0 ('the juice bar')
18 LOAD_NAME 0 (fr)
21 LOAD_CONST 0 ('the juice bar')
24 COMPARE_OP 8 (is)
27 BUILD_TUPLE 3
31 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
3 35 LOAD_NAME 1 (print)
38 LOAD_CONST 2 ('%r == %r: %r')
41 LOAD_NAME 0 (fr)
44 LOAD_CONST 0 ('the juice bar')
47 LOAD_NAME 0 (fr)
50 LOAD_CONST 0 ('the juice bar')
53 COMPARE_OP 2 (==)
56 BUILD_TUPLE 3
60 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
64 LOAD_CONST 3 (None)
This is a disassembly of the bytecode for just the
fr test; note the
LOAD_CONST references; these load a constant (stored at position
0 to both set
fr and to later compare if
fr is 'the juice bar') is the same object.
The function object has a similar
co_const construct, which is what Python dutifully returns when called:
>>> class Food:
... def favourite_restaurant(self):
... return "the foo diner"
(None, 'the foo diner')
3 0 LOAD_CONST 1 ('the foo diner')
As a result, the
'the foo diner' string in the method is not the same object as the
'the foo diner' string used in your
__name__ == '__main__' block further down.
Armed with this knowledge you can generate wholly new string objects:
>>> a = 'foo'
>>> b = 'bar'
>>> ab = a + ' ' + b
>>> ab is a + ' ' + b
>>> ab == a + ' ' + b
Generally speaking, you are right.
is always tests for identity,
== for equality. If the identity test passes, then it is usually true that the objects are equal too.
There is one big exception in the standard library:
>>> nan = float('nan')
>>> nan is nan
>>> nan == nan
The Not-a-number floating point constant is never equal to anything. Not even with itself.
It should be noted that Python translates
op1 == op2 into a call to the
object.__eq__() special hook method, which is free to return whatever it likes. Usually that's a boolean, but that is not required:
True are returned for a successful comparison. However, these methods can return any value, so if the comparison operator is used in a Boolean context (e.g., in the condition of an if statement), Python will call
bool() on the value to determine if the result is true or false.