Рубрики
Без рубрики

Самоинициализирующиеся классы

Базовый учебник о том, как использовать метаклассы и декораторы в Python.

Автор оригинала: Marco Predari.

Python славится своей высокой производительностью, в основном благодаря чистому и лаконичному синтаксису. Тем не менее, некоторые задачи требуют написания шаблонного кода. Например, любой конструктор класса получит необходимые значения для инициализации состояния экземпляра объекта с помощью последовательности self.fieldname .

Обычно имя формальных параметров конструктора совпадает с полями для инициализации (т. Е. Каждая строка конструктора будет читаться как self.fieldname ).

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

Требования

  • Версия Python 3.*

дорожная карта

Я создам класс с именем Person с некоторыми атрибутами наиболее распространенным способом. Затем я шаг за шагом представлю код с метаклассами и декораторами с логикой “автоматического инициализации”, также используя магический модуль inspect .

Базовый код

Я создал main.py и в начале мой код выглядит так:

  class Person ():
    def __init__(self, first_name, last_name, birth_date, sex, address):
      self.first_name = first_name
      self.last_name  = last_name
      self.birth_date = birth_date
      self.sex        = sex
      self.address     = address
    def __repr__(self):
      return "{} {} {} {} {}"\
      .format(self.first_name, self.last_name, self.birth_date, self.sex, self.address)
  
  if __name__ == '__main__':
    john = Person('Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street')
    print(john)

Модуль автозапуска

Я создам новый файл с именем autoinit.py , где я буду размещать всю свою логику.

class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    return type.__new__(meta, classname, supers, classdict)

Я создал класс AutoInit , который на данный момент ничего не делает. Это всего лишь простой метакласс. Поэтому я могу сразу же использовать свой метакласс в классе Person , как это:

#main.py
from autoinit import AutoInit

class Person (metaclass=AutoInit):
  ...
  ...
  ...

Так что это первое изменение в классе person, которое мне пришлось сделать. Мы завершим рефакторинг позже, как только модуль autoinit будет завершен.

Декорируйте метод класса ‘__init__’

Метод __init__ является методом конструктора класса. Чтобы достичь нашей цели, мы должны поместить декоратора вокруг этого метода, чтобы включить нормальное поведение.

#autoinit.py
class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    classdict['__init__'] = autoInitDecorator(classdict['__init__'])
    return type.__new__(meta, classname, supers, classdict)

def autoInitDecorator (toDecoreFun):
  def wrapper(*args):
    print("Hello from autoinit decorator")
    toDecoreFun(*args)
  return wrapper

Модуль “Проверка”

Из модуля inspect я буду использовать функцию getargspec . Из документации по Python:

Get the names and default values of a function's arguments.
A tuple of four things is returned: (args, varargs, varkw, defaults).
'args' is a list of the argument names (it may contain nested lists).
'varargs' and 'varkw' are the names of the * and ** arguments or None.
'defaults' is an n-tuple of the default values of the last n arguments.

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

#autoinit.py
class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    classdict['__init__'] = autoInitDecorator(classdict['__init__'])
    return type.__new__(meta, classname, supers, classdict)

def autoInitDecorator (toDecoreFun):
  def wrapper(*args):
    
    # ['self', 'first_name', 'last_name', 'birth_date', 'sex', 'address']
    argsnames = getargspec(toDecoreFun)[0]
    
    # the values provided when a new instance is created minus the 'self' reference
    # ['Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street']
    argsvalues = [x for x in args[1:]]
    
    # 'self' -> the reference to the instance
    objref = args[0]
    
    # setting the attribute with the corrisponding values to the instance
    # note I am skipping the 'self' reference
    for x in argsnames[1:]:
     	objref.__setattr__(x,argsvalues.pop(0))
    
  return wrapper

Рефакторинг кода

Так что мой main.py код будет выглядеть следующим образом:

  from autoinit import AutoInit

  class Person (metaclass=AutoInit):
    def __init__(self, first_name, last_name, birth_date, sex, address):
      pass
    def __repr__(self):
      return "{} {} {} {} {}"\
      .format(self.first_name, self.last_name, self.birth_date, self.sex, self.address)
  
  if __name__ == '__main__':
    john = Person('Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street')
    print(john)

Вот и все, ребята

Обратите внимание, что этот пример не предназначен для работы также с ключевыми аргументами (возможно, в будущей статье я мог бы расширить это упражнение).

Метаклассы и декораторы-очень мощные инструменты в Python. Я очень быстро делаю удивительные вещи на других языках. Но также легко впасть в кошмары кода, когда исходный код вырастает.