• Lambda returning tuple question, multi-expression

    From aapost@21:1/5 to All on Wed Mar 8 16:56:42 2023
    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Giving 2 working lambda examples using a returned tuple to accomplish
    multiple expressions - what sort of gotchas, if any, might make the
    following bad practice if I am missing something?


    Example 1:

    import tkinter as tk

    main = tk.Tk()
    e1 = tk.Entry(master=main)
    e1["state"] = "disabled"
    e1.pack()
    e2 = tk.Entry(master=main)
    e2["state"] = "disabled"
    e2.pack()
    e3 = tk.Entry(master=main)
    e3["state"] = "disabled"
    e3.pack()

    b = tk.Button(master=main, text="Enable")
    b.config(
    command=lambda: (
    e1.config(state="normal"),
    e2.config(state="normal"),
    e3.config(state="normal")
    )
    )
    b.pack()


    Example 2:

    import tkinter as tk

    main = tk.Tk()
    l = tk.Label(master=main)
    l.a = {"seconds":0}
    l._updater = lambda: (
    l.a.update({"seconds": 1 + l.a["seconds"]}),
    l.config(text=l.a["seconds"]),
    l.after(ms=1000, func=l._updater)
    )
    l._updater()
    l.pack()

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Cameron Simpson@21:1/5 to aapost on Thu Mar 9 09:44:56 2023
    On 08Mar2023 16:56, aapost <aapost@idontexist.club> wrote:
    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Giving 2 working lambda examples using a returned tuple to accomplish >multiple expressions - what sort of gotchas, if any, might make the
    following bad practice if I am missing something?

    There're no real gotchas: tuples are evaluated left to right, so you
    should have things happen in the order you've expressed them (if the
    order matters). What you lose with a lambda is control constructs like
    loops and if-statements (well, there's the `x if foo else y` but that
    gets cumbersome quickly). Once things get complicated you may want to
    define a more complication function using `def`:

    def callback1():
    ... do complicated stuff ...

    b.config(command=callback1)

    The only other issue, which applies across the board with GUIs and is
    nothing specific to lambdas is that the GUI only renders and operates
    while the main loop is running. When your callbacks do trivial stuff
    you're fine. If they block (eg waiting for user input or network calls
    etc) the GUI is also blocked. You need threads or other concurrent
    approaches if the GUI is to stay responsive.

    The flip side of that is that a concurrent context like a Thread should
    not interact with the GUI directly. In things like Qt I've actually had
    that mistake crash the app because the Qt framework is (was?) not thread
    safe. You need to arrange that GUI actions occur in the main programme
    thread. I think the same applies with tk, and is anyway probably good
    practice for any GUI. It's not as hard as it sounds - typically when
    something happens asynchronously you arrange to issue an "event", and
    the GUI mainloop will process that as it happens - the event callback
    will be fired (called) by the main loop itself and thus the callback
    gets to do its thing in the main loop.

    Cheers,
    Cameron Simpson <cs@cskk.id.au>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to aapost on Wed Mar 8 18:37:07 2023
    On 3/8/2023 4:56 PM, aapost wrote:
    b = tk.Button(master=main, text="Enable")
    b.config(
        command=lambda: (
            e1.config(state="normal"),
            e2.config(state="normal"),
            e3.config(state="normal")
        )
    )

    It's hard to understand what you are trying to do here. I don't
    remember just now what .config() will return, but the lambda will return
    a tuple of *something*, probably (None, None, None). Even if the tuple
    does contain three non-None values, config() requires named parameters,
    not a tuple. In the course of executing the lambda, your three controls
    e1, e2, and e2 will get configured, but you could just write a function
    to do that:

    def set_entries_enabled_state(enabled = True):
    state = 'normal' if enabled else 'disabled'
    for e in (e1, e2, e3):
    e.config(state=state)

    def config_b_and_entries(enabled = True):
    state = 'normal' if enabled else 'disabled'
    b.config(state = state)
    set_entries_enabled_state(enabled)

    This is easy to read and understand. (I may not have remembered some Tk details correctly).

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to aapost on Wed Mar 8 23:19:51 2023
    On 3/8/23 16:56, aapost wrote:
    Thomas > Cameron

    def set_entries_enabled_state(enabled = True):
    state = 'normal' if enabled else 'disabled'
    for e in (e1, e2, e3):
    e.config(state=state)

    def config_b_and_entries(enabled = True):
    state = 'normal' if enabled else 'disabled'
    b.config(state = state)
    set_entries_enabled_state(enabled)

    I admit adding commenting might be useful to clearly express intent for patterns I don't commonly see. But when you are managing a bunch of
    widgets and experimenting with the design, the variable name word salad approach 'in the name of readability' starts to melt your brain a bit
    and it all starts too look like a wall of..:

    the_thing.the_things_thing.do_this_thing_to_that_other_thing_but_only_if_this_one_time()

    So the button b in the example only needs a reference to a callable
    configured, set with the command= parameter in .config()

    The code:

    def func():
    pass
    b.config(command=func)

    Is equivalent. So that if the button is clicked, code at func (or the
    lambda) gets called.

    In both cases (as per my intent), care about the return values of the expressions does not matter and I am fairly certain regardless of what
    the callable returns, it does not get evaluated/assigned/considered
    anywhere. (at least that is how I have been considering it, a void
    foo(void) if you will..). Now as far as the tuple, yes, left to right evaluation is what I expect, (hope anyway, lol),

    meaning (3, False, 1, x := 5+9, print("hello{}".format(x)))
    would return a tuple of (3, False, 1, 14, None) which gets assigned to
    nothing and the string "hello14" printed to console.

    Now.. When you want to assign a callable that requires args, the main
    examples people give are a combo of them both,

    def func(x,y,z):
    pass
    x = y = x = "something"
    b.config(command=lambda x,y,z: func(x,y,z))

    So as far as the examples given above (which I can't really parse), if
    you meant for passing in a bool value, to do so would require something
    like:

    b.config(command=lambda enabled: config_b_and_entries(enabled))

    Which that type of thing to me gets even harder to grok after a while,
    and I guess for me I find having to go to a different scope or a
    separate file to parse a bunch of definitions like these:

    def set_entries_enabled_state(enabled = True):

    def config_b_and_entries(enabled = True):

    ends up taking me out of an object oriented focus / headspace of the
    layout at hand.


    And it is sort of like.. Ok I can either

    b.config(command=lambda: (
    a.expr,
    b.expr.update({something: "morecomplicated"}),
    c.expr
    )
    )

    OR..

    b.config(command=lambda a=a, b=b, c=c, s=something: foo(a, b, c, s))

    somewhere else:
    def foo(a, b, c, something):
    a.expr
    b.expr.update({something: "morecomplicated"})
    c.expr


    When you are trying to add a bit of dynamic feel to the tediousness of
    the widget management, keeping things kind of contained to their object
    just feels more natural to me (at the moment anyway).. (all the packing, unpacking, making collections of widgets within frames appear or go away
    based on states, colour changes based on input, dynamic widget
    generation and where their relative attachment should go, etc)

    I read a lot of sentiment against complicated lambdas suggesting one
    should go for more functions, but I guess I feel pushing a more
    complicated lambda to contain behavior more closely to an instance feels
    more intuitive at the moment (and the sentiment against it doesn't
    really resonate with me), so long as it isn't introducing some inherent behavioral flaw or bug I am blind to..

    Of course I might change my mind at some point during a refactor and
    think "what the hell is that, why didn't I just..".. Which will probably
    come in a few weeks. lol

    One additional note on the Thread comment, I haven't really needed to
    dig in to that too deeply, but Threading is amazing for tkinter UI troubleshooting,

    if you add something like:
    t = threading.Thread(target=maintk.mainloop)
    and run it with python -i
    so long has you have attached every widget to some relative position on
    to the root (maintk in this case), you can interact with any
    object/widget directly live to see what is going on or what a change does.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to aapost on Thu Mar 9 00:13:00 2023
    On 3/8/2023 11:19 PM, aapost wrote:
    In both cases (as per my intent)

    Well, that's the trouble. You haven't stated your intent, so we're
    forced to try to reverse engineer it. Below I state what my
    reverse-engineering effort thinks is your intent. It would be better if
    you actually said clearly what you want to achieve.

    So as far as the examples given above (which I can't really parse), if
    you meant for passing in a bool value, to do so would require something
    like:

    b.config(command=lambda enabled: config_b_and_entries(enabled))

    As best as I can understand what you are trying to do here, it seems
    like you want to enable/disable those Entry widgets when you configure
    the b widget to be enabled/disabled. That way their states would all
    track each other, with only a single command needing to be issued.

    That seems like a sensible goal. The function config_b_and_entries()
    that I suggested is not meant to be the target of a lambda expression
    that is the argument of b.config(). It is meant to be called *instead*
    of b.config(command = lambda.....). I can't see any benefit of trying
    to force this coordination of states by using an obscure lambda
    expression when you can achieve the same result with a straightforward,
    easy to read function or method call.

    OTOH, if you are not trying to achieve this coordination of states, then
    what are you trying to do? Don't go making us guess any more.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to Thomas Passin on Thu Mar 9 03:29:35 2023
    On 3/9/23 00:13, Thomas Passin wrote:


    lol.. 'us'..

    So.. to give an example from your own code:

    but_play = Tk.Button(_frame, text='Play', width = BUTTONWIDTH + 1, pady
    = PADY, command=lambda x=plotmgr:play_macro(x), bg = BUTTON_BG, font =
    NEWFONT)

    Can be written as:

    b = Tk.Button(master=_frame)
    b.config(text='Play', width = BUTTONWIDTH + 1, pady = PADY,
    command=lambda x=plotmgr:play_macro(x), bg = BUTTON_BG, font = NEWFONT)

    .config() is just a way of adding/modifying most of the same
    initialization arguments after the instantiation of the object.

    tkinter objects are flexible by design, You can also do
    b["text"] = "adsfa"
    b["command"] = lambda: (a,b,c,d)

    I could also rewrite the original example with the exact same result:
    b = tk.Button(
    master=main,
    text="Enable",
    command=lambda: (
    e1.config(state="normal"),
    e2.config(state="normal"),
    e3.config(state="normal")
    )
    )
    b.pack()

    b is not changing states at any point. Nothing "happens" to state when .config() is called. b does nothing without a command= configured.
    .config() is binding the button press event via command= to a call so
    than an action can occur on button press. There is no 'instead' of
    b.config() (unless of course using one of the equivalent examples above
    to do the exact same thing)

    The disconnect is not in the not understanding of my code, it's in the
    not understanding of .config(). (which is no big deal, I forgot what a
    file buffer was just this week, lol)

    So, as far as the examples, they are simplified abstract
    psuedo-like-code illustrations to accompany the ask of and open ended
    question regarding a programming practice.

    (I did forgot to include main.mainloop() at the end of each one, as I
    was using python -i when I quickly wrote them)

    example 1 described:
    A button press does sequence of actions against widgets, i.e. I have 3
    disabled widgets, I press a button, now in sequence all 3 are enabled so
    I can type in them. Without a lambda tuple sequence this requires a def
    and an another lambda.

    example 2 described:
    dict with an attribute assigned to a label (you can't assign directly to external variables within a lambda, but you can call something with a
    method like .update(), which why would that be considered any different
    than any other state changing call?)
    Then lambda assigned to a label and called to start it up, recursively
    performs an update to said widget every 1 second. There are several ways
    to do this without a lambda tuple sequence, none as concise.

    The 'what I am trying to do' is ask a question regarding opinions and
    practices on issuing a sequence of actions within a lambda via a tuple
    (since the common practice approaches against it - mainly with tkinter -
    feel more convoluted), and in doing so leaving it open ended to get a
    feel on what opinions are, and to see if any opinions influence mine.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Alan Gauld@21:1/5 to aapost on Thu Mar 9 09:06:13 2023
    On 08/03/2023 21:56, aapost wrote:
    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Lambdas are very common in GUI callbacks but I admit I've never seen
    tuples used to create multiple expressions. That's a neat trick I
    hadn't thought of and will probably use.

    Giving 2 working lambda examples using a returned tuple to accomplish multiple expressions - what sort of gotchas, if any, might make the
    following bad practice if I am missing something?

    Not bad practice per-se but you are still limited to expressions, no
    loops for example (although you could fake it with a list comp,
    but that gets ugly fast!)

    Also layout is all important here. It could get very messy to read if indentation isn't clear. You only have to look at some Javascript code
    with function definitions as arguments to functions to see how clunky
    that can be.

    Similarly debugging so much code passed as arguments might be an
    issue - no easy way to step into the lambda.

    But as an alternative to writing many typical event handlers it's
    definitely a valid approach that I'll be trying.

    b = tk.Button(master=main, text="Enable")
    b.config(
    command=lambda: (
    e1.config(state="normal"),
    e2.config(state="normal"),
    e3.config(state="normal")
    )
    )

    You could of course single line that with:

    b = tk.Button(master=main,
    text="Enable",
    command=lambda: (
    e1.config(state="normal"),
    e2.config(state="normal"),
    e3.config(state="normal")
    )
    )

    It's not a radical change from using a lamba as a
    callback but it does extend the use case to cover
    a common GUI scenario.

    I like it. I wish I'd thought of it years ago.
    Thanks for sharing.

    --
    Alan G
    Author of the Learn to Program web site
    http://www.alan-g.me.uk/
    http://www.amazon.com/author/alan_gauld
    Follow my photo-blog on Flickr at:
    http://www.flickr.com/photos/alangauldphotos

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Weatherby,Gerard@21:1/5 to All on Thu Mar 9 10:11:24 2023
    Other than the admittedly subjective viewpoint that using the lambda is confusing, there’s probably nothing wrong with the lambda approach. A couple of alternatives:


    def e():
    for e in (e1,e2,e3):
    e.config(state="normal")
    b = tk.Button(master=main, text="Enable",command=e)

    or

    b = tk.Button(master=main, text="Enable",command=lambda: [e.config(state="normal") for e in (e1, e2, e3)])

    From: Python-list <python-list-bounces+gweatherby=uchc.edu@python.org> on behalf of aapost <aapost@idontexist.club>
    Date: Wednesday, March 8, 2023 at 5:15 PM
    To: python-list@python.org <python-list@python.org>
    Subject: Lambda returning tuple question, multi-expression
    *** Attention: This is an external email. Use caution responding, opening attachments or clicking on links. ***

    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Giving 2 working lambda examples using a returned tuple to accomplish
    multiple expressions - what sort of gotchas, if any, might make the
    following bad practice if I am missing something?


    Example 1:

    import tkinter as tk

    main = tk.Tk()
    e1 = tk.Entry(master=main)
    e1["state"] = "disabled"
    e1.pack()
    e2 = tk.Entry(master=main)
    e2["state"] = "disabled"
    e2.pack()
    e3 = tk.Entry(master=main)
    e3["state"] = "disabled"
    e3.pack()

    b = tk.Button(master=main, text="Enable")
    b.config(
    command=lambda: (
    e1.config(state="normal"),
    e2.config(state="normal"),
    e3.config(state="normal")
    )
    )
    b.pack()


    Example 2:

    import tkinter as tk

    main = tk.Tk()
    l = tk.Label(master=main)
    l.a = {"seconds":0}
    l._updater = lambda: (
    l.a.update({"seconds": 1 + l.a["seconds"]}),
    l.config(text=l.a["seconds"]),
    l.after(ms=1000, func=l._updater)
    )
    l._updater()
    l.pack()

    -- https://urldefense.com/v3/__https://mail.python.org/mailman/listinfo/python-list__;!!Cn_UX_p3!i8mp9fRKlBLziXCn6-OIC8fNx0LBohis8m6VARp17Igg5036wrTflGiwwptY18Rgkriw5MquUKxe9Fglqpu8FHEy$<https://urldefense.com/v3/__https:/mail.python.org/mailman/listinfo/
    python-list__;!!Cn_UX_p3!i8mp9fRKlBLziXCn6-OIC8fNx0LBohis8m6VARp17Igg5036wrTflGiwwptY18Rgkriw5MquUKxe9Fglqpu8FHEy$>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Chris Angelico@21:1/5 to aapost on Thu Mar 9 22:27:11 2023
    On Thu, 9 Mar 2023 at 09:14, aapost <aapost@idontexist.club> wrote:

    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Giving 2 working lambda examples using a returned tuple to accomplish multiple expressions - what sort of gotchas, if any, might make the
    following bad practice if I am missing something?

    IMO there's nothing wrong with this, but it's worth considering a few alternatives. For instance, the single-use function might be less
    annoying if you don't have to also name the function in the
    constructor; this could be achieved with proper use of a decorator, or
    possibly use of a class namespace, with an appropriate helper. But
    otherwise, this tuple trick seems pretty much fine to me, although
    you're still restricted to a series of expressions (no statements), so
    your code is still going to get a bit awkward if it does anything
    complicated.

    ChrisA

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Stefan Ram@21:1/5 to Thomas Passin on Thu Mar 9 20:57:03 2023
    Thomas Passin <list1@tompassin.net> writes:
    I finally realized why I am uncomfortable with doing this kind of thing.
    It's because, IMHO, lambda expressions should not have side effects

    From the highly acclaimed book by Lutz, all with effects
    (which you call "side effects"):

    ...

    widget = Button(None, # but contains just an expression
    text='Hello event world',
    command=(lambda: print('Hello lambda world') or sys.exit()) )

    ...

    Button(text='ni', command=(lambda: handler(X, 'spam'))) # lambda adds arguments

    ...

    Button(command=(lambda: handler('spam'))) # OK: wrap in a lambda to defer

    ...

    Button(text='ni', command=(lambda X=X: handler(X, 'spam')))

    ...

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to aapost on Thu Mar 9 15:25:08 2023
    On 3/9/2023 3:29 AM, aapost wrote:
    The 'what I am trying to do' is ask a question regarding opinions and practices on issuing a sequence of actions within a lambda via a tuple
    (since the common practice approaches against it - mainly with tkinter -
    feel more convoluted), and in doing so leaving it open ended to get a
    feel on what opinions are, and to see if any opinions influence mine.

    I finally realized why I am uncomfortable with doing this kind of thing.
    It's because, IMHO, lambda expressions should not have side effects
    and should not require much mental effort to grasp what they do. Yes,
    you can do so. You can even print from a lambda (and it might be useful
    for debugging):

    lambda x: print(f'The lambda received {x}') or x*x

    The Python Reference on readthedocs.io also has a tk example that
    specifically wants the side effect (see https://python-reference.readthedocs.io/en/latest/docs/operators/lambda.html):

    # this is a code snippet from a Tkinter gui app
    # in this case lambda is quite convenient
    self.btn_cancel = Button(self.progress_container, text='Cancel',
    command=lambda: subprocess.call('taskkill /f /im uberzip.exe',
    shell=True))

    Maybe so, but I think it's better not to have side effects hidden away
    in expressions that are hard to read and understand. And being
    anonymous, there is no function name to remind you what the action is
    suppose to do. Much better (still IMHO, of course):

    def kill_uberzip():
    """Kill an external running program named uberzip.exe."""
    subprocess.call('taskkill /f /im uberzip.exe', shell=True))

    self.btn_cancel = Button(self.progress_container, text='Cancel',
    command = kill_uberzip())

    This way, it's easy to understand what btn_cancel() will do each time
    you scan that line of code. Using the lambda makes you reparse the line
    and spend mental effort each time you scan it. And this way, you know
    directly that the button is going to cause a side effect outside your
    program, which you have to infer (an indirect mental operation) when you
    scan the lambda.

    For this particular example, it might turn out that there could be more
    than one instance of uberzip.exe running at the same time. Which one
    should be killed, and how do you kill the right one? With the function,
    you can get those details under control, but I hate to think what might
    happen to the lambda expression.

    Yes, of course, there can be times when the lambda expression is
    somewhat easy to understand and the side effects are harmless. In that
    case, it may be easy enough to grasp quickly that the anonymous function
    would not benefit from having a name. So OK, it's not a hard-and-fast
    rule.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Cameron Simpson@21:1/5 to Alan Gauld on Fri Mar 10 08:37:49 2023
    On 09Mar2023 09:06, Alan Gauld <learn2program@gmail.com> wrote:
    Also layout is all important here. It could get very messy to read if >indentation isn't clear. You only have to look at some Javascript code
    with function definitions as arguments to functions to see how clunky
    that can be.

    Just a note that some code formatters use a trailing comma on the last
    element to make the commas fold points. Both yapf (my preference) and
    black let you write a line like (and, indeed, flatten if short enough):

    ( a, b, c )

    but if you write:

    ( a, b, c, )

    they'll fold the lines like:

    ( a,
    b,
    c,
    )

    in varying flavours of indentation depending on tuning. The point being
    that if, like me, you often have a code formatter active-on-save it can
    be hinted to nicely present complex tuples (or parameter lists and
    imports).

    It isn't magic, but can be quite effective.

    Cheers,
    Cameron Simpson <cs@cskk.id.au>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Cameron Simpson@21:1/5 to Alan Gauld on Fri Mar 10 08:31:42 2023
    On 09Mar2023 09:06, Alan Gauld <learn2program@gmail.com> wrote:
    On 08/03/2023 21:56, aapost wrote:
    When making a UI there are a lot of binding/trace operations that need
    to occur that lead to a lot of annoying 1 use function definitions. I
    don't really see lambda use like below.

    Lambdas are very common in GUI callbacks but I admit I've never seen
    tuples used to create multiple expressions. That's a neat trick I
    hadn't thought of and will probably use.

    I often uses as a debugging hack. Given:

    foo=lambda: expr

    I'll often write:

    foo=lambda: (X("foo happening here!"), ...maybe more..., expr)[-1]

    to embed some debug tracing in a lambda defined expression.

    Cheers,
    Cameron Simpson <cs@cskk.id.au>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Chris Angelico@21:1/5 to Thomas Passin on Fri Mar 10 09:11:31 2023
    On Fri, 10 Mar 2023 at 07:43, Thomas Passin <list1@tompassin.net> wrote:

    On 3/9/2023 3:29 AM, aapost wrote:
    The 'what I am trying to do' is ask a question regarding opinions and practices on issuing a sequence of actions within a lambda via a tuple (since the common practice approaches against it - mainly with tkinter - feel more convoluted), and in doing so leaving it open ended to get a
    feel on what opinions are, and to see if any opinions influence mine.

    I finally realized why I am uncomfortable with doing this kind of thing.
    It's because, IMHO, lambda expressions should not have side effects
    and should not require much mental effort to grasp what they do. Yes,
    you can do so. You can even print from a lambda (and it might be useful
    for debugging):

    lambda x: print(f'The lambda received {x}') or x*x

    I'm not sure why lambda functions shouldn't have side effects. They
    can be used for anything, as long as it's a single expression (which
    can, of course, be composed of other expressions).

    The Python Reference on readthedocs.io also has a tk example that specifically wants the side effect (see https://python-reference.readthedocs.io/en/latest/docs/operators/lambda.html):

    # this is a code snippet from a Tkinter gui app
    # in this case lambda is quite convenient
    self.btn_cancel = Button(self.progress_container, text='Cancel',
    command=lambda: subprocess.call('taskkill /f /im uberzip.exe',
    shell=True))

    Maybe so, but I think it's better not to have side effects hidden away
    in expressions that are hard to read and understand. And being
    anonymous, there is no function name to remind you what the action is
    suppose to do. Much better (still IMHO, of course):

    The purpose of the lambda function is to tie the effect to the
    function. Otherwise, you have to go elsewhere to figure out what each
    one does. It's not inherently better to give names to everything,
    especially when those functions are only ever used once. In fact, I'd
    say that that is usually *worse*.

    def kill_uberzip():
    """Kill an external running program named uberzip.exe."""
    subprocess.call('taskkill /f /im uberzip.exe', shell=True))

    self.btn_cancel = Button(self.progress_container, text='Cancel',
    command = kill_uberzip())

    And this is one of the reasons it's worse: with the lambda function,
    you won't fall into this trap. It's a classic bug, and a rather nasty
    one; can you see it? If not, this is a VERY strong argument in favour
    of the lambda function. Even if you can, the mere fact that you made
    this error indicates how easy it is to slip into the faulty way of
    writing it.

    This way, it's easy to understand what btn_cancel() will do each time
    you scan that line of code.

    Only if the name is enough information, and it seldom is. Whereas with
    the lambda function, you have the entire code there, so you can
    understand what btn_cancel does without having to bookmark your spot
    there, go elsewhere, and find out what kill_uberzip() does.

    For this particular example, it might turn out that there could be more
    than one instance of uberzip.exe running at the same time. Which one
    should be killed, and how do you kill the right one? With the function,
    you can get those details under control, but I hate to think what might happen to the lambda expression.

    Maybe, but this is just the example getting in the way. Obviously the
    larger a function needs to be, the less reasonable to make it a lambda function.

    That's why I'm of the opinion that it's better to use a different
    system, such as function decorators or a class namespace. For example:

    self.btn_cancel = Button(...)

    @on(self.btn_cancel, "command")
    def kill_uberzip(): ...

    Or alternatively:

    class btn_cancel(Button):
    def command(self):
    ...

    These kinds of systems allow much more flexibility than lambda
    functions, without the issues of name repetition and disconnection.

    Yes, of course, there can be times when the lambda expression is
    somewhat easy to understand and the side effects are harmless. In that
    case, it may be easy enough to grasp quickly that the anonymous function would not benefit from having a name. So OK, it's not a hard-and-fast
    rule.

    Definitely not a hard-and-fast rule, but in defense of the Python's
    lambda, at least we don't have resonance cascades happening.

    ChrisA

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to Alan Gauld on Thu Mar 9 17:16:15 2023
    On 3/9/23 04:06, Alan Gauld wrote:


    Thank you for the feedback, I appreciate the comments.

    To add a little extra, there is actually a reason I lean toward overuse
    of .config() for a lot of things even though they could be sent to the constructor (other than that w["attribute"]= doesn't work in a lambda).

    It comes from a use case I needed to solve in the shortcoming of tk with
    frames not being scrollable, requiring a canvas.

    When dynamically generating a series of widgets (i.e. loading a custom
    json/xml layout config file), getting the scrollable area right is a bit
    of a chore with a catch-22 situation.

    With an inner frame in a canvas, pack_propogate(tk.True) is the default
    on the frame, so that the children dictate the size, but in doing so it prevents the frame from expanding to the full width of the area it's
    given within the UI layout.

    If you turn pack_propogate to tk.False, it breaks the ability for the
    canvas to return bbox(tk.ALL) (which is kind of rough anyway as the tcl
    docs state "The return value may overestimate the actual bounding box by
    a few pixels."). So you end up with 1,1 and no way of knowing what size scrollable area to set. Trying to obtain this value from a source
    outside of the canvas requires knowing what you are placing the canvas
    in each time, and was creating a similar catch-22 as the outer widget
    doesn't know what it wants it's size to be without knowing what the
    inner widget wants.. Switching to False, grabbing bbox, then back to
    True of course causes an unsightly flicker and disrupts the smoothness
    of the user experience.

    So my solution is to create a widget instantiator which does a few
    things, mainly adding something I call a "ghost".

    it's something like this:

    instantiator(qlass, master, config_args=None, pack_args=None,
    init_args=None, ghost=False):
    if not init_args:
    init_args = {}
    object = qlass(master=master, **init_args)
    if hasattr(master, "ghost"):
    object.ghost = qlass(master=master.ghost, **init_args)
    elif ghost:
    object.ghost = qlass(master=tk.Frame(), **init_args)

    When i pass it a canvas and say ghost=True, the canvas gets a .ghost
    duplicate, which is attached to an arbitrary frame that I never pack and
    stays invisible.

    Subsequent widgets created to the canvas then see that their parent,
    starting with the canvas, have a ghost, and in return get a ghost of
    themselves attached to their parents ghost.

    This allows you to get an accurate bbox size from the unseen ghost
    canvas that mirrors the visible version.

    Keeping the init_args down to only what is necessary helps in
    consistency, and the subsequent config_args and pack_args I have in
    their respective dicts.

    This also allows me to create a quality of life pack function I call ppack()

    def ppack(self):
    self.pack(**self.pack_args)
    if hasattr(self, "ghost"):
    self.ghost.pack(**self.ghost.pack_args)
    return self

    That allows each primary widget to manage and track their own set of configurations.

    Of course I could strip a lot of that crap out if I find a better and
    smooth way of obtaining those bbox numbers, but I didn't see any quick solutions in glancing through the tcl/tk source so I went with what
    works for now.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to Cameron Simpson on Thu Mar 9 17:55:01 2023
    On 3/9/23 16:37, Cameron Simpson wrote:
    On 09Mar2023 09:06, Alan Gauld <learn2program@gmail.com> wrote:

    Just a note that some code formatters use a trailing comma on the last element to make the commas fold points. Both yapf (my preference) and
    black let you write a line like (and, indeed, flatten if short enough):

        ( a, b, c )

    but if you write:

        ( a, b, c, )

    they'll fold the lines like:

        ( a,
          b,
          c,
        )
    Cameron Simpson <cs@cskk.id.au>


    Thanks for the info, good to know, I actually do like the idea of
    trailing commas for tuples (helps prevent things like the difference
    between ("abc") and ("abc",) and makes swapping things around nicer.

    I've just been using a lot of json lately and it has been subconsciously training me different, lol.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Cameron Simpson@21:1/5 to aapost on Sat Mar 11 08:08:21 2023
    On 09Mar2023 17:55, aapost <aapost@idontexist.club> wrote:
    On 3/9/23 16:37, Cameron Simpson wrote:
    Just a note that some code formatters use a trailing comma on the last >>element to make the commas fold points. Both yapf (my preference) and
    black let you write a line like (and, indeed, flatten if short
    enough):

        ( a, b, c )

    but if you write:

        ( a, b, c, )

    they'll fold the lines like:

        ( a,
          b,
          c,
        )
    Cameron Simpson <cs@cskk.id.au>


    Thanks for the info, good to know, I actually do like the idea of
    trailing commas for tuples (helps prevent things like the difference
    between ("abc") and ("abc",) and makes swapping things around nicer.

    Just keep in mind that if you restructure code with copy/paste it can be
    a small issue (not really tied to the trailing comma but commas in
    general. More than once I've been bitten by doing roughly this:

    f( a,
    b=x+y,
    )

    shuffled to:

    b=x+y,
    f( a,
    b=b,
    )

    Whoops! It shows up almost immediately, but the first time it took me a
    while to see that stray comma.

    I've just been using a lot of json lately and it has been
    subconsciously training me different, lol.

    Yes, it hates the trailing comma. So annoying.

    Cheers,
    Cameron Simpson <cs@cskk.id.au>

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to Thomas Passin on Fri Mar 10 18:46:40 2023
    On 3/9/23 15:25, Thomas Passin wrote:
    # this is a code snippet from a Tkinter gui app
    # in this case lambda is quite convenient
    self.btn_cancel = Button(self.progress_container, text='Cancel',
         command=lambda: subprocess.call('taskkill /f /im uberzip.exe',
         shell=True))

    def kill_uberzip():
        """Kill an external running program named uberzip.exe."""
        subprocess.call('taskkill /f /im uberzip.exe', shell=True))

    self.btn_cancel = Button(self.progress_container, text='Cancel',
         command = kill_uberzip())

    This way, it's easy to understand what btn_cancel() will do each time
    you scan that line of code.  Using the lambda makes you reparse the line
    and spend mental effort each time you scan it.  And this way, you know directly that the button is going to cause a side effect outside your program, which you have to infer (an indirect mental operation) when you
    scan the lambda.

    For this particular example, it might turn out that there could be more
    than one instance of uberzip.exe running at the same time.  Which one
    should be killed, and how do you kill the right one?  With the function,
    you can get those details under control, but I hate to think what might happen to the lambda expression.

    Yes, of course, there can be times when the lambda expression is
    somewhat easy to understand and the side effects are harmless.  In that case, it may be easy enough to grasp quickly that the anonymous function would not benefit from having a name.  So OK, it's not a hard-and-fast
    rule.

    The not knowing which uberzip to kill is a design choice, the bad design
    of the the example is not really the fault of the lambda.

    And in general with naming anything, much has to do with context.
    Buttons perform actions, it should be implicitly understood at the
    button level and in context of where it is placed what it is a button is
    for. A button without a command has no use, so it is understood it needs
    to do something, with good design it would be better to know what the
    buttons purpose is, rather than having to then parse an additional
    function to figure it out.


    Case in point, to solve the subprocess problem, (ignoring the "whys" and
    the lack of usefulness of this example, just grok the pattern, maybe you
    are designing a game of whack-a-mole and are watching 4 things and need
    to do something when you see a change from 1 of them)


    import tkinter as tk
    import subprocess

    main = tk.Tk()
    main.pids = {
    "messages": None,
    "syslog": None,
    "kern": None,
    "user": None,
    }

    tk.Button(
    master=main,
    text="Start tailing logs",
    command=lambda: (
    main.pids.update({"messages" :subprocess.Popen(["tail", "-n", "1",
    "-f", "/var/log/messages"])}),
    main.pids.update({"syslog" :subprocess.Popen(["tail", "-n", "1", "-f",
    "/var/log/syslog"])}),
    main.pids.update({"kern" :subprocess.Popen(["tail", "-n", "1", "-f",
    "/var/log/kern.log"])}),
    main.pids.update({"user" :subprocess.Popen(["tail", "-n", "1", "-f",
    "/var/log/user.log"])}),
    ),
    ).pack()

    tk.Button(
    master=main,
    text="Kill messages tail",
    command=lambda: (
    main.pids["messages"].kill() if main.pids["messages"] else None,
    ),
    ).pack()

    tk.Button(
    master=main,
    text="Kill syslog tail",
    command=lambda: (
    main.pids["syslog"].kill() if main.pids["syslog"] else None,
    ),
    ).pack()

    tk.Button(
    master=main,
    text="Kill kern tail",
    command=lambda: (
    main.pids["kern"].kill() if main.pids["kern"] else None,
    ),
    ).pack()

    tk.Button(
    master=main,
    text="Kill user tail",
    command=lambda: (
    main.pids["user"].kill() if main.pids["user"] else None,
    ),
    ).pack()

    tk.Button(
    master=main,
    text="Kill all tails",
    command=lambda: (
    main.pids["messages"].kill() if main.pids["messages"] else None,
    main.pids["syslog"].kill() if main.pids["syslog"] else None,
    main.pids["kern"].kill() if main.pids["kern"] else None,
    main.pids["user"].kill() if main.pids["user"] else None,
    ),
    ).pack()

    main.mainloop()

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to aapost on Fri Mar 10 19:07:12 2023
    On 3/10/23 18:46, aapost wrote:

            main.pids.update({"messages" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/messages"])}),
            main.pids.update({"syslog" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/syslog"])}),
            main.pids.update({"kern" :subprocess.Popen(["tail", "-n", "1",
    "-f", "/var/log/kern.log"])}),
            main.pids.update({"user" :subprocess.Popen(["tail", "-n", "1",
    "-f", "/var/log/user.log"])}),
        ),

    To pre-emptively address the bug there it would need to be:

    main.pids.update({"messages" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/messages"])}) if not main.pids["messages"] else None, main.pids.update({"syslog" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/syslog"])}) if not main.pids["syslog"] else None, main.pids.update({"kern" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/kern.log"])}) if not main.pids["kern"] else None, main.pids.update({"user" :subprocess.Popen(["tail", "-n", "1", "-f", "/var/log/user.log"])}) if not main.pids["user"] else None,

    which does start to break down readability due to line length, as there
    isn't really an indention rule set for something uncommonly used.

    but some renaming makes the pattern clearer

    pids.update({"messages" :subprocess.Popen(["cmd1"])}) if not
    pids["messages"] else None,
    pids.update({"syslog" :subprocess.Popen(["cmd2"])}) if not
    pids["syslog"] else None,
    pids.update({"kern" :subprocess.Popen(["cmd3"])}) if not pids["kern"]
    else None,
    pids.update({"user" :subprocess.Popen(["cmd4"])}) if not pids["user"]
    else None,

    and adding a comment to say something like
    # starting a series of processes via lambda tuple sequence if process
    not running
    #pattern: p.update({"name":sp.Popen(cmd)}) if not p["name"] else None,

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to 2QdxY4RzWzUUiLuE@potatochowder.com on Fri Mar 10 23:10:47 2023
    On 3/10/2023 10:37 PM, 2QdxY4RzWzUUiLuE@potatochowder.com wrote:
    On 2023-03-10 at 22:16:05 -0500,
    Thomas Passin <list1@tompassin.net> wrote:

    I'd make the pattern in this example even more understandable and less
    error-prone:

    def update_pids(target):
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)}) if not \
    pids[target] else None

    I might be missing something, but how is that more understandable and
    less error prone than any of the following:

    The main point is that there can be an easy-to-read and
    easy-to-understand expression to use in the lambda. I don't care about
    the exact details here. The command wasn't even mine. But you only
    have to scan it and grasp it once, in the def:, and then only until you
    get it working. In the lambda, my suggestions makes it much easier to understand what the lambda is expected to do - and it will still be
    clear in three months when the code gets revisited.


    if not pids[target]:
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)})

    or:

    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids[target] or pids.update({target: subprocess.Popen(cmd)})

    or:

    if pids[target]:
    pass
    else:
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)})

    Picking a nit, that's not a good place to continue that line with the backslash, either.
    I only continued the line for the purposes of the post because it would
    have gotten line wrapped otherwise. If it were my code in a real module
    I would probably have treated it differently.

    IMO, "not pids[target]" should be atomic.

    Again, it wasn't my code. I don't even know if the code fragments would actually work - they were presented as illustrative examples by the OP.
    And anyway, these nits obscure the point of my post. In a nutshell, it
    is to use good naming and encapsulation to make your code as close to self-documenting and as error-free as is reasonably possible, whenever
    you can. Good function names are only one step towards this goal.

    Lambdas often work against this goal, so it's worthwhile spending some
    effort to see how to counteract this tendency.

    And it's a goal. You can never meet it 100%. That's OK. There will
    always be a matter of taste involved. That's OK too. It's still a
    worthwhile goal.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From 2QdxY4RzWzUUiLuE@potatochowder.com@21:1/5 to Thomas Passin on Fri Mar 10 21:37:43 2023
    On 2023-03-10 at 22:16:05 -0500,
    Thomas Passin <list1@tompassin.net> wrote:

    I'd make the pattern in this example even more understandable and less error-prone:

    def update_pids(target):
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)}) if not \
    pids[target] else None

    I might be missing something, but how is that more understandable and
    less error prone than any of the following:

    if not pids[target]:
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)})

    or:

    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids[target] or pids.update({target: subprocess.Popen(cmd)})

    or:

    if pids[target]:
    pass
    else:
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)})

    Picking a nit, that's not a good place to continue that line with the backslash, either. IMO, "not pids[target]" should be atomic.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to aapost on Fri Mar 10 22:16:05 2023
    On 3/10/2023 7:07 PM, aapost wrote:
    which does start to break down readability due to line length, as there
    isn't really an indention rule set for something uncommonly used.

    but some renaming makes the pattern clearer

    pids.update({"messages" :subprocess.Popen(["cmd1"])}) if not
    pids["messages"] else None,
    pids.update({"syslog" :subprocess.Popen(["cmd2"])}) if not
    pids["syslog"] else None,
    pids.update({"kern" :subprocess.Popen(["cmd3"])}) if not pids["kern"]
    else None,
    pids.update({"user" :subprocess.Popen(["cmd4"])}) if not pids["user"]
    else None,

    I'd make the pattern in this example even more understandable and less error-prone:

    def update_pids(target):
    cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
    pids.update({target: subprocess.Popen(cmd)}) if not \
    pids[target] else None

    lambda x: ( # The Tk callback includes an event arg, doesn't it?
    update_pids('messages'),
    update_pids('syslog'),
    # etc
    )

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From aapost@21:1/5 to Thomas Passin on Fri Mar 10 23:15:56 2023
    On 3/10/23 22:16, Thomas Passin wrote:
    On 3/10/2023 7:07 PM, aapost wrote:
    which does start to break down readability due to line length, as
    there isn't really an indention rule set for something uncommonly used.

    but some renaming makes the pattern clearer

    pids.update({"messages" :subprocess.Popen(["cmd1"])}) if not
    pids["messages"] else None,
    pids.update({"syslog" :subprocess.Popen(["cmd2"])}) if not
    pids["syslog"] else None,
    pids.update({"kern" :subprocess.Popen(["cmd3"])}) if not pids["kern"]
    else None,
    pids.update({"user" :subprocess.Popen(["cmd4"])}) if not pids["user"]
    else None,

    I'd make the pattern in this example even more understandable and less error-prone:

    def update_pids(target):
        cmd = ["tail", "-n", "1", "-f", f"/var/log/{target}"]
        pids.update({target: subprocess.Popen(cmd)}) if not \
            pids[target] else None

    lambda x: ( # The Tk callback includes an event arg, doesn't it?
                update_pids('messages'),
                update_pids('syslog'),
                # etc
              )


    So yeah, that's along the same lines. cmd could be in a function, or
    just outside the lambdas. Could also do from subprocess import Popen to
    shorten that, etc.

    The .trace( on the tkinter Vars use events, so you have to do something
    like
    lambda _a, _b, _c: stuff

    But not in the above case. My focus these last couple week hasn't used
    any of those so the specifics aren't as fresh (still heading back in
    that direction), when required python will definitely let you know, lol.

    The additional note in the above is, when taking the def route above,
    the thing you would have to consider is what scope is the dictionary pids?

    Do you need to submit it to the lambda and subsequently the function
    such as
    lambda pids=pids: (
    update_pids("messages", pids),
    update_pids("syslog", pids),

    So that update_pids can access it? Or in your design do you have a
    separate management of it that def update_pids already has access to?
    (either is valid depending on design intent).

    In the direction I am going, in trying to build some compound widgets,
    and I need to grab stuff from one widget and send it to another within
    the same widget group, so in cases where it goes to an external
    function, it has to be sent as an arg through the lambda
    whatever=whatever: ( function(whatever), )

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Passin@21:1/5 to aapost on Sat Mar 11 00:45:01 2023
    On 3/10/2023 11:15 PM, aapost wrote:
    On 3/10/23 22:16, Thomas Passin wrote:
    [...]
    The additional note in the above is, when taking the def route above,
    the thing you would have to consider is what scope is the dictionary pids?

    Do you need to submit it to the lambda and subsequently the function
    such as
    lambda pids=pids: (
        update_pids("messages", pids),
        update_pids("syslog", pids),

    So that update_pids can access it? Or in your design do you have a
    separate management of it that def update_pids already has access to?
    (either is valid depending on design intent).

    It's easy enough to try out. I'm not going to simulate the dictionary,
    because it's basically just another argument, and I assume that your
    example code uses it because it needs to be in that form. For the scope question, see below after the basic example.

    def printme(x): print(x) # Let's not quibble about inline defs!

    printme('xxx')
    xxx
    cmd = lambda x: (
    ... printme('this'),
    ... printme('is'),
    ... printme('a test')
    ... )

    (Yes, I know it's not recommended to assign a lambda to a variable name,
    but I'm doing it here just to demonstrate that the lambda works as desired).

    cmd(2)
    this
    is
    a test
    (None, None, None)

    So it executes the intended steps and returns a tuple of three None
    values, as expected. When used as the target of the "command" arg in
    the Tk control constructor, I presume the tuple would be ignored just as
    a return of None would be. But that would need to be tested.

    If returning a tuple instead of None were to cause a problem, you could
    do this:

    cmd = lambda x: (
    printme('this'),
    printme('is'),
    printme('a test')
    ) and None

    cmd(2)
    this
    is
    a test

    But now you are introducing a construct whose purpose is not totally
    obvious, does not cause any intended effect, and in fact is only
    required by the nature of the code receiving the callback, which you
    cannot know by reading this code. So if this construct turns out to be
    needed, we're forced to take a step away from having the code be as
    direct and understandable as possible. It's still better in that way
    than the earlier illustrations (which BTW would also have required the
    "and None" construct).

    To test out the scope question:

    def printme(x): print(y) # Print some other variable, not "x"
    y = 'I am y' # Will "y" get passed through into the lambda?

    cmd = lambda x: (
    ... printme('this'),
    ... printme('is'),
    ... printme('a test')
    ... ) and None

    cmd(2)
    I am y
    I am y
    I am y

    But maybe the original value of "y" gets baked in at compile time. Let's
    see:

    y = 'I am a post-compile assignment'
    cmd(2)
    I am a post-compile assignment
    I am a post-compile assignment
    I am a post-compile assignment

    Good, the current value of "y" gets used.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)