Python: подкласс `type` для создания специализированных типов (например," список целых ") - PullRequest
7 голосов
/ 13 июня 2011

Я пытаюсь создать подкласс type, чтобы создать класс, позволяющий создавать специализированные типы.например, ListType:

>>> ListOfInt = ListType(list, value_type=int)
>>> issubclass(ListOfInt, list)
True
>>> issubclass(list, ListOfInt)
False
>>> # And so on ...

Однако, этот ListOfInt никогда не будет использоваться для создания экземпляров!Я просто использую его как экземпляр type, которым я могу манипулировать для сравнения с другими типами ... В частности, в моем случае мне нужно найти подходящую операцию, в соответствии с типом ввода, и мне нужнотип, который содержит больше точности (например, list of int или XML string и т. д.).

Итак, вот что я придумал:

class SpzType(type):

    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, C):
        return NotImplemented

    def __new__(cls, base, **features):
        name = 'SpzOf%s' % base.__name__
        bases = (base,)
        attrs = {}
        return super(SpzType, cls).__new__(cls, name, bases, attrs)

    def __init__(self, base, **features):
        for name, value in features.items():
            setattr(self, name, value)

Использование abc не очевидно в приведенном выше коде ... однако, если я хочу написать подкласс ListType, как в примере сверху, он становится полезным ...

Базовая функциональность фактически работает:

>>> class SimpleType(SpzType): pass
>>> t = SimpleType(int)
>>> issubclass(t, int)
True
>>> issubclass(int, t)
False

Но когда я пытаюсь проверить, является ли t экземпляром SpzType, Python сходит с ума:

>>> isinstance(t, SpzType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)

Я изучил с pdb.pm(), что происходит, иЯ обнаружил, что следующий код вызывает ошибку:

>>> SpzType.__subclasscheck__(SimpleType)
TypeError: __subclasscheck__() takes exactly one argument (0 given)

WeIrD?!Очевидно, что есть аргумент ... Так что это значит?Любая идея ?Я неправильно использовал abc?

Ответы [ 4 ]

1 голос
/ 15 июня 2011

Вот версия моего ответа для декоратора, которая работает с любым классом.Декоратор возвращает фабричную функцию, которая возвращает подкласс исходного класса с требуемыми атрибутами.Приятной особенностью этого подхода является то, что он не требует метакласса, поэтому вы можете использовать метакласс (например, ABCMeta), если это необходимо, без конфликтов.

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

Если этосуществует, метод класса __classinit__() передает аргументы, передаваемые фабрике, поэтому сам класс может иметь код для проверки аргументов и установки своих атрибутов.(Это будет вызвано после метакласса __init__().) Если __classinit__() возвращает класс, этот класс возвращается фабрикой вместо сгенерированного, так что вы даже можете расширить процедуру генерации таким способом (например, для типапроверенный класс списка, вы можете вернуть один из двух внутренних классов в зависимости от того, следует ли привести элементы к типу элемента или нет).

Если __classinit__() не существует, аргументы, передаваемые фабрике,просто установите в качестве атрибутов класса для нового класса.

Для удобства создания контейнерных классов с ограниченным типом я обрабатывал тип элемента отдельно от dict функции.Если оно не будет передано, оно будет проигнорировано.

Как и прежде, классы, сгенерированные фабрикой, кэшируются, так что каждый раз, когда вы вызываете класс с одинаковыми функциями, вы получаете один и тот же экземпляр объекта класса.

def template_class(cls, classcache={}):

    def factory(element_type=None, **features):

        key = (cls, element_type) + tuple(features.items())
        if key in classcache:
            return classcache[key]

        newname  = cls.__name__
        if element_type or features:
            newname += "("
            if element_type:
                newname += element_type.__name__
                if features:
                    newname += ", "
            newname += ", ".join(key + "=" + repr(value)
                                 for key, value in features.items())
            newname += ")"

        newclass = type(cls)(newname, (cls,), {})
        if hasattr(newclass, "__classinit__"):
            classinit = getattr(cls.__classinit__, "im_func", cls.__classinit__)
            newclass = classinit(newclass, element_type, features) or newclass
        else:
            if element_type:
                newclass.element_type = element_type
            for key, value in features.items():
                setattr(newclass, key, value)

        classcache[key] = newclass
        return newclass

    factory.__name__ = cls.__name__
    return factory

Пример списка классов с ограничением типов (на самом деле, с преобразованием типов):

@template_class
class ListOf(list):

    def __classinit__(cls, element_type, features):
        if isinstance(element_type, type):
            cls.element_type = element_type
        else:
            raise TypeError("need element type")

    def __init__(self, iterable):
        for item in iterable:
            try:
                self.append(self.element_type(item))
            except ValueError:
                raise TypeError("value '%s' not convertible to %s"
                        % (item, self.element_type.__name__))

    # etc., to provide type conversion for items added to list 

Создание новых классов:

Floatlist = ListOf(float)
Intlist   = ListOf(int)

Затем создать экземпляр:

print FloatList((1, 2, 3))       # 1.0, 2.0, 3.0
print IntList((1.0, 2.5, 3.14))  # 1, 2, 3

Или просто создайте класс и создайте его экземпляр за один шаг:

print ListOf(float)((1, 2, 3))
print ListOf(int)((1.0, 2.5, 3.14))
1 голос
/ 14 июня 2011

То, что вы хотите сделать, вероятно, можно было бы сделать проще, используя функцию фабрики классов, как показано ниже. По крайней мере, для меня это упрощает отслеживание различных уровней, на которых я пытаюсь работать.

def listOf(base, types={}, **features):
    key = (base,) + tuple(features.items())
    if key in types:
        return types[key]
    else:

        if not isinstance(base, type):
            raise TypeError("require element type, got '%s'" % base)

        class C(list):

             def __init__(self, iterable=[]):
                 for item in iterable:
                     try:    # try to convert to desired type
                         self.append(self._base(item))
                     except ValueError:
                         raise TypeError("value '%s' not convertible to %s"
                            % (item, self._base.__name__))

              # similar methods to type-check other list mutations

        C.__name__ = "listOf(%s)" % base.__name__
        C._base = base
        C.__dict__.update(features)  
        types[key] = C
        return C

Обратите внимание, что я использую dict в качестве кеша здесь, чтобы вы получили один и тот же объект класса для данной комбинации типа элемента и функций. Это делает listOf(int) is listOf(int) всегда True.

1 голос
/ 14 июня 2011

Благодаря комментарию от kindall я изменил код на следующий:

class SpzType(abc.ABCMeta):

    def __subclasshook__(self, C):
        return NotImplemented

    def __new__(cls, base, **features):
        name = 'SpzOf%s' % base.__name__
        bases = (base,)
        attrs = {}
        new_spz = super(SpzType, cls).__new__(cls, name, bases, attrs)
        new_spz.__subclasshook__ = classmethod(cls.__subclasshook__)
        return new_spz

    def __init__(self, base, **features):
        for name, value in features.items():
            setattr(self, name, value)

Итак, SpzType теперь является подклассом abc.ABCMeta, и subclasshook реализован как метод экземпляра.Он прекрасно работает, и это (ИМО) элегантно !!!

РЕДАКТИРОВАТЬ: была хитрая вещь ... потому что __subclasshook__ должен быть метод класса, поэтому я должен вызвать функцию метода класса вручную ..иначе не получится, если я захочу реализовать __subclasshook__.

1 голос
/ 13 июня 2011

Я не совсем уверен, чего вы хотите достичь. Может быть, лучше использовать collections модуль вместо abc напрямую?

Подробнее об общих классах коллекций можно узнать в PEP 3119

...