Question regarding unexpected behavior in using __enter__ method

dn PythonList at DancesWithMice.info
Thu Apr 20 22:49:49 EDT 2023


On 21/04/2023 10.44, Lorenzo Catoni wrote:
> I am writing to seek your assistance in understanding an unexpected
> behavior that I encountered while using the __enter__ method. I have
> provided a code snippet below to illustrate the problem:

It is expected behavior - just not what WE might have expected!


>>>> class X:
> ...     __enter__ = int
> ...     __exit__ = lambda *_: None
> ...
>>>> with X() as x:
> ...     pass
> ...
>>>> x
> 0

Note that what is happening is the creation of an alias for the int 
built-in function.

The docs say:
«
class int(x=0)
class int(x, base=10)

     Return an integer object constructed from a number or string x, or 
return 0 if no arguments are given. If x defines __int__(), int(x) 
returns x.__int__(). If x defines __index__(), it returns x.__index__(). 
If x defines __trunc__(), it returns x.__trunc__(). For floating point 
numbers, this truncates towards zero.
...
»

(https://docs.python.org/3/library/functions.html#int)

No argument is given. int() delivers as-promised. Hence, the x == 0 
result obtained.



> As you can see, the __enter__ method does not throw any exceptions and
> returns the output of "int()" correctly. However, one would normally expect
> the input parameter "self" to be passed to the function.
> 
> On the other hand, when I implemented a custom function in place of the
> __enter__ method, I encountered the following TypeError:
> 
> ```
>>>> def myint(*a, **kw):
> ...     return int(*a, **kw)
> ...
>>>> class X:
> ...     __enter__ = myint
> ...     __exit__ = lambda *_: None
> ...
>>>> with X() as x:
> ...     pass
> ...
> Traceback (most recent call last):
>    File "<stdin>", line 1, in <module>
>    File "<stdin>", line 2, in myint
> TypeError: int() argument must be a string, a bytes-like object or a real
> number, not 'X'
> ```
> Here, the TypeError occurred because "self" was passed as an input
> parameter to "myint". Can someone explain why this unexpected behavior
> occurs only in the latter case?
> 
> I tested this issue on the following Python versions, and the problem
> persists on all of them:
> - Python 3.8.10 (default, Nov 14 2022, 12:59:47) [GCC 9.4.0] on linux
> - Python 3.10.10 (main, Feb  8 2023, 14:50:01) [GCC 9.4.0] on linux
> - Python 3.10.7 (tags/v3.10.7:6cc6b13, Sep  5 2022, 14:08:36) [MSC v.1933
> 64 bit (AMD64)] on win32
> 
> I appreciate any input or insights that you might have on this matter.


(you know this next part!)

However, if int() is fed an X-object, which in no way represents a 
numeric or numeric-able value, then it crashes. After the class 
definition, try:

int( X )

=> int 0


Right-y-ho: the evidence.

Firstly, what happens when int() is called with no argument?

print( "int", int(), )


Note that whereas int and myint are both called with no argument(s), and 
therefore int() defaults to 0, myint is attempting to use the arguments 
as part of its return-value - "pass-through".

As identified, the first argument (the a-tuple's zero-th element) is 
going to be x's self - which is NOT numeric, stored in a tuple - which 
is NOT numeric...


Thus, if we "shadow" the built-in int() with a user-function, we can see 
what is being passed-in

def int( *a, **kw, ):
     print( locals(), )
     print( "a", a, type( a ), id( a ), )
     print( len( a ), a[ 0 ], type( a[ 0 ], ) )
     print( "kw", kw, type( kw ), id( kw ), )
     return 42

class X:
     __enter__ = int
     __exit__ = lambda *_: None

with X() as x:
     pass

print( "After first CM", x, "\n\n")

del( int )


def myint(*a, **kw):
     print( locals(), )
     print( "a", a, type( a ), id( a ), )
     print( len( a ), a[ 0 ], type( a[ 0 ], ) )
     print( "kw", kw, type( kw ), id( kw ), )
     return int(*a, **kw)

class Y:
     __enter__ = myint
     __exit__ = lambda *_: None


print( Y, type( Y ), id( Y ), )

with Y() as y:
     print( y, type( y ), id( y ), )
     pass

print( y )


=>
{'a': (<__main__.X object at 0x7f9b6bf13b90>,), 'kw': {}}
a (<__main__.X object at 0x7f9b6bf13b90>,) <class 'tuple'> 140305733882528
1 <__main__.X object at 0x7f9b6bf13b90> <class '__main__.X'>
kw {} <class 'dict'> 140305734120576
After first CM 42


<class '__main__.Y'> <class 'type'> 93904023389520
{'a': (<__main__.Y object at 0x7f9b6bf2c0d0>,), 'kw': {}}
a (<__main__.Y object at 0x7f9b6bf2c0d0>,) <class 'tuple'> 140305507712640
1 <__main__.Y object at 0x7f9b6bf2c0d0> <class '__main__.Y'>
kw {} <class 'dict'> 140305507621376
Traceback (most recent call last):
   File "/home/dn/Projects/analyzer/lorenzo.py", line 53, in <module>
     with Y() as y:
   File "/home/dn/Projects/analyzer/lorenzo.py", line 44, in myint
     return int(*a, **kw)
            ^^^^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a 
real number, not 'Y'


If you remover the del() and leave my play-version of int(), Python can 
make it work...

(the second half of the output)

<class '__main__.Y'> <class 'type'> 94557671306576
{'a': (<__main__.Y object at 0x7f950ee2c0d0>,), 'kw': {}}
a (<__main__.Y object at 0x7f950ee2c0d0>,) <class 'tuple'> 140278176579200
1 <__main__.Y object at 0x7f950ee2c0d0> <class '__main__.Y'>
kw {} <class 'dict'> 140278176487936
{'a': (<__main__.Y object at 0x7f950ee2c0d0>,), 'kw': {}}
a (<__main__.Y object at 0x7f950ee2c0d0>,) <class 'tuple'> 140278176579152
1 <__main__.Y object at 0x7f950ee2c0d0> <class '__main__.Y'>
kw {} <class 'dict'> 140278176482368
42 <class 'int'> 140278410201800
42


So, it rather depends upon what you want returned from the actual 
myint() function.


Web.Refs:
https://docs.python.org/3/reference/datamodel.html?highlight=context%20manager#with-statement-context-managers
https://docs.python.org/3/reference/compound_stmts.html?highlight=context%20manager#the-with-statement



I'm curious though, why not:

class X:
     def __enter__( etc
     def __exit__( etc

with the actual code from myint9) as the suite/body of the __enter__()?
(in which case, the rôle of self is 'standard operating procedure' and 
may be more obvious)

-- 
Regards,
=dn


More information about the Python-list mailing list