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?
CodePudding user response:
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
.
CodePudding user response:
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