[Tutor] Which is better style for a function that modifies a list?

dn PyTutor at DancesWithMice.info
Thu Jun 24 02:04:28 EDT 2021


On 24/06/2021 15.51, boB Stepp wrote:
> On Wed, Jun 23, 2021 at 10:09 PM dn via Tutor <tutor at python.org> wrote:
>>
>> On 24/06/2021 12.31, boB Stepp wrote:
>>> On Wed, Jun 23, 2021 at 6:58 PM boB Stepp <robertvstepp at gmail.com> wrote:
>>>>
>>>> I am tremendously enjoying "Practical Programming -- An Introduction
>>>> to Computer Science Using Python 3.6, 3rd Edition" by Paul Gries,
>>>> Jennifer Campbell and Jason Montojo, c. 2017.  I highly recommend it
>>>> to anyone who needs to learn fundamental Python and/or introductory
>>>> computer science.
> 
> [OT:  I finally encountered my first real quibble with the book's
> authors on page 155.  Here they give an example:
> 
> values = [4, 10, 3, 8, -6]
> for i in range(len(values)):
>     print(i)
> 
> where they wish to demonstrate sometimes you need to use the index of
> a list when looping.  That's a good point to make, but of course we
> are more satisfied with using
> 
> for i, value in enumerate(values):
>     [...]
> 
> I actually bothered tonight to file a "suggestion" on the book's website.

+1
(nevertheless still a book that is well-worth the reading time!)


> Another thing they did later on this page was to loop over a list's
> elements and modify them in place.  I felt they should have issued a
> strong warning about the perils of doing such things.]
> 
>>>>
>>>> In its chapter on lists the authors point out that a function like
>>>>
>>>> def remove_last_item(L: list) -> list:
>>>>     """Return list L with the last item removed."""
>>>>
>>>>     del L[-1]
>>>>     return L
>>>>
>>>> does not require the return statement since the original passed in
>>>> list will be modified in place, so there is no need for the function
>>>> to return anything.  I know from experience reading the Tutor and main
>>>> lists that this is a frequent "gotcha" that catches many fellow
>>>> learners.  So I wonder:
>>>>
>>>> 1)  For such a function that mutates the passed-in list is it
>>>> clearer/better style to keep or omit the return statement?  Which
>>>> reads better?
>>>
>>> In light of an earlier thread I started plus now examining all of the
>>> list methods I conclude that the Pythonic way must be to omit the
>>> "return" statement, so that "None" is returned (Or return "None"
>>> explicitly.) whenever a list is modified in place without returning a
>>> new list.
>>
>>
>> +1, why add an unnecessary LoC?
>>
>> Unless... the intent is to return an entirely new list.
>>
>> Except... Zen of Python says "explicit > implicit".
>>
>> ???
> 
> It seems to me that modern Python is just as likely to violate the Zen
> as follow it.  I guess it was never meant to be taken particularly
> literally anyway.

It's amusing. Sadly, what was once a joke between friends is probably
non-PC by today's standards, given that the 'dig' at GvR might now be
taken by some as 'racist'. Sigh!

Then there is the "one way" to do things, which, as you say, must surely
have 'gone by the board' way back in Python 2.n days...


>> I'm concerned that the function offers little, um, functionality. In
>> short, would it not be better to write:
>>
>> del original_list[ -1 ]
>>
>> rather than:
>>
>> remove_last_item( original_list )
> 
> Ah, dn, you must give the authors a break here.  They were merely
> trying to provide a bare-bones example to illustrate a point that they
> wanted the reader to understand.  My question is merely extrapolating
> from their simple example to explore good Python style and practice.

Then the observation applies to the extrapolation, not the text!

Yes, of all people I know that finding 'good' concise-examples is not
easy...


>> There is a branch of programming-thought called "Functional Programming"
>> and one of the aspects of code it seeks to avoid is "side-effects". The
>> 'gotcha' implicit within mutable collections has been discussed in an
>> earlier thread. If we want to remove_/del is it really a side-effect?
> 
> As I gain more knowledge and do more consideration of intentional side
> effects in code -- or worse, unintentional! -- it seems a breeding
> ground for subtle bugs in large, complex code bases.  If I ever get a
> decent handle on Python perhaps I will investigate one of these true
> functional programming languages.

No need. Some intro-reading will explain the concepts. They are easily
taken on-board (as you've already demonstrated). Thereafter, there are
plenty of articles which talk about FP in Python or using Python for FP.

Per the above, the problem for 'purists' attempting to 'push' FP, is
that it is simply not possible. As long as the code includes I/O, it
(technically) violates FP-principles.

However, there are indeed (once again, as you have already observed)
some very valuable lessons to be learned from what the FP-crowd have to
offer. Avoiding whole categories of error, eg mutating mutables within a
sub-namespace, amongst them!

Whilst on the subject (and I may have posed this question before) how
should one document such, in order to prevent later readers/maintainers
of the code from falling into the gaping-jaws of this 'gotcha'?

Modifying the code above:

>>>> def remove_last_item():
>>>>     """Remove last item from the list."""
>>>>
>>>>     del L[-1]

we have no parameters, and no return-value. Thus no 'typing' hints
appear within the def. Accordingly, we need 'manual documentation'.
Whereas the original version (with return-value list) is fully
documented, as-is. How do people (or is that: do people) document that
they are expecting (and messing with) a mutable within a function?


PS while we're being snippy about the range()/enumerate() idiom (above),
may I point-out that PEP-8 implies that "L" names a class, not a list.
Cue comments about the intent of PEP-8...


>> Perhaps if we were considering the wider objective(s), and thereafter
>> why we are removing the list's (or string's, or ...) last element, ie
>> what other functionality we will be asking of the 'list', our thinking
>> may lead us to create a custom class/object, sub-classed from list (or ...)?
>>
>> Whilst this seems an OTT solution to the bare-scenario painted above,
>> assuming other justification, a custom class would enable:
>>
>> particular_purpose_list = CustomClass( etc )
>> ...
>> particular_purpose_list.remove_last_item()
>>
>> Because the internal "state" of our object-instance is maintained within
>> "self", there is now little doubt of what is happening, and to what.
>> There is no 'gotcha' (assuming good practice code). Plus, it elevates
>> the suspicion of a side-effect to being a desired-impact. All of which
>> results in the code/data-structure being more tightly testable because
>> it implements a more bounded purpose.
>>
>>
>> Your thoughts?
> 
> I find no disagreement here *if* a worthwhile purpose for having such
> a method existed for a custom data type.  And more than likely such a
> remove the last item from such a data type might not even be part of
> the public API but be a hidden implementation detail to the user of
> the class.  Still, though, the tests better be good!

Which brings us right back to the concerns about using mutables - and
the problem(s) of adequately testing callables that employ such. How
could one write tests which competently-test the straw-man version (no
parameters, no return-value) outlined above. The function cannot
(really) be unit-tested in-isolation. Thus, its use is highly-dependent
upon the calling environment (and namespace), which opens the topic of
cohesion/coupling...

You've raised valid concerns, but have you developed a head-ache yet?
-- 
Regards,
=dn


More information about the Tutor mailing list