[Tutor] How is "set(ls).add('a') evaluated? [Was: Re: A program that can check if all elements of the list are mutually disjoint]

boB Stepp robertvstepp at gmail.com
Sun Jun 6 13:22:12 EDT 2021


On Sun, Jun 6, 2021 at 3:53 AM dn via Tutor <tutor at python.org> wrote:
>
> On 06/06/2021 14.19, boB Stepp wrote:
> > Ah, I finally see the light, dn!
>
> Regret to advise that such wisdom came from @David, not
> this-David?self.David aka "dn".

My sincere apologies, David (*not* dn)!  I cannot seem to keep the
"bouncing cats" segregated from the " dancing (with) mice" in my head,
not to mention the same first names!

> That said:-
>
>
> > On Sat, Jun 5, 2021 at 9:08 PM boB Stepp <robertvstepp at gmail.com> wrote:
> >> On Sat, Jun 5, 2021 at 8:57 PM David <bouncingcats at gmail.com> wrote:
> >>> On Sun, 6 Jun 2021 at 11:37, boB Stepp <robertvstepp at gmail.com> wrote:
> >>>
> >>>> I have not played around with set's methods and operators to date, so
> >>>> while trying to understand this code I tried out different things in
> >>>> the interpreter.  Along the way I tried something and it surprised me:
> >>>>
> >>>> Python 3.9.5 (tags/v3.9.5:0a7dcbd, May  3 2021, 17:27:52) [MSC v.1928
> >>>> 64 bit (AMD64)] on win32
> >>>> Type "help", "copyright", "credits" or "license" for more information.
> >>>>>>> ls = ["amba", "Joy", "Preet"]
> >>>>>>> z = set(ls).add('a')
> >>>>>>> z
> >>>>>>> print(z)
> >>>> None           # This surprised me.  I was expecting {'amba', 'Joy',
> >>>> 'Preet', 'a'}.
> >>>
> >>> The set() object has an add() method that modifies
> >>> its object and returns None.
> >>
> >> I understand this.  But for "set(ls).add('a')" I am thinking the
> >> following sequence of events occur:
> >>
> >> 1)  "set(ls)" creates the set-type object "{'amba', 'Joy', 'Preet'}.
> >> 2)  ".add('a')" method is called on this set object, resulting in the
> >> new set object "{'amba', 'Joy', 'Preet', 'a'}
> >> 3)  "z" is now assigned to point to this resultant object.
> >
> > (3) is *not* correct.  "z" is assigned to what the add() method is
> > returning, dn's point.
> >
> >>>>> zz = set(ls).union('a')
> >>>>> zz
> >> {'amba', 'Joy', 'Preet', 'a'}
> >>
> >> Why are these different in their results?
> >
> > Here according to the docs the union() method:
> >
> > "Return a new set with elements from the set and all others."
> >
> > Caught up in something elementary once again.  Sigh!
>
> and later:-
> > <Gripe>
> > I find it really hard to remember which functions and methods return
> > "None" and operate by side effect with those that return a new object.
> > This is especially difficult for me as I so often have long breaks
> > between using/studying Python.  I wish that Python had some sort of
> > clever syntax that made it *obvious* which of the two possibilities
> > will occur.
> > </Gripe>
> > ~(:>))
>
>
> It's "pythonic"!
> (hey, don't shoot me, I'm only the piano-player)
>
>
> When we work with objects, we can access their attributes using "dotted
> notation". (remember though: a method (function) is also an attribute)
> Thus, we are able to access a value directly, without using "getters",
> and to change its value without "setters". Because we are changing an
> attribute's value, the modified-value is the result of such. Thus, a
> return of None.
>
> (see also "Functional Programming" and the concept of "side effects")
>
>
> We need to 'tune in' to the documentation to settle such confusion
> (thereafter clarifying any doubt by experimenting with the REPL). In
> this case:
>
> Python 3.9.5 (default, May 14 2021, 00:00:00)
> [GCC 10.3.1 20210422 (Red Hat 10.3.1-1)] on linux
> Type "help", "copyright", "credits" or "license" for more information.
> >>> help( set )
>
> Help on class set in module builtins:
>
> class set(object)
>  |  set() -> new empty set object
>  |  set(iterable) -> new set object
>  |
>  |  Build an unordered collection of unique elements.
>  |
>  |  Methods defined here:
>  |
>  |  __and__(self, value, /)
>  |      Return self&value.
>  |
>  |  __contains__(...)
>  |      x.__contains__(y) <==> y in x.
>  |
> ...
>  |  add(...)
>  |      Add an element to a set.
>  |
>  |      This has no effect if the element is already present.
> ...
>  |  __len__(self, /)
>  |      Return len(self).
> ...
>  |  update(...)
>  |      Update a set with the union of itself and others.
> ...
>
> - where we can see that the set method "and" will return a new set
> (logical given the result will be neither self nor the other set),
> whereas set.add() will modify the contents of this existing set (stated
> more categorically in the explanation for set.update() ):

Ah, but here's the rub.  The help(set) and online docs when they
describe the add() method do *not* _explicitly_ state what the method
returns.  When reading this originally I naively thought it returned
the updated set object.  OTOH, the union() method explicitly states
what is returned.  I guess I am just dense and should have realized
that if no *explicit* mention in the help and docs is made regarding a
method's return value then it by default will return "None".  This now
is making me even more of a believer in using type annotations and
making it *very explicit* for dense people like me what the return
value is for each method and function.  For me the docs and help()
would read better if in every case for every function and method its
return value is always explicitly stated.

Fortunately David (bouncingcats) made me re-read the docs in light of
his very helpful comment and (finally!) saw my mistake.

> Just to add to your gripe, there is (at least) one function which
> provides a result AND a (by-design) internal (to the set) "side-effect":
>
> s = { 1, 2 }
> what = s.pop()
>
> ...from help( set )...
>  |  pop(...)
>  |      Remove and return an arbitrary set element.
>  |      Raises KeyError if the set is empty.

Good point!  But here it is made explicit to the reader *both* the
return value and the side effect.

boB Stepp


More information about the Tutor mailing list