Вход | Регистрация

Метакласове и ООП хакерия

  • Днес ви разказах някои доста особени неща. Хайде ако сте си експериментирали с Python и имате интересен код да го споделяте, да го направите. Ще е забавно, а дори може да получите някоя друга точка :)

    28.04.2009
  • Тъй.

    >>> class A:
    ...     pass
    ...
    >>> A.__class__
    <class 'type'>
    >>> A.__base__
    <class 'object'>
    >>> A.__class__ = object
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: __class__ assignment: only for heap types
    >>> A.__base__ = type
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: readonly attribute

    Но пък:

    >>> A = type("A", (type,), {})
    >>> A.__class__
    <class 'type'>
    >>> A.__base__
    <class 'type'>

    Сега, това е малко безумно, нали. :) Защото:

    class Meta(type):
        def __new__(self, cls, bases, attrs):
            return type.__new__(self, cls, bases, attrs)
    
        def metaFoo(self, a):
            print(a)
    
    A = Meta("A", (Meta,), {})
    print(A.__class__) # <class '__main__.Meta'>
    print(A.__base__) # <class '__main__.Meta'>
    A.metaFoo(1) # TypeError: metaFoo() takes exactly 2 positional arguments (1 given)
    a = A() # __new__() takes exactly 4 positional arguments (1 given)

    Пайтън тук се чупи, защото няма "клас-методи". Тъй като А е едновременно и инстанция, и наследник на Meta, той не знае какво да прави с това metaFoo. По-точно знае - счита го за наследен метод, а не за метод на базовия клас. Втората грешка дори нямам идея що става. :)

    В Ruby е по-забавно:

    class Class
      class << self
        alias oldSelfNew new
    
        def new arg=Object
          p "baaaaa"
          oldSelfNew arg
        end
      end
    end
    
    Klass = Class.new Class # baaaaa
    Klass.superclass # Class
    Klass.class # Class
    class Klass
      def self.bar
        42
      end
    
      def foo
        "foo"
      end
    end
    Klass.bar # 42
    Klass.class_eval "bar" # 42
    Klass.instance_eval "bar" # 42
    
    B = Klass.new # "baaaaa", след което TypeError: wrong instance allocation
    b = B.new

    В смисъл, втф? :) Защо вика клас-методът new на Class, a не instance-методът? Някво супер странно е.

    Едит: Това в Руби е "хакерия", щото принципно не се позволява наследяване от Class:

    irb(main):001:0> class A < Class
    irb(main):002:1> end
    TypeError: can't make subclass of Class
            from (irb):1
    29.04.2009 (променeно 29.04.2009)
  • Защо да се чупи? Прави каквото пише в документацията, че трябва да прави :-).

    Class attribute references are translated to lookups in this dictionary, e.g.,C.x is translated to C.__dict__["x"] (although there are a number of hooks which allow for other means of locating attributes). When the attribute name is not found there, the attribute search continues in the base classes.

    За клас-методи, ползвай декоратора за клас-методи (@classmethod). Тогава ще има наследен клас-метод :-D

    PS. Ужас, това май стана като полиморфизъм при класове.

    29.04.2009 (променeно 29.04.2009)
  • Thumbs_up

    Интересно...

    class Meta(type):
        def __new__(self, cls, bases, attrs):
            return type.__new__(self, cls, bases, attrs)
    
        def poque(self, a):
            print(a)
    
    A = Meta('A', (Meta,), {})
    
    >>> A.poque(2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: poque() takes exactly 2 positional arguments (1 given)
    >>> type.__getattribute__(A, 'poque')(2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: poque() takes exactly 2 positional arguments (1 given)
    >>> object.__getattribute__(A, 'poque')(2)
    2

    Иначе казано: type.__getattribute__ не е същото като object.__getattribute__. Ако ли пък много си обичаш object.__getattribute__:

    class Meta(type):
        def __new__(self, cls, bases, attrs):
            return type.__new__(self, cls, bases, attrs)
    
        def poque(self, a):
            print(a)
    
        __getattribute__ = object.__getattribute__
    
    
    A = Meta('A', (Meta,), {})
    
    
    >>> A.poque(2)
    2

    А може и още:

    A = Meta('A', (Meta,), {'mooq': lambda self, x, y: print(x + y)})
    
    class B(A, metaclass=A): pass
    
    class C(A): pass
    
    >>> A.mooq(2, 3)
    TypeError: <lambda>() takes exactly 3 positional arguments (2 given)
    
    >>> B.mooq(2, 3)
    5
    
    >>> C.mooq(2, 3)
    AttributeError: 'Meta' object has no attribute 'mooq'

    A.mooq се взима от A.__dict__['mooq'], B.mooq би трябвало да работи, защото класът на B е А и object.__getattribute__ намира mooq в А и връща bound method. C вече ме изненадва - нали наследява A, не би ли трябвало да има mooq?

    П.П. Ако това го има на теста, колегите ще ме убият...

    30.04.2009 (променeно 30.04.2009)
  • Това:

    A = Meta('A', (Meta,), {'mooq': lambda self, x, y: print(x + y)})
    
    class B(A, metaclass=A): pass

    Е странно, че работи. В смисъл, вторият ред се преобразува до:

    B = A("B", (A,), {})

    Което означава, че А наследява конструктора __new__ от Meta. Защо?

    Едит: глупости. А наследява тоя конструктор от type.

    >>> B.mooq(2, 3)
    5

    Това също е странно. То е абсолютно същото като това, което си показал по-горе:

    class Meta(type):
        def __new__(self, cls, bases, attrs):
            return type.__new__(self, cls, bases, attrs)
    
        def poque(self, a):
            print(a)
    
    A = Meta('A', (Meta,), {})
    
    >>> A.poque(2)

    Което пък се чупи. В смисъл, в този случай B не наследява mooq, ами го ползва като метод на родителя, което не е случая с A и Meta. Бе странно е. :)

    30.04.2009 (променeно 30.04.2009)
  • Thumbs_up
    class Meta(type):
        def __new__(self, cls, bases, attrs):
            return type.__new__(self, cls, bases, attrs)
    
        def poque(self, a):
            print(a)
    
        __getattribute__ = object.__getattribute__
    
    
    A = Meta('A', (Meta,), {})
    
    >>> Meta.__getattribute__ is object.__getattribute__
    True
    
    >>> A.__getattribute__
    <method-wrapper '__getattribute__' of Meta object at 0x92bd884>
    
    >>> A.__new__ is Meta.__new__
    True
    
    >>> A.poque(2)
    2

    A.poque(2) не се чупи, когато Meta.__getattribute__ is object.__getattribute__. Което има смисъл, защото A.poque е A.__getattribute__('poque') което е Meta.__getattribute__(A, 'poque') т.е. `object.__getattribute__(A, 'poque'). В A.__dict__ няма ключ 'poque', така че се търси в класът на A, което е Meta. Meta.__dict__['poque'] е функция, така че object.__getattribute__ връща bound method и всичко 'работи'.

    30.04.2009
  • Тези дни си зададох въпроса, какво става когато наследяваме класове от класове, с тип даден метаклас. Нещо нямам спомени да сме го споменавали на лекции, затова изпробвах някои неща.

    Резултатът ми даде идеята за следния метаклас:
    Метаклас, създаващ клас, който да не може да се наследява от други класове (нещо подобно на sealed или final клас).

    Илюстрация:

    class A(metaclass=Sealed):
        pass
    
    class B(A): #fails
        pass

    Реализацията, надявам се, ще ви отговори на въпроса за наследяването, а ако ви мързи да разберете/прочетете кода, може да поровите из лекциите (когато бъдат качени ;) ) или из документацията.

    Ето и кода:

    class Sealed(type):
        def __new__(cls, class_name, bases, attr):
            cls._verify_bases(class_name, bases)
            class_ = type.__new__(cls, class_name, bases, attr)
            cls.register(class_)
            return class_
        
        _catalog = set()
    
        @classmethod
        def _verify_bases(cls, class_name, bases):
            for base in bases:
                assert base not in cls._catalog, (
                    "Class '{0}' cannot derive from sealed class {1}"
                                                 .format(class_name, base))
        @classmethod
        def register(cls,class_):
            cls._catalog.add(class_)
    
        @classmethod
        def _clear_catalog(cls):
            cls._catalog.clear()

    Ето и някои "тестове":

    import unittest
    from sealed import Sealed
    
    class SealingTestCase(unittest.TestCase):
        def setUp(self):
            self.A = Sealed("A", (), {})
            
        def test_derive(self):
            self.assertRaises(AssertionError, Sealed, "B", (self.A,), {})
    
        def test_name_overwrite(self):
            A = type("A", (), {})
            B = Sealed("B", (A,), {})
    
        def test_try_overwrite(self):
            A = Sealed("A", (), {})
            self.assertRaises(AssertionError, Sealed, "B", (self.A,), {})
            
        def tearDown(self):
            del self.A
            Sealed._clear_catalog()
    
    if __name__ == "__main__":
    unittest.main()

    Ще се радвам на забележки към кода (включително към стила).

    Тази версия има недостатъка, че не може да ползвате метакласа Sealed директно, когато искате да наследите от друг клас с друг метаклас. Например, искате да създадете клас B(A, metaclass=Sealed), където `A` има някакъв друг мета клас.

    Ето и илюстрация на проблема:

    class Placebo(type):
    def __new__(cls, name, bases, attr):
           return type.__new__(cls, name, bases, attr)
    
    class A(metaclass=Placebo):
        pass
    
    class B(A, metaclass=Sealed): #TypeError: metaclass conflict...
        pass

    Това ще го оправя в евентуалната следваща версия. :)

    02.05.2009 (променeно 03.05.2009)
  • @Атанас

    Кодът ти е толкова Джаварски, че не мога да си сдържа коментара. Като започнем от нуждата от Sealed и завършим с реализацията. Това не е ли по-просто?:

    class sealed(type):
        def __new__(cls, name, bases, attrs):
            assert not any(isinstance(base, sealed) for base in bases)
            return type.__new__(cls, name, bases, attrs)

    Без излишни singleton-и, без ненужно много методи и без излишен зор в unit тестовете.

    Впрочем, не съм навит това да се реализира с метаклас, понеже метакласовете трябва да са общи в една йерархия. Ако имаш клас с метаклас Placebo, не се сещам как ще затвориш негов наследник, освен ако твоя метаклас не наследява Placebo. Дори не съм сигурен дали тогава ще работи.

    Според мен решението ти е да си създадеш собствени object и type, с поведението което искаш и да влачиш от там. Тамън ще си реализираш и твоя @Overload (който трябва да се казва @Multi). Но пък тогава удряме на проблема колко ще си съвместим с друг Python код, yadda-yadda и всичко останало.

    03.05.2009 (променeно 03.05.2009)
  • Не мисля, че има нужда от sealed в Python, но беше интересно упражнение.

    Съгласен съм, че singleton-а е излишен (за тази версия), иначе any-то го смених с итерация, щото исках да ми казва, кой базов клас е проблемният (и съответно кода стана дългичък).

    Като цяло това с йерархията на метакласовете не ми харесва как са го измислили, сигурно бъркам концепцията. Вероятно, защото възприемам метакласовете, предимно като силно развити декоратори на класове, а декораторите имат добра синтактична и, в повечето случаи, семантична композируемост. Наистина някои метакласове са несъвместими логически един с друг, но пък за други, според мен е добре да са композируеми (например "абстрактния" ABCMeta).

    Попринцип имам идея как да заобиколя проблема с йерархиите и да интегрирам метакласа с произволна йерархия.

    Със singleton-а, до колкото си го представям, решението ще е по-просто в разширения случай. Като се замисля, трябва да е възможно да се реши и в общия случай без него - с някоя допълнителна инжекция на данни. Ще пробвам :)

    Що се отнася до @Overload (което не беше първото име, а го откраднах от GvR :P ), там още експериментирам и в текущата ми версия съм елиминирал нуждата от декоратори. С метакласове изглежда по-елегантно. Ако номера с йерархиите тук мине, може и там да го ползвам. Ако не, може да скрия метакласа, но това е друга тема. :)

    03.05.2009 (променeно 03.05.2009)
  • Как си елиминирал декораторите?

    Не пускай детайли, защото шеста задача ще е точно това, което ти си предал за трета, но все пак се опитай да ми обясниш, понеже ми е непонятно.

    P.S.: GvR е голям ум (то какво оставаше), но е кретенско да си кръстиш multi-dispatch-а overload, тъй като първото е runtime, а второто - compile-time. Но както и да е, няма да го критикуваме за нищо друго, освен малоумно кратките ламбди. :)

    03.05.2009 (променeно 03.05.2009)
  • Ами закъчил съм се на някои допълнителни кукички. Е доста съм го изменил като реализация. Singleton-а не ме кефеше още тогава, но нямаше време да го преправям. Сега го няма и е далеч по-нормално. Ползвам и някои неща, за които не знаех тогава, а взехме след това на лекции. Имам още да доизбистрям contract-а за използване. В момента може да се каже, че не можеш да имаш обикновени методи, което мисля да махна като изискване.

    04.05.2009 (променeно 04.05.2009)
  • __prepare__ беше ключовият момент. Това не го знаех. Още по-хубаво -- така съвсем ще мога да разширя задачата. Супер.

    04.05.2009
  • Да, това е :) Можеше да не го издаваш :P

    Между другото имам и друго подобрение - можеш да ползваш името на класа вместо Self-а (който ти не харесваше). Реално подобренията са само синтактични, и има някои малки допълнения към contract-а.

    За статичните неща се отказах, защото не са част от този проблем.

    04.05.2009 (променeно 04.05.2009)
  • Кажи и това как става. Ще го има в условието на задачата, щото иначе хората ще се озорят.

    Дефинираш клас в глобалния namespace или правиш нещо далеч по-хитро? Може би слагаш елемент с това име в dict-а, преди __prepare__ да го върне?

    04.05.2009 (променeно 04.05.2009)
  • Елемент в dict-а, който е равен на специалния тип. Останалата логика си остава. Съответно и допълнителното ограничение, че името не можеш свободно да го ползваш за други неща в твоя код. Ако внимаваш можеш да го позваш, а може и да се направи read-only и да не може да се трие и да не можеш да го ползваш - въпрос на договор.

    Сигурно може и след конструирането да се подменя специалния тип и да няма специален случай после при обработката.

    PS. Май стана малко offtopic, трябваше другаде да го обсъдим :P

    04.05.2009
  • Що бе, чакам с нетърпение някой да каже "За какво, WTF, си говорите?". С радост ще му обясним, а и ще стане нормална приказка, в която обменяме know-how, а не се чудите каква оценка ще имате на края на курса. Тамън и ще поразчупим леда, предвид че тази година сте ми най-трудните за разчупване на леда. Не знам дали защото остарявам или по някаква друга причина :)

    04.05.2009
  • За какво, WTF, си говорите?

    05.05.2009
  • Е, за кво си говорите? Междувременно:

    class Foo:
        def a(self):
            print(1)
    
    class Bar:
        def a(self):
            print(2)
    
        def b(self):
            print(3)
    
    class A(Foo): pass
    class B(Bar): pass
    
    a = A()
    a.a() # 1
    
    A.__bases__ = (Bar,)
    a.a() # 2
    a.__class__ = Foo
    a.a() # 1
    
    b = B()
    b.a() # 2
    B.__bases__ = (A,)
    b.a() # 2
    A.__bases__ = (Foo,)
    b.a() # 1
    
    A.__bases__ = (A,) # TypeError: a __bases__ item causes an inheritance cycle
    
    Foo.__bases__ = (Bar,) # TypeError: __bases__ assignment: 'Bar' deallocator differs from 'object'

    Тва последното не ме кефи. В класовете нов стил имало случаи, когато не може да се променя bases. Някаква идея кога и защо? Ако Foo наследяваше от нещо различно от object, каквото и да е (освен type де), последното щеше да работи. Или поне така си мисля.

    Знаете ли, че в Руби Module е инстанция на Class, но пък Class наследява от Module? :) Тук е по-интелигентно и красиво.

    07.05.2009
  • За разговора, ще оставя на Стефан да обясни. Това с деалокатора е много странно. Иначе, къде е "тук", дето е по-интелигентно и красиво :) ?

    Edit: Тук http://bugs.python.org/issue672115 май коментират защо работи така и т.н. (не, че схванах много какъв е проблема).

    07.05.2009 (променeно 07.05.2009)
  • По темата за комбинирането на метакласове, май намерих нещо като концепция за комбиниране.

    Идеята е проста - създаване на метаклас, наследник на една подходяща поредица от метакласове на базовите класове. Хората са я описали и преди - ако ви се чете - книгата "Putting metaclasses to work" и "реализация" за някоя по-стара версия - http://code.activestate.com/recipes/204197/. Там е дадена реализация, която орязва ненужните базови метакласове.

    Тъй като ще използваме множествено наследяване, друго важно нещо е, че не всеки клас се поддава успешно на множествено наследяване. Една добра практика при писане е да се ползва така наречения "cooperative super call" (което не знам как е изпуснато на лекции :P ), т.е. super() вместо директно извикване през "родителският" клас (в нашият случай, най-често type).

    За нашата задача трябва да променим имплементацията на sealed:

    class sealed(type):
        def __new__(mcls, name, bases, namespace):
            for base in bases:
                assert not isinstance(base, Sealed), (
                    "Class '{0}' cannot derive from sealed class {1}"
                                             .format(name, base.__name__))
            return super().__new__(mcls, name, bases, namespace)

    По темата може да прочетете на http://www.python.org/.../descrintro/ - частта Cooperative methods and "super", частта Metaclasses обеснява защо не се комбинират автоматично базовите класове. (Малко е старичка статията, но става.)

    Ако имате поне два базови метакласове, които конструират класове през type вместо през super() няма как да ги комбинирате. Ако е само един, за да сработят всички, трябва да го сложите последен в списъка за наследяване.

    Имам и реализация, но ще стане много дълго, а като гледам има силен интерес :D

    Между другото композирането мисля, че работи и за __prepare__ ;). С тази уговорка, че трябва да можете да "обвиете" речника от super().__prepare__ така, че да прави каквото ви трябва. (това не съм го пробвал още)

    07.05.2009 (променeно 07.05.2009)