2013-11-20 6 views
1

Позвольте мне начать с того, что я понимаю, как слоты и метаклассы работают на Python. Играя с ними, я столкнулся с интересной проблемой. Вот минимальный пример:Можно ли добавить __slots__ в декоратор класса, который уже определяет __slots__?

def decorator(cls): 
    dct = dict(cls.__dict__) 
    dct['__slots__'] = ('y',) 
    return type('NewClass', cls.__bases__, dct) 

@decorator 
class A(object): 
    __slots__= ('x',) 
    def __init__(self): 
     self.x = 'xx' 

A() 

В результате получается следующее исключение:

Traceback (most recent call last): 
    File "p.py", line 12, in <module> 
    A() 
    File "p.py", line 10, in __init__ 
    self.x = 'xx' 
TypeError: descriptor 'x' for 'A' objects doesn't apply to 'NewClass' object 

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

Конечно, в этом простом примере, либо из следующих двух определений decorator будет работать вокруг проблемы:

def decorator(cls): 
    dct = dict(cls.__dict__) 
    dct['__slots__'] = ('y',) 
    return type('NewClass', (cls,) + cls.__bases__, dct) 

def decorator(cls): 
    class NewClass(cls): 
     __slots__ = ('y',) 
    return NewClass 

Но эти обходные не совсем он такой же, как оригинал, так как они оба добавляют A в качестве базового класса. Они могут терпеть неудачу в более сложной настройке. Например, если дерево наследования более сложное, вы можете столкнуться с следующим исключением: TypeError: multiple bases have instance lay-out conflict.

Так что мой очень специфический вопрос:

ли есть способ создать новый класс, через вызов type, который изменяет атрибут существующего класса __slots__, но не добавляет существующий класс как базовый класс нового класса?

Edit:

Я знаю, что строгие Метаклассы другую работу вокруг моих примерах выше. Существует множество способов сделать минимальные примеры работы, но мой вопрос заключается в создании класса через new, который основан на существующем классе, а не на том, как заставить эти примеры работать. Извините за путаницу.

Edit 2:

Обсуждение в комментариях привело меня более конкретный вопрос, чем я первоначально спросил:

Можно создать класс, через вызов type, который использует слотов и дескрипторов существующего класса, не являясь потомком этого класса?

Если ответ «нет», я был бы признателен за источник, почему нет.

+1

Когда декоратор выполняет класс уже создан. Вы должны определить метакласс для этого. – JBernardo

+0

Вы хотите, чтобы слот 'x' продолжал работать? – user2357112

+0

Да, я хотел бы, чтобы оба 'x' и' y' работали в результирующем классе. – Narcolei

ответ

2

Нет, к сожалению, нет никакого способа сделать что-либо с __slots__ после создания класса (и именно тогда вызывается их декораторы). Единственный способ - использовать метакласс и изменить/добавить __slots__ перед вызовом type.__new__.

Пример такого метаклассе:

class MetaA(type): 
    def __new__(mcls, name, bases, dct): 
     slots = set(dct.get('__slots__',())) 
     slots.add('y') 
     dct['__slots__'] = tuple(slots) 
     return super().__new__(mcls, name, bases, dct) 

class BaseA(metaclass=MetaA): 
    pass 

class A(BaseA): 
    __slots__ = ('x',) 

    def __init__(self): 
     self.x = 1 
     self.y = 2 

print(A().x, A().y) 

Без метаклассами, вы можете сделать некоторые магии и скопировать все из определенного класса и создать новый на лету, но этот код пахнет;)

Основным недостатком такого кода является то, что если кто-то применит другой декоратор, прежде чем ваш, и, скажем, создаст ссылку на украшенный класс где-нибудь, тогда они вернут ссылку на другой класс. То же самое для метаклассов - они будут выполняться дважды. Таким образом, метаклассовый подход лучше, поскольку побочных эффектов нет.


Окончательный ответ о том, почему вы не можете изменить __slots__ после того, как класс создан, зависит от деталей реализации интерпретатора вы работаете. Например, в CPython для каждого слота, который вы определили, класс имеет дескриптор (см. PyMemberDescr_TypePyMemberDef struct в исходном коде CPython), который имеет параметр смещения, где значение слота выровнено во внутреннем хранилище объектов. И у вас просто нет инструментов манипулирования такими вещами в общедоступном Python API. Вы используете гибкость при меньшем использовании памяти (опять же, в CPython, как и в PyPy, вы автоматически получаете одинаковый эффект памяти для всех ваших классов).

Если требуется модификация __slots__, вы можете, возможно, написать расширение C (или работать с ctypes) и сделать это, но это вряд ли является надежным решением.

+0

У вас есть источники, которые доказывают, что это невозможно? Я бы хотел получить окончательный ответ вместо того, чтобы больше работать. – Narcolei

+0

Хорошо, обновлено. – 1st1

+0

Спасибо за обновление. Я знаю, что вы не можете изменять слоты существующего класса, но это не то, что я пытаюсь сделать. Я пытаюсь выяснить, можно ли создать новый класс, который использует слоты и дескрипторы существующего класса, не будучи потомком этого класса. – Narcolei

1

Вы можете сделать это с метаклассами:

class MetaSlot(type): 
    def __new__(mcs, name, bases, dic): 
     dic['__slots__'] += ('y',) 
     return type.__new__(mcs, name, bases, dic) 


class C(metaclass=MetaSlot): # Python 3 syntax 
    __slots__ = ('x',) 

Теперь оба x и y могут быть использованы:

>>> c = C() 
>>> c.y = 10 
>>> c.x = 10 
+0

Предполагается, что он будет работать, будет ли измененный класс определил '__slots__' или нет - ваш ответ, похоже, требует этого. – martineau

+0

@martineau Это всего лишь образец кода. Я почти уверен, что OP может продолжаться отсюда – JBernardo