Во всех языках программирования мы очень рано узнаем о синтаксисе. Синтаксис — это хлеб с маслом для кодирования, без которого наш код просто не будет работать. Одним из элементов синтаксиса Python является ключевое слово class, которое необходимо для создания класса. Но вы действительно?
Мы рассмотрим, как вы можете создать класс в Python, не используя ключевое слово class. Во время этого исследования вы узнаете, что такое класс и все остальные типы данных на самом деле находятся под капотом. Используя эти новообретенные знания, вы сможете делать вещи, которые другие сочли бы невозможными или, по крайней мере, очень сложными.
Заинтересованы? Давайте начнем!
Изучая Python, вы узнаете, что для создания класса вы начинаете с ключевого слова class. Вы даете имя своему классу, добавляете все наследования в скобках и определяете атрибуты и методы своего класса. Ниже приведен простой пример собаки и классов, наследуемых от класса животных:
class Animal:
num_eyes = 2
def __init__(self, name):
self.name = name
def eat(self, food):
print(f"{self.name} eats {food}."
class Cat(Animal):
def meow(self):
print(f"{self.name} meows."
Мы воссоздадим этот класс без использования ключевого слова class.
Все класс?
Прежде чем мы сможем создать эти классы, нам нужно начать с некоторой теории. Начнем с некоторых простых типов данных. my_string="hello world" и my_int=5 . Если бы я вызывал тип для каждого из них, вы ожидали бы увидеть строку и целое число:
>>> type(my_string) <class 'str'> >>> type(my_int) <class 'int'>
Это именно то, что мы получаем. Но когда мы рассмотрим это более внимательно, мы заметим, что str и int — это имена классов. Давайте создадим простой класс и попытаемся найти больше информации о его типе:
>>> class A: >>> pass >>> A <class '__main__.A' >>> type(A) <class 'type'> >>> type(type(A)) <class 'type'
Когда мы вызвали тип для нашей строки и целого числа, мы обнаружили, что тип — это класс с именами «str» и «int» соответственно.
Но когда мы затем попытаемся вызвать тип нашего класса A, мы вернемся <class 'type'> . Если бы мы снова вызвали тип, то получили бы тот же результат. Верно ли то же самое для наших примитивных типов данных?
>>> type(my_string) <class 'str'> >>> type(type(my_string)) <class 'type'> >>> type(int) <class 'str'> >>> type(type(int)) <class 'type'>
Итак, тип всего — это тип? Это не имеет смысла, верно? Давайте более подробно рассмотрим ключевое слово type.
Что такое… Тип?
Мы знаем, что type может принимать один аргумент и возвращать тип этого объекта. Но вы, возможно, не знали, что этот тип может принимать до трех аргументов type(name, bases, dict) -> a new type. Это:
- Имя нового типа
- Любые базы (наследства)
- Словарь атрибутов для нового типа.
Получив эти фрагменты информации, мы получаем новый тип.
Создание класса с помощью ключевого слова type
Давайте попробуем воссоздать следующее, используя только type:
class Animal:
num_eyes = 2
def __init__(self, name):
self.name = name
def eat(self, food):
print(f"eats {food}"
Разобьем класс на более мелкие компоненты. Во-первых, у нас есть имя класса «Животное». У нас есть простой атрибут с именем num_eyes и два метода __init__ и __eat__.
Назовите класс
Начнем с того, что дадим ему имя.
Animal = type("Animal", (), {})
На данный момент мы создали:
class A:
pass
Добавьте атрибуты класса
У этого класса нет наследования, поэтому мы можем пропустить базы и перейти к словарю атрибутов. Здесь мы установим атрибуты класса.
def animal_init(self, name):
self.name = name
Animal = type(
"Animal",
(),
{
"__init__": animal_init,
"eat": lambda self, food: print(f"eats {food}")},
"num_eyes": 2,
}
)
Итак, мы создали наш класс. Теперь у нашего класса есть инициализатор, метод eat и атрибут num_eyes.
Теперь мы воссоздали следующий класс без использования ключевого слова class:
class Animal:
num_eyes = 2
def __init__(self, name):
self.name = name
def eat(self, food):
print(f"eats {food}"
Добавление наследства
Давайте теперь сделаем то же самое с классом Cat. С этим классом нам нужно будет добавить наследование, поскольку он наследуется от Animal.
Опять же, давайте начнем с того, что назовем его и добавим его атрибуты:
Cat = type(
"Cat",
(),
{"meow": lambda self: print(f"{self.name} meows.")}
)
Мы создали следующий класс:
class Cat:
def meow(self):
print(f"{self.name} meows."
Вызов meow для экземпляра Cat вызовет AttributeError, поскольку Cat не имеет атрибута name. Давайте добавим наследование Animal, чтобы исправить это.
Cat = type(
"Cat",
(Animal,),
{"meow": lambda self: print(f"{self.name} meows.")}
)
Теперь мы обновили наш класс до следующего:
class Cat(Animal):
def meow(self):
print(f"{self.name} meows."
И теперь вызов meow будет работать, так как инициализатор требует предоставления name.
Теперь вы узнали, как создавать классы без ключевого слова class. Теперь вы можете взорвать чей-то мозг тем, что вы узнали.
Но подождите, есть еще!
__name__, __bases__ и __dict__
Вы узнали, что тип может принимать три аргумента для создания нового типа. Это name, bases и dict. Если бы мы взяли любой предмет, смогли бы мы узнать его название, основания и дикт? Абсолютно!
Начнем с нашего класса Cat.
>>> Cat.__name__
'Cat'
>>> Cat.__bases__
(<class '__main__.Animal'>,)
>>> Cat.__dict__
mappingproxy({
'meow': <function <lambda> at 0x7f5c470d8310>,
'__module__': '__main__', '__doc__': None
})
Обратите внимание, что мы получили те же аргументы, что и при вызове type (в основном). Вы заметите, что Python добавил для вас аргументы __module__ и __doc__.
Давайте попробуем то же самое с нашим классом Animal.
>>> Animal.__name__
'Animal'
>>> Animal.__bases__
(<class 'object'>,)
>>> Animal.__dict__
mappingproxy({
'__init__': <function a_init at 0x7f5c470aa940>,
'eat': <function <lambda> at 0x7f5c470d84c0>,
'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'Animal' objects>,
'__weakref__': <attribute '__weakref__' of 'Animal' objects>,
'__doc__': None
})
Мы замечаем, что на этот раз Python добавил для нас наследование. Кроме того, при вызове __dict__ возвращается больше элементов. Это связано с тем, что все классы, не имеющие наследования, фактически являются производными от класса объекта. Мы узнали, что int, str и другие примитивные типы данных являются классами. Попробуйте вызвать __name__ , __bases__ и __dict__ на каждом из них и посмотрите, что произойдет. Вы заметите, что и int, и str также содержат наследование object.
С большой властью приходит большая ответственность
Теперь, когда вы научились создавать классы без классов, необходимо задать вопрос. "Должен ли я теперь использовать этот новый метод для создания классов?" Ответ: "в основном нет".
Python предлагает отличный способ создания классов с использованием ключевого слова class. Он легко читается и уже используется большинством людей. Помните, что удобочитаемость имеет значение, поэтому в большинстве случаев я бы посоветовал продолжать следовать этому стандарту.
Однако вы можете столкнуться с ситуациями, когда это может быть полезно, даже в таких ситуациях помните о читабельности. Тем не менее, для полноты картины давайте рассмотрим две вещи, которые вы можете сделать с type :
Отказ от наследства
Функцию type можно использовать, когда вы хотите скопировать класс, но удалить его собственное наследие.
class Duck(Animal):
def speak(self):
return "quack"
AngryDuck = type(
"AngryDuck",
(),
{**Duck.__dict__, "speak": lambda self: "QUUAAACKK"}
)
Имитация self в модульных тестах
Предположим, вы хотите создать простой объект, имитирующий объект self. Это можно использовать в модульном тестировании, когда вы хотите избежать создания экземпляра класса. Предположим, вы хотите протестировать функцию display_name следующего класса:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
self.something_complex()
def something_complex(self):
"""Does something complex that we want to avoid."""
def display_name(self):
return f"{self.first_name[0]} {self.last_name}"
Это простой класс, поэтому создать его экземпляр несложно. Однако этот класс делает что-то сложное как часть __init__, чего мы хотим избежать. Здесь может быть полезно создание пользовательского объекта:
person_instance = type(
"person",
(),
{"first_name": "John", "last_name": "Doe"}
)
Теперь при модульном тестировании мы можем протестировать функцию напрямую:
def test_display_name():
assert Person.display_name(person_instance) == "J Doe"
В большинстве случаев вам следует использовать ключевое слово class для создания классов. Приведенный выше пример модульного теста является таким примером, где использование ключевого слова class было бы предпочтительнее.
Краткое содержание
Изучая, как создать класс без использования ключевого слова class, мы узнали несколько интересных фактов о том, как работает Python. Мы узнали:
- Все классы имеют тип
type. - Все примитивные типы данных относятся к типу
type. - Точно так же, как класс является классом, все типы данных также являются классами.
- Как мы можем проверить имя, базы (наследство) и dict (атрибуты) всех классов.
typeможет содержать 1 или 3 ключевых слова. Одно ключевое слово показывает тип объекта, а три ключевых слова могут использоваться для создания нового класса.
Взяв увеличительное стекло за классы Python и типы данных, мы узнали немного больше о том, как они работают внутри. Я надеюсь, что эта статья помогла вам понять Python с другой стороны. Если вы думаете о больших возможностях использования type для создания объектов, прокомментируйте и дайте мне знать!
Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Посетите наш Community Discord и присоединитесь к нашему Коллективу талантов.