Когато работите с класове в Python, атрибутите на класа могат да се използват за стойности по подразбиране, които могат да бъдат персонализирани по време на инициализация, като например в:

class A:
    value1 = 'value1'
    value2 = 'value2'
    def __init__(self, value1='value1', value2='value2'):
        self.value1 = self.value1
        self.value2 = self.value2

Което може да се обобщи с помощта на **kwargs:

class A:
    value1 = 'value1'
    value2 = 'value2'
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)

Случай, в който добавихме ограничение за разглеждане само на **kwargs, които вече са дефинирани в класа. Но дори и с този модел някои неща може да имат място за подобрение:

  • Избягвайте ръчното кодиране в __init__ (със или без **kwargs)
  • Контролирайте типовете, които се предават
  • Трансформирайте стойностите, ако е необходимо
  • Документирайте типовете, когато са декларирани, а не в документационния низ

Човек, разбира се, може да добави съвети за тип в най-новите версии на Python и да документира параметрите в docstring, но:

  • Подсказките за тип са точно това... подсказки
  • Изглежда по-добре да се документира параметърът, когато е дефиниран.
  • Няма начин да разберете (освен чрез четене на документите, които понякога не съществуват) дали класът се нуждае от повикващия да предостави стойност

И едно последно предупреждение:

  • Тези атрибути, които трябва да бъдат конфигурирани чрез __init__, не са отделени от други атрибути в екземплярите

Моделът на метапарамите

Инсталирайте го с:

pip install metaparams

Следователно библиотеката metaparams предлага следния модел:

import metaparams
class A(metaparams.ParamsBase):
    params = {
        'value1': {
            'value': 'value1',
            'doc': 'This is value1',
            'required': False,
            'type': str,
            'transform': None,
        },
        'value2': {
            'required': True,
        },
        'value3': 'value3',
    }

Предоставихме пълна дефиниция за стойност1, намалена за стойност2 и само действителната стойност по подразбиране е предоставена за стойност3

  • value1 се документира, получава стойност по подразбиране, маркира се като не задължително, трябва да бъде от тип str и няма да претърпи трансформация (Не е дадено, а не функция за трансформация)
  • value2 от друга страна просто е маркирано като задължително. Ако не бъде предоставено, когато се инстанцира хост класът, ще бъде предизвикано Изключение
  • Забележка: Няма нужда да предоставяте стойност по подразбиране за value2, тъй като повикващият трябва действително да предостави стойност.
  • value3 получава стойност по подразбиране. Няма да бъде задължително, няма документация, няма конкретна дефиниция на тип и няма функция за трансформация.

Сега човек може да направи следното:

a = A(value2=22, value3='this is my value')
print(a.params.value1)  # shorthand a.p.value1
print(a.params.value2)  # shorthand a.p.value2
print(a.params.value3)  # shorthand a.p.value3

който отпечатва:

value1
22
this is my value

Забележете, че не сме дефинирали __init__ и въпреки това value2 и value3 са получили стойностите, предадени на екземпляра на класа. Това е така, защото зад кулисите се е случило следното:

  • Дефиницията на params (dict) е превърната динамично в подклас на metaparams.Params
  • Когато A се инстанцира в a, подкласът Params също се инстанцира, прихваща **kwargs и използва стойностите и се инсталира в екземпляра на класа.
  • Следователно има двойственост клас-клас и екземпляр-екземпляр в това, че: A, клас, има атрибут params, който е подклас на metaparams. Params и a, екземпляр, има атрибут params, който е екземпляр на A.params
  • Това е възможно, защото в Python атрибутите на ниво екземпляр скриват дефиницията на ниво клас (без да я презаписват)

Човек все още може да дефинира __init__ и дори да има допълнителни **kwargs, предадени към него:

import metaparams
class A(metaparams.ParamsBase):
    params = {
        'value1': {
            'value': 'value1',
            'doc': 'This is value1',
            'required': False,
            'type': str,
            'transform': None,
        },
        'value2': {
            'required': True,
        },
        'value3': 'value3',
    }
    def __init__(self, **kwargs):
        print('Extra **kwargs:', kwargs)

И след това направете:

a = A(value2=22, some_extra_kw='hello')

който отпечатва:

Extra **kwargs: {'some_extra_kw': 'hello'}

Необходими параметри

Нека да видим какво се случва, когато задължителен параметър (стойност2 в нашите примери) не е предоставен по време на инстанциране:

a = A(value1='only value1')

И грешката е:

...
    a = A(value1='only value1')
...
    raise ValueError(errmsg)
ValueError: Missing value for required parameter "value2" in parameters "__main___A_params"

Повдигнатото изключение е ValueError, тъй като не е предоставена стойност, се повдига, за да уведоми повикващия, че трябва да бъде предоставена стойност2.

Забележка

Името, присвоено автоматично с магия на динамично създадения клас параметри, се опитва да бъде описателно и да ни уведоми къде са нещата. В този случай името е __main___A_params, т.е.:

  • Модул __main__, в клас A

Пълното проследяване, разбира се, също ще посочи в кой файл и ред е възникнала грешката

Проверка на типа

Вече имаме тип, указан за value1, който е str. Нека да видим какво се случва, ако предадем float:

a = A(value2=45, value1=22.0)

Резултатът:

...
    a = A(value2=45, value1=22.0)
...
    raise TypeError(errmsg)
TypeError: Wrong type "<class 'float'>" for param "value1" with type <class 'str'> in parameters "__main___A_params"

TypeError (очевидно) се повдига, ако предадената стойност не е от типа, дефиниран за параметъра.

Трансформация

В примерите по-горе сме показали само дефиницията с:

transform=None

като един от компонентите на параметъра. Нищо не показва, че нищо не трябва да се прави. Нека променим това, за да видим как работят нещата:

import metaparams
class A(metaparams.ParamsBase):
    params = {
        'value1': {
            'value': 'value1',
            'doc': 'This is value1',
            'required': False,
            'type': str,
            'transform': lambda x: x.upper(),
        },
        'value2': {
            'required': True,
        },
        'value3': 'value3',
    }
a = A(value1='hello', value2='no value 2')  # supply required value2
print('a.params.value1:', a.params.value1)

В трансформацията можем да сме сигурни, че можем да приложим x.upper(), защото изискваме типът да бъде str.

Резултатът:

a.params.value1: HELLO

което показва нашата входна стойност hello в главни букви.

Автоматична документация

Една от причините да се занимаваме с това е да документираме параметъра, когато се дефинира. В горните примери това се прави за value1. И магията зад кулисите прави възможно следното да е вярно:

print(A.__doc__)  # print the docstring

което води до следния изход:

Args
  - value1: (default: value1) (required: False) (type: <class 'str'>) (transform: None)
    This is value1
  - value2: (default: None) (required: True) (type: None) (transform: None)
  - value3: (default: value3) (required: False) (type: None) (transform: None)

Параметрите са се документирали автоматично в класа на хоста, което означава, че те ще бъдат например част от автоматично генерирана документация, когато използвате, например, Sphinx

Където наличието на bool или str ще определи дали третата стойност е низът на документа или изискваната индикация.

интеграция на argparse

Моделът params може да се използва за динамично генериране на опции на командния ред с модула argparse, т.е.: добавянето на нови дефиниции към параметрите на клас ще добави нови превключватели на командния ред, които да съответстват на тези дефиниции.

Генериране на превключватели на командния ред

import argparse
from metaparams import ParamsBase
parser = argparse.ArgumentParser(
    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    description=(
        'Some script with auto-generated command line switches '
    )
)
class A(ParamsBase):
  params = {
      'value1': {
          'value': 'value1',
          'doc': 'This is value1',
          'required': False,
          'type': str,
          'transform': None,
      },
      'value2': {
          'required': True,
      },
      'value3': 'value3',
  }

# The integration of the params in the command line switches
A.params._argparse(parser)

Използване на параметрите за инстанциране

args = parser.parse_args()
# The integration of command line switches values for instantiation
a = A(**A.params._parseargs(args))

Или още по-просто:

args = parser.parse_args()
# The integration of command line switches values for instantiation
a = A.params._create(args)

API

Стойностите на параметрите, както е показано по-горе, могат да бъдат достъпни с . (точка) нотация, но има много повече, което може да се направи. Всички методи са с префикс с водеща долна черта (_), за да се избегне сблъсък с имената на параметрите, които крайният потребител може да избере.

Забележете следната връзка клас-клас и екземпляр-екземпляр

  • A.params — Тук A е хост класът, съдържащ параметри, а A.params е параметър клас (динамично генериран)
  • a.params — Тук a е екземпляр на A и a.params е екземпляр на A.params

Персонализиране

Параметрите по подразбиране се дефинират с името params в хост класа:

class A(Paramsbase):
    params = {
        ...
    }

И са достъпни в екземпляра на хост класа като:

a = A()
a.params
# 1st letter of the name params. If the name had a leading underscore
# such as _params, the shortcut would be _p
a.p

Името params и създаването на стенограмата p могат да бъдат персонализирани, когато Paramsbase е подклас с помощта на ключови аргументи за Python ›= 3.6:

from metaparams import MetaParams
class A_poroms(metaclass=MetaParams, _pname='poroms', _pshort=False)
    poroms = {
        ...
    }

Забележете как вместо подкласиране от ParamsBase, когато променяте името на параметрите, това трябва да бъде посочено чрез metaclass=MetaParams

Това е така, защото ParamsBase вече е дефинирал параметри с фиксирано име за декларацията и това вече е зададено за всеки подклас. Причината е, че атрибутите на класа (да не се бъркат с атрибутите на екземпляра) не могат да бъдат изтрити. Замяната на името за декларацията на params би довела до множество атрибути на клас params в хост класа

Ако използвате Python ‹ 3.6, използвайте декоратора, тъй като не се поддържат аргументи за ключови думи при създаването на клас:

from metaparams import metaparams
@metaparams(_pname='poroms', _pshort=False)
class A_poroms:
    poroms = {
        ...
    }

В такъв случай:

  • Параметрите са дефинирани и достъпни под името poroms
  • Не е създаден пряк път p

Друг пример (Python ≥ 3.6)

class A_poroms(metaclass=MetaParams, _pname='_xarams')
    _xarams = {
        ...
    }

или: (Python ‹ 3.6)

from metaparams import metaparams
@metaparams(_pname='_xarams')
class A_poroms:
    _xarams = {
        ...
    }

И сега

  • Параметрите са достъпни под името _xarams
  • Ще бъде създаден пряк път с _x

Характеристиките

Един параметър може да бъде канонично дефиниран (както вече се видя по-горе) по 3 различни начина.

  • Използване на име: запис на стойност в речника на params. Като:
params = { 'myparam1': 'myvalue1', }
  • Това ще бъде вътрешно преведено в пълен dict запис, както е посочено по-долу. Използване на пълен dict запис за param:
params = {
    'myparam1': {
    # Default value for the parameter (default: None)
    'value': 'myvalue1',
    # if param is required for host instantiation (default: False)
    'required': False,
    # Document the param (default: '')
    'doc': 'my documentation',
    # Check if given type is passed (default: None)
    'type': str,
    # Transform given parameter with function (default: None)
    'transform': lambda x: x.upper(),
    # If params should be part of argparse integration (def: True)
    'argparse': True,
}

Забележка: Ако името на параметър завършва с _, той автоматично ще бъде изключен от интегрирането на argparse

Персонализиране

Следните ключови аргументи се приемат от дефиниция на клас (Python ›= 3.6) или от декоратора.

  • _pname (по подразбиране: параметри) Това дефинира основното име за декларацията и атрибута за достъп до декларираните параметри. Забележка: Ако един от базовите класове (като ParamsBase) вече е задал това име, то не може да бъде заменено от подкласове.
  • _pshort (по подразбиране: True) Осигурете 1-буквена стенограма на името, дефинирано в _pname в екземпляра на хост класа, съдържащ параметрите. Например: параметрите също ще бъдат инсталирани като p. Ако дефинираното име има водещ _ (долна черта), то ще бъде зачетено и следващият знак също ще бъде взет. Например: _myparams ще бъде съкратен до _m
  • _pinst (по подразбиране: False) Валидно само в комбинация с _pshort = True. Инсталирайте атрибут на екземпляр, като използвате съкратеното обозначение, _ (долна черта) и името на параметъра. Ако декларацията на params изглежда така:
    class A(ParamsBase, _pinst=True):
          params = {
              'myparam': True,
          }
  • Следното ще бъде вярно в случай на A:
a = A() assert(a.params.myparam == a.p_myparam)

Методите

Класът/инстанциите на params предлага набор от методи като публичен API за проверка на стойностите, стойностите по подразбиране, документация, управление на интеграцията на argparse. Проверете документите: https://metaparams.readthedocs.io

И се забавлявай!