Создание копий встроенных классов - PullRequest
0 голосов
/ 04 мая 2018

Я пытаюсь написать функцию, которая создает классы из классов без изменения исходного.

Простое решение (на основе этого ответа )

def class_operator(cls):
    namespace = dict(vars(cls))
    ...  # modifying namespace
    return type(cls.__qualname__, cls.__bases__, namespace)

отлично работает, кроме type:

>>> class_operator(type)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: type __qualname__ must be a str, not getset_descriptor

Проверено на Python 3,2 - Python 3,6 .

(я знаю, что в текущей версии модификация изменяемых атрибутов в namespace объект изменит исходный класс, но это не так)

Обновление

Даже если мы удалим параметр __qualname__ из namespace, если есть

def class_operator(cls):
    namespace = dict(vars(cls))
    namespace.pop('__qualname__', None)
    return type(cls.__qualname__, cls.__bases__, namespace)

результирующий объект не ведет себя как оригинал type

>>> type_copy = class_operator(type)
>>> type_copy is type
False
>>> type_copy('')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object
>>> type_copy('empty', (), {})
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object

Почему?

Может кто-нибудь объяснить, какой механизм во внутренних элементах Python предотвращает копирование класса type (и многих других встроенных классов).

1 Ответ

0 голосов
/ 04 мая 2018

Проблема здесь в том, что type имеет __qualname__ в __dict__, что является свойством (то есть дескриптором 1005 *), а не строкой:

>>> type.__qualname__
'type'
>>> vars(type)['__qualname__']
<attribute '__qualname__' of 'type' objects>

И попытка присвоить не-строку классу __qualname__ выдает исключение:

>>> class C: pass
...
>>> C.__qualname__ = 'Foo'  # works
>>> C.__qualname__ = 3  # doesn't work
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only assign string to C.__qualname__, not 'int'

Вот почему необходимо удалить __qualname__ из __dict__.

Что касается причины, по которой ваш type_copy не может быть вызван: это потому, что type.__call__ отклоняет все, что не является подклассом type. Это верно как для формы с тремя аргументами:

>>> type.__call__(type, 'x', (), {})
<class '__main__.x'>
>>> type.__call__(type_copy, 'x', (), {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__init__' for 'type' objects doesn't apply to 'type' object

А также форма с одним аргументом, которая на самом деле работает только с type в качестве первого аргумента:

>>> type.__call__(type, 3)
<class 'int'>
>>> type.__call__(type_copy, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type.__new__() takes exactly 3 arguments (1 given)

Это не легко обойти. Исправить форму с тремя аргументами достаточно просто: мы делаем копию пустым подклассом type.

>>> type_copy = type('type_copy', (type,), {})
>>> type_copy('MyClass', (), {})
<class '__main__.MyClass'>

Но форма с одним аргументом type намного противнее, поскольку она работает только в том случае, если первый аргумент - type. Мы можем реализовать пользовательский метод __call__, но этот метод должен быть записан в метаклассе, что означает, что type(type_copy) будет отличаться от type(type).

>>> class TypeCopyMeta(type):
...     def __call__(self, *args):
...         if len(args) == 1:
...             return type(*args)
...         return super().__call__(*args)
... 
>>> type_copy = TypeCopyMeta('type_copy', (type,), {})
>>> type_copy(3)  # works
<class 'int'>
>>> type_copy('MyClass', (), {})  # also works
<class '__main__.MyClass'>
>>> type(type), type(type_copy)  # but they're not identical
(<class 'type'>, <class '__main__.TypeCopyMeta'>)

Есть две причины, по которым type так сложно скопировать:

  1. Это реализовано в C. Вы столкнетесь с подобными проблемами, если попытаетесь скопировать другие встроенные типы, такие как int или str.
  2. Тот факт, что type является экземпляром самого :

    >>> type(type)
    <class 'type'>
    

    Это то, что обычно невозможно. Размывает грань между классом и экземпляром. Это хаотическое накопление атрибутов экземпляра и класса. Вот почему __qualname__ является строкой при доступе как type.__qualname__, но дескриптором при обращении как vars(type)['__qualname__'].


Как видите, невозможно сделать идеальную копию type. Каждая реализация имеет различные компромиссы.

Самое простое решение - создать подкласс type, который не поддерживает вызов с одним аргументом type(some_object):

import builtins

def copy_class(cls):
    # if it's a builtin class, copy it by subclassing
    if getattr(builtins, cls.__name__, None) is cls:
        namespace = {}
        bases = (cls,)
    else:
        namespace = dict(vars(cls))
        bases = cls.__bases__

    cls_copy = type(cls.__name__, bases, namespace)
    cls_copy.__qualname__ = cls.__qualname__
    return cls_copy

Сложное решение - создать собственный метакласс:

import builtins

def copy_class(cls):
    if cls is type:
        namespace = {}
        bases = (cls,)

        class metaclass(type):
            def __call__(self, *args):
                if len(args) == 1:
                    return type(*args)
                return super().__call__(*args)

        metaclass.__name__ = type.__name__
        metaclass.__qualname__ = type.__qualname__
    # if it's a builtin class, copy it by subclassing
    elif getattr(builtins, cls.__name__, None) is cls:
        namespace = {}
        bases = (cls,)
        metaclass = type
    else:
        namespace = dict(vars(cls))
        bases = cls.__bases__
        metaclass = type

    cls_copy = metaclass(cls.__name__, bases, namespace)
    cls_copy.__qualname__ = cls.__qualname__
    return cls_copy
...