简体   繁体   中英

Metaclasses: why method is not inherited from Base class?

I'm trying to understand metaclass black magic in Python. AFAIK, metaclasses can be used, for example, to ensure that some method is implemented in derived class, but I have a problem grandchildrens . It seems that I need to explicitly implement all required derived methods even if there is no reason(?) to do that.

Look at this:

~ $ ipython
Python 3.6.5 (default, Apr 14 2018, 13:17:30) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: class Meta(type):
   ...:     def __new__(cls, name, bases, body):
   ...:         if 'some_method' not in body:
   ...:             raise AttributeError('some_method should be implemented')
   ...:         return super().__new__(cls, name, bases, body)
   ...: 
   ...: class Base(metaclass=Meta):
   ...:     def some_method(self):
   ...:         print('Yay')
   ...: 
   ...: class Derived(Base):
   ...:     pass
   ...: 
   ...: 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-1-51c072cc7909> in <module>()
      9         print('Yay')
     10 
---> 11 class Derived(Base):
     12     pass

<ipython-input-1-51c072cc7909> in __new__(cls, name, bases, body)
      2     def __new__(cls, name, bases, body):
      3         if 'some_method' not in body:
----> 4             raise AttributeError('some_method should be implemented')
      5         return super().__new__(cls, name, bases, body)
      6 

AttributeError: some_method should be implemented

As far I understand this should not happen, because some_method should be derived from Base class like here:

In [3]: class Base:
   ...:     def some_method(self):
   ...:         print('Yay')
   ...: 
   ...: class Derived(Base):
   ...:     pass
   ...: 
   ...: 

In [4]: Derived().some_method()
Yay

This is expected? If yes, could you give me some hint how to fix this behaviour, so I wouldn't need to reimplement methods?

You seem to be trying to reinvent the wheel that is the abc module and its @abstractmethod decorator. I'm not sure why you want to do that, but if you do, you should look at its source (which is linked from the docs).

Your solution checks that the required name is implemented in the body of the class. That won't be true for inherited methods, but you seem to know that, so I won't explain. What you want to know is: what can you do instead? You need to check for inherited methods explicitly.

The way abc does it is pretty simple: before checking whether all abstract methods are implemented, it calls getattr on each base . (The best way to simulate what getattr will do on the class is to call getattr , after all.) Anything that's found there doesn't need to appear in the body.

Your use case—with a static single required method instead of a dynamic set of methods that have to be harvested from the output of a decorator—is even simpler:

if (not any(hasattr(base, 'some_method') for base in bases)
    and 'some_method' not in body):

Inheritance isn't something that injects a definition into a class. Rather, it allows the lookup for an attribute to proceed beyond the immediate class if it is not defined locally.

In the absence of your metaclass, a call to Defined().some_method() proceeds roughly as follows:

  1. An instance of Defined is created.
  2. An attribute named some_method is looked for that instance's __dict__ attribute, and is not found.
  3. some_method is looked for in Derived.__dict__ and not found.
  4. some_method is searched for in the __dict__ attributes of the classes in Derived 's based classes, starting with Base .
  5. Base.__dict__['some_method'] is found, and its value is called with the instance of Derived as its implicit first argument.

With your metaclass, Derived is created with the same metaclass used by its base class, meaning Meta.__new__ (not type.__new__ ) is used to create Derived . Since some_method is not in the body of the class statement, it does not appear in the dict object passed to Meta.__new__ , resulting in the AttributeError being raised. (Note that you should be raising NotImplemented , not AttributeError , as you aren't attempting to access any particular attribute yet.)

body contains what is defined in the class you're defining. In your case, some_method isn't defined in Derived , it doesn't exist in it.

When you use Derived().some_method() (or even Derived.some_method ), it will perform a lookup. It will not find it in Derived, but then it will try the parent classes, aka the bases, using the MRO (Method Resolution Order).

See the params in def __new__(cls, name, bases, body): , the bases are separate from body which is what you defined in your class, before MRO kicks in.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM