2

I have two classes in separate files, a.py and b.py.

# a.py
import logging

LOG = logging.getLogger(__name__)

class A:
    def name(self):
        # Do something
        LOG.debug("Did something") # LOG reads __name__ and print it.

I can not modify a.py in any way.

# b.py
import a

class B(A):
    pass

The log output is:

DEBUG:<TIME> called in a, Did something

I want to change __name__ so that a child class's call should log called in b.

My project logs its module name as part of the output. However, when I inherit a class and call the parent class method, I cannot know it is called from A or B because it only shows the parent's module name. How can I change it? Or, some other way to avoid this?

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
osflw
  • 79
  • 1
  • 8
  • Surely you should be referring to `self.__name__` not `__name__`? – smci Sep 10 '21 at 01:35
  • @smci. No, OP wants the module namespace – Mad Physicist Sep 10 '21 at 01:36
  • @MadPhysicist Thanks. @smci I want to solve without changing log in logging which reads `__name__` of module. – osflw Sep 10 '21 at 01:37
  • 1
    It seems like trying to alter `__name__` which is part of python's module import system is a bad way to get functionality like this. Why not just use a property on the instance or class? – Mark Sep 10 '21 at 01:50
  • @Mark. It's not part of the import system exactly. It's not used once it's set as far as I'm aware. The problem is that `A` is unalterable. – Mad Physicist Sep 10 '21 at 03:15
  • @MadPhysicist I guess I would be more precise to say it is part of the *expected* module system, in that anyone reading your code would expect certain conventions when using `__name__`. For example the convention of using `logging.getLogger(__name__)`. I think tampering with this violates the principle of least surprise. – Mark Sep 10 '21 at 03:26
  • 1
    @Mark. I agree. But you can alter other things in a careful manner. My answer suggests an alternative that does not directly violate least surprise. – Mad Physicist Sep 10 '21 at 03:28
  • @osflw. Let me know if my edit is acceptable. You shouldn't leave "Update" or "Edit" in your question except in very rare circumstances. The idea that your prior work is valuable and shouldn't be clobbered by improvements is totally false. There's an edit history we could look at it we wanted to see a more incomplete version of your question. – Mad Physicist Sep 10 '21 at 03:40
  • @MadPhysicist Thanks for your editing. It's more readable. I thought I should leave my edit history for the answers made before it. – osflw Sep 10 '21 at 04:12
  • @osflw. Only if your edit fundamentally invalidates some answers is that a valid concern. In this case, it's something that clearly should have been mentioned in the question to begin with. – Mad Physicist Sep 10 '21 at 04:12
  • @MadPhysicist I got it. Thanks. – osflw Sep 10 '21 at 04:14

2 Answers2

2

There is a way to do it, but it's more involved than you might think.

Where does the name __name__ come from? The answer is that it's a global variable. This is to be expected, since the global namespace is the module namespace. For example:

>>> globals()['__name__']
'__main__'

If you look up callables in the Standard Type Hierarchy of python's Data Model documentation, you will find that a function's __globals__ attribute is read-only. That means that the function A.name will always look up its global attributes in the namespace of module a (not that the module namespace itself is read-only).

The only way to alter globals for a function is to copy the function object. This has been hashed out on Stack Overflow at least a couple of times:

Copying classes has come up as well:

Solution 1

I've gone ahead and implemented both techniques in a utility library I made called haggis. Specifically, haggis.objects.copy_class will do what you want:

from haggis.objects import copy_class
import a

class B(copy_class(A, globals(), __name__)):
    pass

This will copy A. All function code and non-function attributes will be referenced directly. However, function objects will have the updated __globals__ and __module__ attributes applied.

The major problem with this method is that it breaks your intended inheritance hierarchy slightly. Functionally, you won't see a difference, but issubclass(b.B, a.A) will no longer be true.

Solution 2 (probably the best one)

I can see how it would be a bit cumbersome to copy all the methods of A and break the inheritance hierarchy by doing this. Luckily, there is another method, haggis.objects.copy_func, which can help you copy just the name method out of A:

from haggis.objects import copy_func
import a

class B(A):
    name = copy_func(A.name, globals(), __name__)

This solution is a bit more robust, because copying classes is much more finicky than copying functions in python. It's fairly efficient because both versions of name will actually share the same code object: only the metadata, including __globals__, will be different.

Solution 3

You have a much simpler solution available if you can alter A ever so slightly. Every (normal) class in python has a __module__ attribute that is the __name__ attribute of the module where it was defined.

You can just rewrite A as

class A:
    def name(self):
        # Do something
        print("Did in %s" % self.__module__)

Using self.__module__ instead of __name__ is roughly analogous to using type(self) instead of __class__.

Solution 4

Another simple idea is to properly override A.name:

class B(A):
    def name(self):
        # Do something
        LOG.debug("Did something")

Of course I can see how this would not work well if # Do something is a complex task that is not implemented elsewhere. It's probably much more efficient to just copy the function object directly out of A.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Thanks for your detailed solution. Because I cannot change `a.py` (added in the text), according to your answer, I have to copy all methods in A to B. – osflw Sep 10 '21 at 02:53
  • @osflw. Yes, but I already wrote the function for that, which is not very long: https://github.com/madphysicist/haggis/blob/f7dacb0ce6acec5d8665ea7b85d6866174da5849/src/haggis/objects.py#L145 – Mad Physicist Sep 10 '21 at 03:10
  • @osflw. You can avoid copying *all* the methods. The problem is that `A` is not implemented for the flexibility you want, so you can either re-implement it, or work around it. You aren't being given the choice. – Mad Physicist Sep 10 '21 at 03:17
  • @osflw. I've updated my answer. You only really need to copy one method. It's like a more efficient form of overriding: the code object will be shared, and pretty much only the `__globals__` and `__module__` attributes are changed, which is exactly what you are looking for. – Mad Physicist Sep 10 '21 at 03:23
  • Thanks, I got your point. It was very helpful. I think the fastest solution is overriding the methods that use LOG. – osflw Sep 10 '21 at 04:28
  • @osflw. Feel free to select the answer whenever you're ready – Mad Physicist Sep 10 '21 at 04:29
1

Solution:

Try using inspect.stack:

import inspect
class A:
    def __init__(self):
        pass
    def name(self):
        # Do something
        print("Did in %s" % inspect.stack()[1].filename.rpartition('/')[-1][:-3])

And now running b.py would output:

Did in b

Explanation:

inspect can inspect live objects in python scripts, as metioned in the docs:

Return a list of frame records for the caller’s stack. The first entry in the returned list represents the caller; the last entry represents the outermost call on the stack.

Also it mentioned that it outputs a NamedTuple like:

FrameInfo(frame, filename, lineno, function, code_context, index)

So we could just get the live filename with indexing the second element of this tuple, that would give the full path, so we would have to split it up the path and extract the filename from the full path using str.rpartition.

Edit:

For not editing a.py, you could only try:

b.py:

import inspect
exec(inspect.getsource(__import__('a')))
class B(A):
    pass

And it would give:

Did in b
U13-Forward
  • 69,221
  • 14
  • 89
  • 114