Question regarding unexpected behavior in using __enter__ method

avi.e.gross at gmail.com avi.e.gross at gmail.com
Tue Apr 25 23:47:06 EDT 2023


I think you got that right, Rob. A method created in a class is normally expected to care about the class in the sense that it often wants to access internal aspects and is given a "this" or "self" or whatever name you choose as a first argument. As noted, it is sometimes possible to create a function attached not to an object but to the class itself  as in, I think, the math class that is not normally instantiated as an object but lets you use things like math.pi and math.cos() and so on.

A comment on dunder methods in python is that they have a sort of purpose albeit you can hijack some to do other things. The protocol for WITH is a bit slippery as __enter__() and __exit__ are expected to do some abstract things that loosely are intended to set up something at the start in a way that will be (guaranteed) to be done if the exit routine is called when done. This can be about opening a file, or network connection and later closing it, or setting up some data structure and freeing the memory at the end, but it could be ANYTHING you feel like. For example, it can turn logging of some kind on and off and also compress the log file at the end. Or it could set up changes to the object that are there for the duration of the WITH and then reset the changes back at the end. 

An imaginary example might be to start caching what some methods are doing or replace a method by another, then empty the cache at the end or put back the redirected one.

And if what you want done at the beginning or end is outside the object being worked on, fine. Consider wrapping your function call in a simple function that calls the one you want after ignoring or removing the first argument. There are decorators that can do things like that.

So if you want int() or some existing plain non-member function, define an f() whose body calls int() with all arguments passed along other than the first. 

I just wrote and tested a trivial example where for some reason you just want to call sum() either with an iterable argument or with a second unnamed or named argument that specified a start you can add to. If this is written as a class method, it would have a first argument of "self" to ignore so I simulate that here:

def plusone(first, *rest, **named):
  return(sum(*rest, **named))

If you call this as below with valid arguments, it sort of swallows the first argument and passes the rest along:

>>> plusone("ignore", [])
0
>>> plusone("ignore", [1,2,3])
6
>>> plusone("ignore", [1,2,3], 100)
106
>>> plusone("ignore", range(7), start=100)
121

Yes, anything like this adds overhead. It does add flexibility and allows you to hijack the WITH protocol to do other things perhaps never anticipated but that may make sense, such as changing a companion object rather than the current one. But you need to live within some rules to do things and that means knowing there will be a first argument.

Avi
-----Original Message-----
From: Python-list <python-list-bounces+avi.e.gross=gmail.com at python.org> On Behalf Of Rob Cliffe via Python-list
Sent: Saturday, April 22, 2023 9:56 AM
To: Lorenzo Catoni <l.catoni.99 at gmail.com>; python-list at python.org
Subject: Re: Question regarding unexpected behavior in using __enter__ method

This puzzled me at first, but I think others have nailed it.  It is not 
to do with the 'with' statement, but with the way functions are defined.
When a class is instantiated, as in x=X():
     the instance object gets (at least in effect), as attributes, 
copies of functions defined *in the class* (using def or lambda) but 
they become "bound methods", i.e. bound to the instance.  Whenever they 
are called, they will be called with the instance as the first argument, 
aka self:
     class X(object):
         def func(*args, **kargs): pass
     x = X()
     y = ()
x.func and y.func are two *different" functions.  When x.func is called, 
x is added as the first argument.  When y.func is called. y is added as 
the first argument.
      boundFunc = y.func
     boundFunc() # Adds y as first argument.
Indeed, these functions have an attribute called __self__ whose value is 
... you guessed it ... the object they are bound to
When a function is defined outside of a class, it remains a simple 
function, not bound to any object.  It does not have a __self__ 
attribute.  Neither does a built-in type such as 'int'.
Nor for that matter does the class function X.func:
     X.func() # Called with no arguments

Best wishes
Rob Cliffe

On 20/04/2023 23:44, Lorenzo Catoni wrote:
> Dear Python Mailing List members,
>
> 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:
>
> ```
>>>> class X:
> ...     __enter__ = int
> ...     __exit__ = lambda *_: None
> ...
>>>> with X() as x:
> ...     pass
> ...
>>>> x
> 0
> ```
> 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.
>
> Thank you for your help in advance!
>
> Best regards,
> Lorenzo Catoni

-- 
https://mail.python.org/mailman/listinfo/python-list



More information about the Python-list mailing list