I don't think that what you're doing is good style. I think you're adding a lot of extra complexity and places for things to go wrong, or become hard to understand, or whatever. I don't think you lose anything by just making very small methods that call super
and your special method.
With that out of the way, here is (what I propose) is a better and safer way to do it
What you're trying to do is (semi-automatically) change how certain data elements are retrieved and executed when called. To me, this sounds like a descriptor. Descriptors allow you to override how you set, get, or delete an attribute on an object, like so (example taken from above documentation link):
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print('Retrieving', self.name)
return self.val
def __set__(self, obj, val):
print('Updating', self.name)
self.val = val
>>> class MyClass(object):
... x = RevealAccess(10, 'var "x"')
... y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
For yours, what you need is a descriptor that auto-calls super
for you. Note - its important to use super
to handle cases of multiple-inheritance; as pointed out in the comments, your current implementation will only ever call the deque
method directly, instead of taking the time to deal with __mro__
. In the implementation below, we:
- Use the
__get__
method to automatically get the class (objtype
) andself
(obj
) of the instance (not the descriptor). This provides enough information for us to callsuper()
. - Use
getattr
to get the attribute in question from the result ofsuper
, and lets us ignore the complexities of__mro__
. - Create a wrapper function (don't forget to use
functools.wraps
) that calls the wrapped function with the appropriate arguments, and then the extra function you specified. __get__
returns the wrapped function, and nowMyClass.<<attr_name>>
will be that function
from functools import wraps
class AutoSuperDescriptor(object):
def __init__(self, attr_name, extra):
self.attr_name = attr_name
self.extra = extra
def __get__(self, obj, objtype):
wrapped_function = getattr(
super(objtype, obj), self.attr_name
)
extra_bits = self.extra
@wraps(wrapped_function)
def wrapper(yourself, *args, **kwargs):
result = wrapped_function(
yourself, *args, **kwargs
)
extra_bits(yourself)
return result
return wrapper
You create your descriptor by providing enough information to find the overwritten attribute, as well as the function we want to add on to the end. For example, you might do this:
class FileMirroredDeque(deque):
append = AutoSuperDescriptor("append", updates_file)
If that meets your need then you're done.
But wait, there's more
Its pretty tedious and error-prone to do this by hand for each method you want to apply this to. Wouldn't it be nice if there was a way to just tie into the creation of a class and have it do this for us?
There is, and the answer is metaclasses. Metaclasses are a really complex, and really powerful, tool in the Python toolbox. Anytime you find yourself using them you should think really, really hard about if that is the best way to do it. I don't think this is one of those times (see start of this answer), but if you wanted to do that, I think we'd have to do something like this:
- Define a way to identify the elements that should be overridden at class initialization
- Define a way to identify what extra behavior needs to be added to them
- Add the behavior.
The third one is easy - we implemented it with our descriptor up above.
The first two are a little more complicated, and have a few options. We could choose between adding kwargs
to the class initialization:
class FileMirroredDeque(
deque,
metaclass=SuperChargerMetaclass,
methods_to_supercharge = [""],
method_to_supercharge_with = "method_name"
):
I think this would get clunky and obscure things. Instead, I'm going to propose that we define a special attribute (I used the __<<name>>__
naming convention, which is a no-no, but I thought made it clearer for demonstrative purposes) to list the attributes to override, and then set an attribute on the extra method itself to find it. The final implementation of the class looks like so:
def supercharger(func):
func.__is_supercharger = True
return func
class FileMirroredDeque(
deque, metaclass=SuperChargerMetaClass
):
__supercharge__ = [
"__delitem__",
"__iadd__",
"__imul__",
"__setitem__",
"append",
"appendleft",
"extend",
"extendleft",
"insert",
"pop",
"popleft",
"remove",
"reverse",
"rotate",
]
@supercharger
def _update_file(self):
with open(self.path, "w") as f:
json.dump(list(self), f, indent=self._indent)
def __init__(
self,
cache_path,
maxlen=None,
clean=False,
file_indent=None,
):
super().__init__((), maxlen)
self.path = cache_path
self._indent = file_indent
self._bak_file = None
if clean:
self._update_file() # Overwrite contents of file.
return # Don't import data from file.
with open(
self.path, "a+"
) as f: # Creates file if it doesn't exist
f.seek(
0, 0
) # Seek back to beginning of file for json decode.
contents = f.read()
try:
contents = json.loads(contents or "[]")
self.extend(
contents
) # initializes internal list with persistent data or empty list.
except json.decoder.JSONDecodeError:
new_file = (
os.path.basename(self.path)
+ datetime.datetime.utcnow().strftime(
"_%Y_%m_%d_%H_%M_%S"
)
+ ".bak"
)
new_path = os.path.join(
os.path.dirname(self.path), new_file
)
print(
f"File: {self.path} was not valid JSON. It will be copied to {new_path} and a new "
f"file will be created."
)
shutil.copyfile(self.path, new_path)
self._bak_file = new_path
But how do we actually get this to happen? Like so!
class SuperChargerMetaClass(type):
_SUPERCHARGER_ATTRNAME = "__supercharge__"
@classmethod
def _get_supercharger(cls, klass_attrs):
return next(
method
for name, method in klass_attrs.items()
if getattr(method, "__is_supercharger", False)
)
def __init__(cls, name, bases, attrs):
supercharger = cls._get_supercharger(attrs)
superchargees = attrs.get(
cls._SUPERCHARGER_ATTRNAME, ()
)
for method_to_supercharge in superchargees:
attrs[
method_to_supercharge
] = AutoSuperDescriptor(
method_to_supercharge, supercharger
)
return super().__init__(name, bases, attrs)
In the __init__
method (__new__
would probably work too, but I ended up getting this to work as __init__
so I left it, we get the "supercharger" method to append to our functions, and our list of "superchargees" that need the behavior added to them. From there, its just a matter of making the relevant attribute look to our descriptor instead of whatever (if anything) was there before.