Атрибут класса, которого нет на подклассах - PullRequest
1 голос
/ 04 мая 2020

В основном для забавы, чтобы помочь с эргономикой парсера, который я пишу, я бы очень хотел переопределить класс '__getattr__, не делая того же для его подклассов. Для объяснения приведем код:

class AttrMeta(type):
    def __getattr__(self, name):
        if name == "A":
            return "hi there."

class Main(metaclass=AttrMeta):
    pass

class Subclass(Main):
    pass

print(Main.A)  # => hi there.
print(Subclass.A) # => hi there.
print(Main().A) # => AttributeError: 'Main' object has no attribute 'A'
print(Subclass().A) # => AttributeError: 'Subclass' object has no attribute 'A'

Это все нормально, но я бы хотел, чтобы Subclass не наследовал __getattr__. Для этого первым делом я попытался переопределить __new__ в AttrMeta следующим образом:

# in AttrMeta...
def __new__(mcs, name, bases, namespace, **kwargs):
    if bases:
        return type.__new__(type, name, bases, namespace, **kwargs) 
    return type.__new__(mcs, name, bases, namespace, **kwargs)

Идея состояла в том, чтобы убрать метакласс из подклассов Main, которые обнаруживаются глядя на bases. Это привело к бесконечным рекурсиям при создании class Subclass(Main), что имеет смысл - как вы могли иметь такую ​​разорванную цепочку наследования?

Моя следующая идея состояла в том, чтобы удалить __getattr__ вручную:

# in AttrMeta...
def __new__(mcs, name, bases, namespace, **kwargs):
    new_cls = type.__new__(mcs, name, bases, namespace, **kwargs)
    if bases:
        del new_cls.__getattr__
    return new_cls

Что ж, в ретроспективе очевидно, что вы не можете этого сделать: вы получаете AttributeError: __getattr__. Это потому, что Subclass.__getattr__ связан с AttrMeta - на самом деле, "__getattr__" not in dir(Subclass) имеет место.

В этот момент я сказал себе: эй. Тим Питерс сказал, что люди, которым нужны метаклассы, знают это на самом деле, и я просто не из этих людей. На самом деле мне совсем не нужно писать этот парсер, я просто пытаюсь, вы знаете, немного отвлечься во время карантина. Возможно, я просто сделаю это, назначив __getattr__ в классе и посмотрим, что произойдет. Итак, я попробовал:

class Main:
    pass

class Subclass(Main):
    pass

def __getattr__(self, name):
    if name == "A":
        return "hi there."

Main.__getattr__ = __getattr__

Main.A # raises AttributeError: type object 'Main' has no attribute 'A'

На данный момент у меня закончились идеи. Я, вероятно, просто сдамся и позволю Subclass иметь атрибут getter. Я не хочу переопределять __getattr__ на каждом Subclass, поскольку их много, но мне было любопытно, есть ли какие-нибудь идеи там. Спасибо за чтение этого длинного поста.

Ответы [ 2 ]

0 голосов
/ 05 мая 2020

Нельзя сделать атрибут или метод «не унаследованным» в Python - вы можете просто затенять ранее существующий метод или атрибут на подклассах, и вы можете использовать его для внедрения замены, которая вызовет AttributeError ,

В этом случае, начиная с Python 3.6, вам даже не нужен метакласс - вы можете использовать метод __init_subclass__, который запускается при создании подкласса (но не текущего класса), заменить __getattr__ на тот, который поднимет:

class Main:
    def __getattr__(self, attr):
        if attr == "A":
            return "A"
        raise AttributeError()

    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        def __getattr__(self, attr):
            # blocks the original getattr
            raise AttributeError(attr)

        # optional check: allow subclasses to implement their own __getattr__ methods:
        if "__getattr__" in cls.__dict__:
            return
        cls.__getattr__ = __getattr__


class SubClass(Main):
    pass

И это запустится:


In [95]: Main().A                                                                                              
Out[95]: 'A'

In [96]: SubClass().A                                                                                          
---------------------------------------------------------------------------
AttributeError  

Однако вы добавляете __getattr__ к самому метаклассу - (позор я, я только что видел это сейчас) - так что атрибуты динамического c могут показаться на классах, а не только на экземплярах.

Более простой способ go их - отметить оригинальную "Основу" класс с некоторым атрибутом и в метаклассе __getattr__ проверьте, вызывается ли getattr для этого базового класса.

(Подход __init_subclass__ не будет работать, поскольку сам метакласс такой же, нет «подкласса метакласса», который будет использоваться подклассами основного класса)




mark = "_AttrBase"

def find_marked(cls):
    for sclass in cls.__mro__:
        if sclass.__dict__.get(mark, False):
            return sclass
    return None

class AttrMeta(type):
    def __new__(mcls, name, bases, namespace, **kwd):
        cls = super().__new__(mcls, name, bases, namespace, **kwd)
        if not find_marked(cls):
            # This line injects a getattr on the instances as well, so 
            # that instances have the same behavior for these as the classes:
            cls.__getattr__ = lambda self, attr: getattr(self.__class__, attr)
            setattr(cls, mark, True)
        return cls

    def __getattr__(cls, attr):
        if find_marked(cls) is not cls:
            raise AttributeError(attr)
        if attr == "A":
            return "A"
        raise AttributeError(attr)

class Main(metaclass=AttrMeta):
    pass

class SubClass(Main):
    pass
0 голосов
/ 04 мая 2020

Ах, я понял, по крайней мере, один способ, который работает. Идея состоит в том, чтобы добавить новый метакласс, например так:

class AttrMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        if bases:
            mcs = NoMoreMeta
        return type.__new__(mcs, name, bases, namespace, **kwargs)

    def __getattr__(self, name):
        if name == "A":
            return "hi there."


class NoMoreMeta(AttrMeta):
    def __getattr__(self, name):
        if name == "A":
            raise AttributeError(name)


class Main(metaclass=AttrMeta):
    pass


class Subclass(Main):
    pass

Тогда Main.A работает, как указано выше, но Subclass.A вызывает соответствующую ошибку.

...