To restate the motivation: the hope is that there is a potentially large
benefit of being able to more easily refactor code with something like a
nameof().

I am going to make the claim that:

   1. this benefit is actually minimal and does not address a lot of other
   existing refactoring problems/chores, and
   2. comes at the cost of a possible loss of readability, and therefore
   isn't worth the trade

Consider the following typical code (I hope it is nice; I feel like I can
certainly read it):

import logging
import functools

class FunctionLogger:
    """Records use of a function.
    The optional `log` parameter is to be a callable that accepts a string.
    It is logging.info by default.
    """
    log = logging.info
    def __init__(self, log=None):
        if log is not None:
            self.log = log
    def __call__(self, func):        func_name = func.__name__
        @functools.wraps(func)        def wrapper(*args, **kwargs):
        try:                self.log(f"called {func_name!r}")
  except:                cls_name = type(self).__name__
logging.exception(f"failed to log {func_name!r} call; is
{cls_name}.log set correctly?")            finally:
return func(*args, **kwargs)        return wrapper

Let's think through: what refactoring must occur to change the "log"
parameter to something else? And, how readable does the code remain
afterward?

At least some of the code could certainly benefit from being more
easily refactored (by an IDE) with nameof(), but how much?

import logging
import functools

class FunctionLogger:
    # NOTE: this docstring will result in a runtime error, so can't
even use nameof() here to help ourselves
    f"""Records use of a function.
    The optional `{nameof(FunctionLogger.log)}` parameter is to be a
callable that accepts a string.
    It is logging.info by default.
    """
    log = logging.info  # no need for nameof() here, but see below *
    def __init__(self, log=None):  # IDE will refactor the init
signature SEPARATELY (see below *)
        if log is not None:  # no help from nameof() in __init__ body
            # IDE will get this one just fine anyway; shouldn't use
nameof() in this situation
            # using it actually hurts readability
            setattr(self, nameof(self.log), log)
    def __call__(self, func):
        func_name = nameof(func)  # it's different, but NOT an
improvement over func.__name__
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                self.log(f"called {func_name!r}")
            except:
                cls_name = nameof(type(self))  # different, but NOT an
improvement over type(self).__name__
                log_param_name = nameof(self.log)  # ok, HERE is a
place we are definitely going to benefit from nameof()
                #  readability of next line might actually be slightly
improved...? But perhaps it's also worse...?
                # but the IDE will refactor next line automatically,
which is handy.                logging.exception(f"failed to log
{func_name!r} call; is {cls_name}.{log_param_name} set correctly?")
        finally:                return func(*args, **kwargs)
return wrapper

* For the class level member and init signature/body: the IDE won't know to
include the class member and the init signature/body in a refactor of a
member-level variable, and this problem isn't helped by nameof(). Multiple
refactoring chores have to be completed to change the parameter name: 1.
the init signature, 2. the class level variable, 3. object level variable,
4. the class docstring, 5. all of the f strings. Using a nameof() only
totally prevents one of them (the f strings; it can't help with the
docstring).

So I guess there is SOME benefit here. But I think it comes at the cost of
some potential loss of readability, and there are some major places in
which nameof() doesn't bring any help at all (looking at you, class
docstring).

And there is a ready alternative: use a global if you think you might
rename your parameters later (see code at bottom). This rolls TWO of the
refactoring chores into one (the f strings and the docstring).

*Bottom line: refactoring remains a chore. The win is minimal. Do nothing
option is preferred.*

import logging
import functools

_FOO = "foo"  # haven't decided on .foo member name yet...


class FunctionLogger:
    # with a global, this docstring will work!
    f"""Records use of a function.
    The optional `{_FOO}` parameter is to be a callable that accepts a string.
    It is logging.info by default.
    """
    log = logging.info

    def __init__(self, log=None):
        if log is not None:
            self.log = log

    def __call__(self, func):
        func_name = func.__name__

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                self.log(f"called {func_name!r}")
            except:
                cls_name = type(self).__name__
                # employ the global below for the error message,
mischief managed
                logging.exception(f"failed to log {func_name!r} call;
is {cls_name}.{_FOO} set correctly?")
            finally:
                return func(*args, **kwargs)
        return wrapper


---
Ricky.

"I've never met a Kentucky man who wasn't either thinking about going home
or actually going home." - Happy Chandler
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/JOUJOABM5KX2VJZMBPCXTUFLYZXBVYXB/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to