Python class init kwargs

How to Put Keyword Arguments in your Python Class Definitions

I have recently looked at using a library called SQLModel . SQLModel is an ORM being developed by the same guy behind FastAPI , Sebastián Ramírez. This library combines pydantic and SQLAlchemy to give users a new and hopefully better way to define their Models. This is an example model that inherits from SQLModel .

from sqlmodel import SQLModel, Field class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str secret_name: str age: int | None 

Not gonna lie, defining database models the same way you would a dataclass sounds cool. Now, take a closer look at this syntax table=True beside the class definition. This technique is new to me (and off the record guys, I had to read through some SQLALchemy codes for a few hours simply because I explicitly forgot to put it in my model). When you think about it, ORM models are definitely one of the places where it makes sense to put keyword arguments in the class definition itself. But how would one do it? How would someone prompt users to put a keyword argument in their class definitions?

Using a Metaclass

As the saying goes, if you have to ask why you would need a metaclass, then you probably don’t need it. But now that we have a use case, let’s have a discussion. Refer to the code below:

class StudentMeta(type): def __new__(cls, *args, **kwargs): print(f"args = >") print(f"kwargs = >") return super().__new__(cls, *args) class Student(metaclass=StudentMeta, table=True): . troy = Student() 

StudentMeta is our metaclass, Student is our class, and troy is a Student object. Below is the output of the code:

args = ('Student', (), '__module__': '__main__', '__qualname__': 'Student'>) kwargs = 'table': True> 

Note that args is a tuple with 3 elements. Traditionally, these are called name , bases , and dict (not to be confused with the dictionary built-in). You may go further and run the following:

print(f"troy.__class__.__name__ = >") print(f"troy.__class__.__bases__ = >") print(f"troy.__class__.__dict__ = >") # Output should be: # troy.__class__.__name__ = 'Student' # troy.__class__.__bases__ = (,) # troy.__class__.__dict__ = mappingproxy(, '__weakref__': ,'__doc__': None>) 
  • troy is an object of type Student ;
  • Student is a class of type StudentMeta ;
  • StudentMeta is a class of type .

StudentMeta is undergoing a normal type creation, except that we have it modified to print all args and kwargs used in the instantiation. Take note that the return value of __new__ is return super().__new__(cls, *args) . This is because type(. ) require only the name , bases , and dict to create a new type.

That leaves us with kwargs. Do note that print(f»») was triggered when Student was defined.

Using __init__subclass__

__init__subclass__ use subclassing to modify the behavior of a Parent’s subclass.

class Base: def __init_subclass__(cls, *args, **kwargs) -> None: print(f"args = >") print(f"kwargs = >") super().__init_subclass__() class Student(Base, table=True): . abed = Student() 
  • abed is an object of type Student ;
  • Student is a class of type ;
  • Base is a class of type .
args = () kwargs = 'table': True> 

There are no args because we did not have to create a new type ; __init__subclass__ only modifies the creation of a new child class (which is what we did in the earlier example). Note that __init__subclass__ does not have a return statement.

print(f"abed.__class__.__name__ = >") print(f"abed.__class__.__bases__ = >") print(f"abed.__class__.__dict__ = >") # Output should be: # abed.__class__.__name__ = 'Student' # abed.__class__.__bases__ = (,) # abed.__class__.__dict__ = mappingproxy() 

Putting it all together

In this article, we covered two ways to use keyword arguments in your class definitions. Metaclasses offer a way to modify the type creation of classes. A simpler way would be to use __init__subclass__ which modifies only the behavior of the child class’ creation.

Regardless of the method, these keyword arguments can only be used during the creation of a class.

Источник

Инстанцирование в Python

Какой метод вызывается первым при этом вызове Foo? Большинство новичков, да и, возможно, немало опытных питонистов тут же ответят: «метод __init__». Но если внимательно приглядеться к сниппетам выше, вскоре станет понятно, что такой ответ неверен.

__init__ не возвращает никакого результата, а Foo(1, y=2), напротив, возвращает экземпляр класса. К тому же __init__ принимает self в качестве первого параметра, чего не происходит при вызове Foo(1, y=2). Создание экземпляра происходит немного сложнее, о чём мы и поговорим в этой статье.

Порядок создания объекта

Инстанцирование в Python состоит из нескольких стадий. Понимание каждого шага делает нас чуть ближе к пониманию языка в целом. Foo — это класс, но в Питоне классы это тоже объекты! Классы, функции, методы и экземпляры — всё это объекты, и всякий раз, когда вы ставите скобки после их имени, вы вызываете их метод __call__. Так что Foo(1, y=2) — это эквивалент Foo.__call__(1, y=2). Причём метод __call__ объявлен в классе объекта Foo. Какой же класс у объекта Foo?

Так что класс Foo — это экземпляр класса type и вызов метода __call__ последнего возвращает класс Foo. Теперь давайте разберём, что из себя представляет метод __call__ класса type. Ниже находятся его реализации на C в CPython и в PyPy. Если надоест их смотреть, прокручивайте чуть дальше, чтобы найти упрощённую версию:

CPython

static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) < PyObject *obj; if (type->tp_new == NULL) < PyErr_Format(PyExc_TypeError, "cannot create '%.100s' instances", type->tp_name); return NULL; > obj = type->tp_new(type, args, kwds); obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL); if (obj == NULL) return NULL; /* Ugly exception: when the call was type(something), don't call tp_init on the result. */ if (type == &PyType_Type && PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 && (kwds == NULL || (PyDict_Check(kwds) && PyDict_Size(kwds) == 0))) return obj; /* If the returned object is not an instance of type, it won't be initialized. */ if (!PyType_IsSubtype(Py_TYPE(obj), type)) return obj; type = Py_TYPE(obj); if (type->tp_init != NULL) < int res = type->tp_init(obj, args, kwds); if (res < 0) < assert(PyErr_Occurred()); Py_DECREF(obj); obj = NULL; >else < assert(!PyErr_Occurred()); >> return obj; >

PyPy

def descr_call(self, space, __args__): promote(self) # invoke the __new__ of the type if not we_are_jitted(): # note that the annotator will figure out that self.w_new_function # can only be None if the newshortcut config option is not set w_newfunc = self.w_new_function else: # for the JIT it is better to take the slow path because normal lookup # is nicely optimized, but the self.w_new_function attribute is not # known to the JIT w_newfunc = None if w_newfunc is None: w_newtype, w_newdescr = self.lookup_where('__new__') if w_newdescr is None: # see test_crash_mro_without_object_1 raise oefmt(space.w_TypeError, "cannot create '%N' instances", self) w_newfunc = space.get(w_newdescr, self) if (space.config.objspace.std.newshortcut and not we_are_jitted() and isinstance(w_newtype, W_TypeObject)): self.w_new_function = w_newfunc w_newobject = space.call_obj_args(w_newfunc, self, __args__) call_init = space.isinstance_w(w_newobject, self) # maybe invoke the __init__ of the type if (call_init and not (space.is_w(self, space.w_type) and not __args__.keywords and len(__args__.arguments_w) == 1)): w_descr = space.lookup(w_newobject, '__init__') if w_descr is not None: # see test_crash_mro_without_object_2 w_result = space.get_and_call_args(w_descr, w_newobject, __args__) if not space.is_w(w_result, space.w_None): raise oefmt(space.w_TypeError, "__init__() should return None") return w_newobject

Если забыть про всевозможные проверки на ошибки, то коды выше примерно эквивалентны такому:

def __call__(obj_type, *args, **kwargs): obj = obj_type.__new__(*args, **kwargs) if obj is not None and issubclass(obj, obj_type): obj.__init__(*args, **kwargs) return obj

__new__ выделяет память под «пустой» объект и вызывает __init__, чтобы его инициализировать.

  1. Foo(*args, **kwargs) эквивалентно Foo.__call__(*args, **kwargs).
  2. Так как объект Foo — это экземпляр класса type, то вызов Foo.__call__(*args, **kwargs) эквивалентен type.__call__(Foo, *args, **kwargs).
  3. type.__call__(Foo, *args, **kwargs) вызывает метод type.__new__(Foo, *args, **kwargs), возвращающий obj.
  4. obj инициализируется при вызове obj.__init__(*args, **kwargs).
  5. Результат всего процесса — инициализированный obj.

Кастомизация

Теперь давайте переключим наше внимание на __new__. Этот метод выделяет память под объект и возвращает его. Вы вольны кастомизировать этот процесс множеством разных способов. Следует отметить, что, хотя __new__ и является статическим методом, вам не нужно объявлять его используя @staticmethod: интерпретатор обрабатывает __new__ как специальный случай.

Распространённый пример переопределения __new__ — создание Синглтона:

class Singleton(object): _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance
>>> s1 = Singleton() . s2 = Singleton() . s1 is s2 True

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

Другой пример переопределения __new__ — реализация паттерна Борг («Borg»):

class Borg(object): _dict = None def __new__(cls, *args, **kwargs): obj = super().__new__(cls, *args, **kwargs) if cls._dict is None: cls._dict = obj.__dict__ else: obj.__dict__ = cls._dict return obj
>>> b1 = Borg() . b2 = Borg() . b1 is b2 False >>> b1.x = 8 . b2.x 8

Учтите, что хотя примеры выше и демонстрируют возможности переопределения __new__, это ещё не значит что его обязательно нужно использовать:

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

— Арион Спрэг, Хорошо забытое старое в Python

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

Источник

Читайте также:  Обращение по индексу питон
Оцените статью