Python: подкласс `type` для создания специализированных типов (например, «список int»)

#python #abc

#python #азбука #abc

Вопрос:

Я пытаюсь создать подкласс 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)
  

Странно?! Очевидно, что аргумент есть … Итак, что это значит? Есть идея? Я злоупотреблял abc ?

Комментарии:

1. Вы уверены, что не хотите, ABCMeta чтобы в качестве метакласса использовался созданный тип, а не ваш метакласс?

2. Если вам нужен язык, который понимает параметризованные типы и может выполнять проверку типов во время компиляции, тогда используйте один. Предполагая, что у вас это получилось, я не понимаю, как это могло бы что-то улучшить по сравнению с простым подклассом list и отклонением не- int s в интерфейсе.

3. @kindall: Хм… Да, возможно, вы правы!!! Потому что в моем случае экземпляры на самом деле сами являются типами, и я, вероятно, перепутал с этим. Если подумать, если я хочу, чтобы мой ListOfInt класс был определен как подкласс ListOfObject , issubclasshook должен находиться в ListOfObject классе . Я попробую завтра и опубликую ответ, если это сработает!

4. @Karl Knechtel: очевидно, что если я делаю подобные сложные вещи, то это потому, что мне это нужно! На самом деле это для библиотеки, которую я пишу ( pypi.python.org/pypi/any2any/0.1 ), и эти экземпляры SpzType не предназначены для создания самих экземпляров. Меня просто интересует определение семантического ListOfObject , JSONStr и т.д…

5. @kindall: даже при том, что вы, возможно, правы — и опубликованный код не может достичь того, чего я хочу — (я еще не пробовал), определенно происходит что-то прикольное … Может быть даже ошибка, нет?

Ответ №1:

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

В PEP 3119 есть больше информации об универсальных классах коллекции

Комментарии:

1. Что ж … история со списком была просто примером … Что я хочу сделать, так это иметь экземпляры type, содержащие дополнительную информацию. Представьте себе тип XMLStr or JSONStr (который SpzType позволял бы легко создавать и который уважал бы issubclass(XMLStr, str) == True ) !? Представьте тип OddInt , … и так далее, и тому подобное …

Ответ №2:

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

 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. Я пытался работать подобным образом раньше, но это не давало хороших результатов из-за большого количества повторений (например, заводская функция для ListSpzType , заводская функция для StrSpzType , для IntSpzType , …) и синтаксис не очень приятный для конечного пользователя.

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

3. Вообще-то, я беру свои слова обратно. Вы могли бы довольно легко просто создать новые дочерние классы из переданного класса, используя type() конструктор. Затем создайте для него декоратор класса…

Ответ №3:

Благодаря комментарию от 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 реализован как метод экземпляра. Это отлично работает и (IMO) элегантно!!!

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

Комментарии:

1. Да, это довольно ловко! Я только что написал решение с использованием декоратора, которое я опубликую, если хотите, но я думаю, что вы поняли этот ритм.

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

3. Опубликовал версию декоратора.

Ответ №4:

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

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

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

Если __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. Я бы добавил, что это выглядит намного длиннее, чем ваше решение, но большая часть дополнительного кода включает в себя проверку того, что сгенерированный класс имеет __name__ атрибут, который отражает функции, используемые в классе. Например, попробуйте print ListOf(int, convert=True) Добавить это в метакласс, и кода станет ничуть не меньше.

2. Спасибо! Это длиннее, и некоторые части кажутся немного халтурными 😛 но в итоге это действительно выглядит хорошо!