Метакласи в Python: що це таке

  • 20 ноября, 16:37
  • 3164
  • 0

Давайте поговоримо про специфіку мови Python і його функціонал.

Щоб створити свій власний метаклас в Python, потрібно скористатися підклас type , стандартний метаклас в Python. Найчастіше метакласи використовуються в ролі віртуального конструктора. Щоб створити екземпляр класу, потрібно спочатку викликати цей самий клас. Точно так чинить і Python: для створення нового класу викликає метаклас. Метакласи визначаються за допомогою базових класів в атрибуті __metaclass__ . При створенні класу допускається використання методів __init__ і __new__ . З їх допомогою можна користуватися додатковими функціями. Під час виконання оператора class генерується простір імен, які будуть містити атрибути майбутнього класу. Потім, для безпосереднього створення, викликається метаклас з ім'ям і атрибутами.

приклад:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType
class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__

Класи як об'єкти

Перед тим як починати розбиратися в метаклассом, потрібно добре розуміти як працюють звичайні класи в Python, а вони дуже своєрідні. У більшості мов це просто фрагменти коду, які описують створення об'єкта. Дане судження вірно і для Python:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

Але є один нюанс. Класи в Python це об'єкти. Коли виконується оператор class , Python створює в пам'яті об'єкт з ім'ям ObjectCreator .

>>> class ObjectCreator(object):
...       pass
...

Об'єкт здатний сам створювати екземпляри, так що це клас. А об'єкт ось чому:

  • його можна призначити в якості змінної
  • копіюється
  • є можливість додати до нього атрибути
  • передається в ролі параметра функції

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

Динамічне створення класів

Якщо класи в Python - це об'єкти, значить, як і будь-який інший об'єкт, їх можна створювати на ходу. Приклад створення класу в функції за допомогою class :

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

Але це не дуже-то динамічно, так як все одно доведеться прописувати весь клас самостійно.

Виходячи з того, що класи є об'єктами, можна зробити висновок, що вони повинні чимось генеруватися. Під час виконання оператора class, Python автоматично створює цей об'єкт, але є можливість зробити це вручну.

Пам'ятайте функцію type ? Стара добра функція, що дозволяє визначити тип об'єкта:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

Ця функція може створювати класи на ходу. Як параметр type приймає опис класу, і повертає клас.

Функція type працює наступним чином:

type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

наприклад:

>>> class MyShinyClass(object):
...       pass

Можна створити вручну:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

Ймовірно, ви звернули увагу на те, що MyShinyClass виступає і в якості імені класу, і в якості змінної для зберігання посилань на клас.

type приймає словник для визначення атрибутів класу.

>>> class Foo(object):
...       bar = True

Можна написати як:

>>> Foo = type('Foo', (), {'bar':True})

Використовується як звичайний клас:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

Звичайно ж, його можна успадковувати:

>>>   class FooChild(Foo):
...         pass

вийде:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

Потім в свій клас потрібно буде додати методи. Для цього просто визначте функцію з відповідною сигнатурою і призначте її як атрибут.

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

Після динамічного створення можна додати ще методів:

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

У чому ж суть? Класи в Python є об'єктами, тому можна динамічно створювати клас на ходу. Саме це і робить Python під час виконання оператора class.

Що ж таке метаклас?

Якщо говорити в двох словах, то метаклас - це "штуковина", що створює класи. Щоб створювати об'єкти, ми визначаємо класи, правильно? Але ми дізналися, що класи в Python є об'єктами. Насправді метакласи - це те, що створює дані об'єкти. Досить складно пояснити. Краще наведемо приклад:

MyClass = MetaClass()
my_object = MyClass()

Раніше вже згадувалося, що type дозволяє робити щось на зразок цього:

MyClass = type('MyClass', (), {})

Швидше за все, ви ставите питанням: чому ім'я функції пишеться з маленької літери?

Це питання відповідності зі  str - класом, який відповідає за створення рядків, і int - класом, що створює цілочисельні об'єкти. type - це просто клас, який створює об'єкти класу. Перевірити можна за допомогою атрибута __class__ . Все, що ви бачите в Python - об'єкти. У тому числі і рядки, числа, класи і функції. Все це об'єкти, і всі вони були створені з класу:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

Цікаве питання: який __class__ у кожного __class__ ?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

Можна зробити висновок, що метаклас створює об'єкти класу. Це можна назвати "фабрикою класів". type - вбудований метаклас, який використовує Python. Також можна створити свій власний метаклас.

Атрибут __metaclass__

При написанні класу можна додати атрибут __metaclass__ :

class Foo(object):
    __metaclass__ = something...
    [...]

Якщо це зробити, то для створення класу Foo Python буде використовувати метаклас.

Якщо написати class Foo (object), об'єкт класу Foo не відразу створиться в пам'яті.

Python буде шукати __metaclass__ . Як тільки атрибут буде знайдений, він використовується для створення класу Foo. У тому випадку, якщо цього не відбудеться, Python буде використовувати type для створення класу.

Якщо написати:

class Foo(Bar):
    pass

Python робить наступне:

Перевірить, чи є атрибут __metaclass__ у класі Foo ? Якщо він є, створить в пам'яті об'єкт класу з ім'ям Foo з використанням того, що знаходиться в __metaclass__ .

Якщо Python раптом не зможе знайти __metaclass__ , він буде шукати цей атрибут на рівні модуля і після цього повторить процедуру. У разі якщо він взагалі не може знайти будь-якого __metaclass__ , Python використовує власний метаклас type , щоб створити об'єкт класу.

Тепер питання: що можна додати в __metaclass__ ?

Відповідь: що може створювати класи.

А що може створити клас? type або його підкласи, а також все, що його використовує.

Призначені для користувача метакласи

Основною метою метакласу є автоматична зміна класу під час його створення. Зазвичай це робиться для API, коли потрібно створити класи, відповідні поточному контексту. Наприклад, ви вирішили, що всі класи в модулі повинні мати свої атрибути, і вони повинні бути записані у верхньому регістрі. Щоб вирішити це завдання, можна задати __metaclass__ на рівні модуля.

Таким чином, потрібно просто повідомити метакласу, що всі атрибути повинні бути в верхньому регістрі. __metaclass__ дійсно може бути будь-яким  об'єктом, він не обов'язково повинен бути формальним класом. Отже, почнемо з простого прикладу, з використанням функції.

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """

    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

Тепер те ж саме, але з використанням метакласу:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

Але це не зовсім ООП, так як type не переважає, а викликається безпосередньо. Давайте реалізуємо це:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
               uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

Швидше за все, ви помітили додатковий аргумент upperattr_metaclass. У ньому немає нічого особливого: цей метод першим аргументом отримує поточний екземпляр. Точно так же, як і self для звичайних методів. Імена аргументів такі довгі для наочності, але для self  все імена мають назви звичайної довжини. Тому реальний метаклас буде виглядати так:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

Використовуючи метод super , можна зробити код більш "чистим":

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

Причина складності коду, що використовує метакласи, полягає не в самих метакласах. Код складний тому, що зазвичай метакласи використовуються для складних завдань, заснованих на спадкуванні, інтроспекції й маніпуляції такими змінними, як __dict__

Навіщо використовувати класи метаклас замість функцій?

Є кілька причин для цього:

  • Зрозуміліші назви. Коли ви читаєте UpperAttrMetaclass (type) , ви знаєте що буде далі.
  • Ви можете використовувати ООП. Метаклас може успадковуватися від метакласу, перевизначати батьківські методи.
  • Можна краще структурувати свій код. Навряд чи ви будете використовувати метакласи для чогось простого. Звичайно це більш складні завдання. Можливість створювати кілька методів і групувати їх в один клас дуже корисна, щоб зробити код більш зручним для читання.
  • Можете використовувати __new__ , __init__ і __call__ . Це відкриває простір для творчості. Зазвичай все це можна зробити в __new__ , але деяким людям просто зручніше працювати в __init__ .

Навіщо використовувати метакласи?

"Метакласи - це магія, про яку 99% користувачів не варто навіть замислюватися. Якщо вам цікаво, чи потрібні вони вам - тоді точно ні. Люди, яким вони насправді потрібні, знають, навіщо, і що з ними робити."

~ Гуру Python Tim Peters

В основному метакласи використовуються для створення API. Типовим прикладом є Django ORM. Можна написати щось на зразок цього:

class Person(models.Model):

    name = models.CharField(max_length=30)

    age = models.IntegerField()

Але якщо написати так:

guy = Person(name='bob', age='35')

print(guy.age)

Він не поверне об'єкт IntegerField . Він поверне код int і навіть може взяти його безпосередньо з бази даних.

Висновок

По-перше, класи - це об'єкти, що створюють екземпляри. Класи самі є екземплярами метакласів.

>&gt;> class Foo(object): pass
>>> id(Foo)
142630324

В Python все є об'єктами. Всі вони є або екземплярами класів, або екземплярами метакласів. За винятком type. type - сам собі метаклас. Його неможливо створити в чистому Python, це можна зробити тільки за допомогою невеликого чітерства.

По-друге, метакласи складні. Якщо вам не потрібні складні зміни класу, метакласи використовувати не варто. Просто змінити клас можна двома способами:

  • вручну
  • декораторами класу

У 99% випадків краще використовувати ці методи, а в 98% зміни класу взагалі не потрібні.

оригінал


0 комментариев
Сортировка:
Добавить комментарий