Пользовательские атрибуты в Python
Вы когда нибудь задумывались о том, что происходит, когда вы ставите точку в python? Что скрывает за собой символ str(“\u002E”)? Какие тайны он хранит? Если без мистики, вы знаете как происходит поиск и установка значений пользовательских атрибутов в python? Хотели бы узнать? Тогда… добро пожаловать!
Чтобы время, проведённое за чтением прошло легко, приятно и с пользой, было бы неплохо знать несколько базовых понятий языка. В частности, понимание type и object будут исключительно полезны, так же как знание нескольких примеров обеих сущностей. Почитать о них можно, в том числе, здесь.
Немного о терминологии, которую я использую, прежде чем мы приступим к тому, ради чего собрались:
- Объект есть любая сущность в python (функция, число, строка… словом, всё).
- Класс это объект, чьим типом является type (тип можно подсмотреть в атрибуте __class__).
- Экземпляр некоторого класса A — это объект, у которого в атрибуте __class__ есть ссылка на класс A.
Ах, да, все примеры в статье написаны на python3! Это определённо следует учесть.
Если ничто из вышесказанного не смогло умерить ваше желание узнать, что там будет дальше, приступим!
__dict__
- Сам объект (o.__dict__ и его системные атрибуты).
- Класс объекта (o.__class__.__dict__). Только __dict__ класса, не системные атрибуты.
- Классы, от которых унасаледован класс объекта (o.__class__.__bases__.__dict__).
class StuffHolder: stuff = "class stuff" a = StuffHolder() b = StuffHolder() a.stuff # "class stuff" b.stuff # "class stuff" b.b_stuff = "b stuff" b.b_stuff # "b stuff" a.b_stuff # AttributeError
В примере описан класс StuffHolder с одним атрибутом stuff, который, наследуют оба его экземпляра. Добавление объекту b атрибута b_stuff, никак не отражается на a.
Посмотрим на __dict__ всех действующих лиц:
StuffHolder.__dict__ # a.__dict__ # <> b.__dict__ # a.__class__ # b.__class__ #
(У класса StuffHolder в __dict__ хранится объект класса dict_proxy с кучей разного барахла, на которое пока не нужно обращать внимание).
Ни у a ни у b в __dict__ нет атрибута stuff, не найдя его там, механизм поиска ищет его в __dict__ класса (StuffHolder), успешно находит и возвращает значение, присвоенное ему в классе. Ссылка на класс хранится в атрибуте __class__ объекта.
Поиск атрибута происходит во время выполнения, так что даже после создания экземпляров, все изменения в __dict__ класса отразятся в них:
a.new_stuff # AttributeError b.new_stuff # AttributeError StuffHolder.new_stuff = "new" StuffHolder.__dict__ # a.new_stuff # "new" b.new_stuff # "new"
В случае присваивания значения атрибуту экземпляра, изменяется только __dict__ экземпляра, то есть значение в __dict__ класса остаётся неизменным (в случае, если значением атрибута класса не является data descriptor):
StuffHolder.__dict__ # c = StuffHolder() c.__dict__ # <> c.stuff = "more c stuff" c.__dict__ # StuffHolder.__dict__ #
Если имена атрибутов в классе и экземпляре совпадают, интерпретатор при поиске значения выдаст значение экземпляра (в случае, если значением атрибута класса не является data descriptor):
StuffHolder.__dict__ # d = StuffHolder() d.stuff # "class stuff" d.stuff = "d stuff" d.stuff # "d stuff"
По большому счёту это всё, что можно сказать про __dict__. Это хранилище атрибутов, определённых пользователем. Поиск в нём производится во время выполнения и при поиске учитывается __dict__ класса объекта и базовых классов. Также важно знать, что есть несколько способов переопределить это поведение. Одним из них является великий и могучий Дескриптор!
Дескрипторы
С простыми типами в качестве значений атрибутов пока всё ясно. Посмотрим, как ведёт себя функция в тех же условиях:
class FuncHolder: def func(self): pass fh = FuncHolder() FuncHolder.func # FuncHolder.__dict__ # <. 'func': . > fh.func # >
WTF!? Спросите вы… возможно. Я бы спросил. Чем функция в этом случае отличается от того, что мы уже видели? Ответ прост: методом __get__.
FuncHolder.func.__class__.__get__ #
Этот метод переопределяет механизм получения значения атрибута func экземпляра fh, а объект, который реализует этот метод непереводимо называется non-data descriptor.
Дескриптор — это объект, доступ к которому через атрибут переопределён методами в дескриптор протоколе:
descr.__get__(self, obj, type=None) --> value (переопределяет способ получения значения атрибута) descr.__set__(self, obj, value) --> None (переопределяет способ присваивания значения атрибуту) descr.__delete__(self, obj) --> None (переопределяет способ удаления атрибута)
- Data Descriptor (дескриптор данных) — объект, который реализует метод __get__() и __set__()
- Non-data Descriptor (дескриптор не данных?) — объект, который реализует метод __get__()
Дескрипторы данных
Рассмотрим повнимательней дескриптор данных:
class DataDesc: def __get__(self, obj, cls): print("Trying to access from class ".format(obj, cls)) def __set__(self, obj, val): print("Trying to set for ".format(val, obj)) def __delete__(self, obj): print("Trying to delete from ".format(obj)) class DataHolder: data = DataDesc() d = DataHolder() DataHolder.data # Trying to access from None class d.data # Trying to access from class d.data = 1 # Trying to set 1 for del(d.data) # Trying to delete from
Стоит обратить внимание, что вызов DataHolder.data передаёт в метод __get__ None вместо экземпляра класса.
Проверим утверждение о том, что у дата дескрипторов преимущество перед записями в __dict__ экземпляра:
d.__dict__["data"] = "override!" d.__dict__ # d.data # Trying to access from class
Так и есть, запись в __dict__ экземпляра игнорируется, если в __dict__ класса экземпляра (или его базового класса) существует запись с тем же именем и значением — дескриптором данных.
Ещё один важный момент. Если изменить значение атрибута с дескриптором через класс, никаких методов дескриптора вызвано не будет, значение изменится в __dict__ класса как если бы это был обычный атрибут:
DataHolder.__dict__ # . > DataHolder.data = "kick descriptor out" DataHolder.__dict__ # DataHolder.data # "kick descriptor out"
Дескрипторы не данных
Пример дескриптора не данных:
class NonDataDesc: def __get__(self, obj, cls): print("Trying to access from class ".format(obj, cls)) class NonDataHolder: non_data = NonDataDesc() n = NonDataHolder() NonDataHolder.non_data # Trying to access from None class n.non_data # Trying to access from class n.non_data = 1 n.non_data # 1 n.__dict__ #
Его поведение слегка отличается от того, что вытворял дата-дескриптор. При попытке присвоить значение атрибуту non_data, оно записалось в __dict__ экземпляра, скрыв таким образом дескриптор, который хранится в __dict__ класса.
Примеры использования
Дескрипторы это мощный инструмент, позволяющий контролировать доступ к атрибутам экземпляра класса. Один из примеров их использования — функции, при вызове через экземпляр они становятся методами (см. пример выше). Также распространённый способ применения дескрипторов — создание свойства (property). Под свойством я подразумеваю некое значение, характеризующее состояние объекта, доступ к которому управляется с помощью специальных методов (геттеров, сеттеров). Создать свойство просто с помощью дескриптора:
class Descriptor: def __get__(self, obj, type): print("getter used") def __set__(self, obj, val): print("setter used") def __delete__(self, obj): print("deleter used") class MyClass: prop = Descriptor()
Или можно воспользоваться встроенным классом property, он представляет собой дескриптор данных. Код, представленный выше можно переписать следующим образом:
class MyClass: def _getter(self): print("getter used") def _setter(self, val): print("setter used") def _deleter(self): print("deleter used") prop = property(_getter, _setter, _deleter, "doc string")
В обоих случаях мы получим одинаковое поведение:
m = MyClass() m.prop # getter used m.prop = 1 # setter used del(m.prop) # deleter used
Важно знать, что property всегда является дескриптором данных. Если в его конструктор не передать какую либо из функций (геттер, сеттер или делитер), при попытке выполнить над атрибутом соответствующее действие — выкинется AttributeError.
class MySecondClass: prop = property() m2 = MySecondClass() m2.prop # AttributeError: unreadable attribute m2.prop = 1 # AttributeError: can't set attribute del(m2) # AttributeError: can't delete attribute
- staticmethod — то же, что функция вне класса, в неё не передаётся экземпляр в качестве первого аргумента.
- classmethod — то же, что метод класса, только в качестве первого аргумента передаётся класс экземпляра.
class StaticAndClassMethodHolder: def _method(*args): print("_method called with ", args) static = staticmethod(_method) cls = classmethod(_method) s = StaticAndClassMethodHolder() s._method() # _method called with (,) s.static() # _method called with () s.cls() # _method called with (,)
__getattr__(), __setattr__(), __delattr__() и __getattribute__()
Если нужно определить поведение какого-либо объекта как атрибута, следует использовать дескрипторы (например property). Тоже справедливо для семейства объектов (например функций). Ещё один способ повлиять на доступ к атрибутам: методы __getattr__(), __setattr__(), __delattr__() и __getattribute__(). В отличие от дескрипторов их следует определять для объекта, содержащего атрибуты и вызываются они при доступе к любому атрибуту этого объекта.
__getattr__(self, name) будет вызван в случае, если запрашиваемый атрибут не найден обычным механизмом (в __dict__ экземпляра, класса и т.д.):
class SmartyPants: def __getattr__(self, attr): print("Yep, I know", attr) tellme = "It's a secret" smarty = SmartyPants() smarty.name = "Smartinius Smart" smarty.quicksort # Yep, I know quicksort smarty.python # Yep, I know python smarty.tellme # "It's a secret" smarty.name # "Smartinius Smart"
__getattribute__(self, name) будет вызван при попытке получить значение атрибута. Если этот метод переопределён, стандартный механизм поиска значения атрибута не будет задействован. Следует иметь ввиду, что вызов специальных методов (например __len__(), __str__()) через встроенные функции или неявный вызов через синтаксис языка осуществляется в обход __getattribute__().
class Optimist: attr = "class attribute" def __getattribute__(self, name): print(" is great!".format(name)) def __len__(self): print("__len__ is special") return 0 o = Optimist() o.instance_attr = "instance" o.attr # attr is great! o.dark_beer # dark_beer is great! o.instance_attr # instance_attr is great! o.__len__ # __len__ is great! len(o) # __len__ is special\n 0
__setattr__(self, name, value) будет вызван при попытке установить значение атрибута экземпляра. Аналогично __getattribute__(), если этот метод переопределён, стандартный механизм установки значения не будет задействован:
class NoSetters: attr = "class attribute" def __setattr__(self, name, val): print("not setting =".format(name,val)) no_setters = NoSetters() no_setters.a = 1 # not setting a=1 no_setters.attr = 1 # not setting attr=1 no_setters.__dict__ # <> no_setters.attr # "class attribute" no_setters.a # AttributeError
__delattr__(self, name) — аналогичен __setattr__(), но используется при удалении атрибута.
При переопределении __getattribute__(), __setattr__() и __delattr__() следует иметь ввиду, что стандартный способ получения доступа к атрибутам можно вызвать через object:
class GentleGuy: def __getattribute__(self, name): if name.endswith("_please"): return object.__getattribute__(self, name.replace("_please", "")) raise AttributeError("And the magic word!?") gentle = GentleGuy() gentle.coffee = "some coffee" gentle.coffee # AttributeError gentle.coffee_please # "some coffee"
Соль
- Если определён метод a.__class__.__getattribute__(), то вызывается он и возвращается полученное значение.
- Если attrname это специальный (определённый python-ом) атрибут, такой как __class__ или __doc__, возвращается его значение.
- Проверяется a.__class__.__dict__ на наличие записи с attrname. Если она существует и значением является дескриптор данных, возвращается результат вызова метода __get__() дескриптора. Также проверяются все базовые классы.
- Если в a.__dict__ существует запись с именем attrname, возвращается значение этой записи. Если a — это класс, то атрибут ищется и среди его базовых классов и, если там или в __dict__a дескриптор данных — возвращается результат __get__() дескриптора.
- Проверяется a.__class__.__dict__, если в нём существует запись с attrname и это “дескриптор не данных”, возвращается результат __get__() дескриптора, если запись существует и там не дескриптор, возвращается значение записи. Также обыскиваются базовые классы.
- Если существует метод a.__class__.__getattr__(), он вызывается и возвращается его результат. Если такого метода нет — выкидывается AttributeError.
- Если существует метод a.__class__.__setattr__(), он вызывается.
- Проверяется a.__class__.__dict__, если в нём есть запись с attrname и это дескриптор данных — вызывается метод __set__() дескриптора. Также проверяются базовые классы.
- В a.__dict__ добавляется запись value с ключом attrname.
__slots__
Как пишет Guido в своей истории python о том, как изобретались new-style classes:
… Я боялся что изменения в системе классов плохо повлияют на производительность. В частности, чтобы дескрипторы данных работали корректно, все манипуляции атрибутами объекта начинались с проверки __dict__ класса на то, что этот атрибут является дескриптором данных…
На случай, если пользователи разочаруются ухудшением производительности, заботливые разработчики python придумали __slots__.
Наличие __slots__ ограничивает возможные имена атрибутов объекта теми, которые там указаны. Также, так как все имена атрибутов теперь заранее известны, снимает необходимость создавать __dict__ экземпляра.
class Slotter: __slots__ = ["a", "b"] s = Slotter() s.__dict__ # AttributeError s.c = 1 # AttributeError s.a = 1 s.a # 1 s.b = 1 s.b # 1 dir(s) # [ . 'a', 'b' . ]
Оказалось, что опасения Guido не оправдались, но к тому времени, как это стало ясно, было уже слишком поздно. К тому же, использование __slots__ действительно может увеличить производительность, особенно уменьшив количество используемой памяти при создании множества небольших объектов.
Заключение
Доступ к атрибутом в python можно контролировать огромным количеством способов. Каждый из них решает свою задачу, а вместе они подходят практически под любой мыслимый сценарий использования объекта. Эти механизмы — основа гибкости языка, наряду с множественным наследованием, метаклассами и прочими вкусностями. У меня ушло некоторое время на то, чтобы разобраться, понять и, главное, принять это множество вариантов работы атрибутов. На первый взгляд оно показалось слегка избыточным и не особенно логичным, но, учитывая, что в ежедневном программировании это редко пригодиться, приятно иметь в своём арсенале такие мощные инструменты.
Надеюсь, и вам эта статья прояснила парочку моментов, до которых руки не доходили разобраться. И теперь, с огнём в глазах и уверенностью в Точке, вы напишите огромное количество наичистейшего, читаемого и устойчивого к изменениям требований кода! Ну или комментарий.
Спасибо за ваше время.
Ссылки
- Shalabh Chaturvedi. Python Attributes and Methods
- Guido Van Rossum. The Inside Story on New-Style Classes
- Python documentation
Создание класса в Python
Эксперименты будем ставить на коде, который решает очень важную и ответственную задачу — выводит на экран вес фруктов. Делает он это, разумеется, с использованием ООП. Куда же без него во “взрослых” проектах?! И пускай от объявленного класса Fruit нет никакого проку, без него код выглядел бы слишком просто, а теперь в самый раз:
class Fruit(): pass apple = Fruit() apple.fruit_name = 'Яблоко' apple.weight_kg = 0.1 orange = Fruit() orange.fruit_name = 'Апельсин' orange.weight_kg = 0.3 print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000) print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Скопируйте код к себе, запустите его в консоли:
$ python script.py Яблоко | вес в граммах 100.0 Апельсин | вес в граммах 300.0
Как это работает
Разберем что написано в этом коде. Первые две строки объявят новый класс Fruit :
class Fruit(): pass
Команда pass ничего не делает. Это заглушка, она нужна лишь потому, что Python требует внутри объявления класса написать хотя бы какой-то код. Написать здесь нечего, поэтому pass .
Следующая строка кода создаст новый объект — яблоко “apple”:
apple = Fruit()
Класс Fruit выступает шаблоном, который описывает свойства общие сразу для всех фруктов: и для яблок, и для апельсинов. Пока что шаблон пуст, и экземпляр apple ничего полезного от вызова Fruit не получит.
Следующие две строки добавят пару атрибутов к яблоку — название фрукта и его вес:
apple.fruit_name = 'Яблоко' apple.weight_kg = 0.1
Затем аналогичным образом по тому же шаблону — классу Fruit — будет создан еще один фрукт, на этот раз апельсин:
orange = Fruit() orange.fruit_name = 'Апельсин' orange.weight_kg = 0.3
Последние строчки кода пересчитают вес фрукта в граммах и выведут в консоль название и вес:
print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000) print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Обязательные атрибуты
Можно заметить, что оба фрукта в программе имеют свойства с одинаковыми названиями — название fruit_name и вес weight_kg . Доработаем класс Fruit таким образом, чтобы все экземпляры созданные по этому шаблону обязательно имели свойства fruit_name и weight_kg .
Идея следующая. Пусть Python каждый раз после создания нового экземпляра фрукта сразу добавляет все нужные атрибуты. Добавим для этого новую функцию init_attrs :
# . объявление класса Fruit . def init_attrs(fruit, fruit_name='неизвестный фрукт', weight_kg=None): fruit.fruit_name = fruit_name fruit.weight_kg = weight_kg apple = Fruit() init_attrs(apple, 'Яблоко', 0.1) orange = Fruit() init_attrs(orange, 'Апельсин', 0.3) # . вызовы print .
Кода стало больше, но он стал надежнее. Добавляя новые фрукты: груши, вишню или абрикосы — вы точно не забудете указать вес и название. Вызовы функции init_attrs гарантирует, что все фрукты получат одинаковый набор обязательных атрибутов.
В Python коде функции похожие на init_attrs встречаются настолько часто, что для них есть стандартное название и специальный синтаксис. Переименуем функцию init_attrs в __init__ и переместим внутрь класса Fruit .
class Fruit(): def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None): fruit.fruit_name = fruit_name fruit.weight_kg = weight_kg apple = Fruit('Яблоко', 0.1) orange = Fruit('Апельсин', 0.3) print(apple.fruit_name, "| вес в граммах", apple.weight_kg * 1000) print(orange.fruit_name, "| вес в граммах", orange.weight_kg * 1000)
Обратите внимание, что в программе нигде нет явного вызова __init__ . Python сам его вызовет выполняя эти строки кода:
apple = Fruit('Яблоко', 0.1) orange = Fruit('Апельсин', 0.3)
Теперь создавать фрукты по шаблону стало еще проще. Не надо писать вызовы функций, достаточно передать все аргументы для __init__ в класс Fruit .
Часто метод __init__ называют конструктором класса по аналогии с другими языками программирования. Это не совсем верно. Подробнее читайте на StackOverflow.
Добавим метод
Можно заметить что в коде пару раз встречается одинаковое выражение для расчета веса в граммах:
print(..., apple.weight_kg * 1000) print(..., orange.weight_kg * 1000)
Пересчет веса можно вынести в отдельную функцию:
# . объявление класса, __init__ для Fruit def get_weight_gr(fruit): return fruit.weight_kg * 1000 apple = Fruit('Яблоко', 0.1) orange = Fruit('Апельсин', 0.3) print(apple.fruit_name, "| вес в граммах", get_weight_gr(apple)) print(orange.fruit_name, "| вес в граммах", get_weight_gr(orange))
Функция get_weight_gr требует единственный аргумент — объект описывающий фрукт. Ей нет разницы, с каким именно фруктом работать, яблоком или апельсином. Главное, это чтобы фрукт был создан по стандартному шаблону — на основе класса Fruit . Python позволяет спрятать функцию get_weight_gr внутрь класса Fruit , чтобы эту связь между ними было не разорвать.
class Fruit(): def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None): fruit.fruit_name = fruit_name fruit.weight_kg = weight_kg def get_weight_gr(fruit): return fruit.weight_kg * 1000
Функции, объявленные внутри класса называют методами и Python предлагает специальный набор инструментов для работы с ними.
По аналогии с функциями метод можно вызвать так:
Fruit.get_weight_gr(apple)
Однако, не вдаваясь в детали, принято делать так:
apple.get_weight_gr()
Python всегда помнит от какого класса был создан тот или иной объект, и знает что яблоко apple принадлежит к классу Fruit . От класса Fruit яблоко apple получило все его методы, включая get_weight_gr . Доступ к методам объекта получают через точку apple.get_weight_gr , а при вызове указывают первый аргумент функции, потому что его автоматически подставляет Python.
Теперь программа выглядит так:
class Fruit(): def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None): fruit.fruit_name = fruit_name fruit.weight_kg = weight_kg def get_weight_gr(fruit): return fruit.weight_kg * 1000 apple = Fruit('Яблоко', 0.1) orange = Fruit('Апельсин', 0.3) print(apple.fruit_name, "| вес в граммах", apple.get_weight_gr()) print(orange.fruit_name, "| вес в граммах", orange.get_weight_gr())
Кто такой self
В Python есть стандартное название для первого атрибута метода. Вернемся к коду с фруктами и увидим там такое объявление методов:
class Fruit(): def __init__(fruit, fruit_name='неизвестный фрукт', weight_kg=None): ... def get_weight_gr(fruit): ...
В методах принято первый аргумент fruit называть self . Ход исполнения программы от этого не меняется, но другим программистам будет проще разобраться в коде:
class Fruit(): def __init__(self, fruit_name='неизвестный фрукт', weight_kg=None): self.fruit_name = fruit_name self.weight_kg = weight_kg def get_weight_gr(self): return self.weight_kg * 1000 apple = Fruit('Яблоко', 0.1) orange = Fruit('Апельсин', 0.3) print(apple.fruit_name, "| вес в граммах", apple.get_weight_gr()) print(orange.fruit_name, "| вес в граммах", orange.get_weight_gr())
Когда __init__ не нужен
Часто новый класс создается не на пустом месте, а наследуется от другого класса, предоставленного библиотекой. Например, так в Django выглядит добавление на сайт нового типа объектов — статей для блога:
from django.db.models import Model class Article(Model): pass
Сразу после объявления нового класса Article указан класс-предок Model . Из него будут позаимствованы все методы, включая готовый к использованию __init__ . Теперь внутри класса Article будет достаточно описать те методы, что отличают его от стандартного класса Model .
Когда аргументов много
Подобно другим функциям метод может принимать больше одного аргумента. Например:
class Fruit(): ... def get_title(self, upper_case, max_length): title = self.fruit_name if upper_case: title = title.upper() if max_length: title = title[:max_length] return title
Метод get_title принимает три аргумента. Так как это не просто функция, а метод, то первым аргументом обязан быть self . Его значение автоматически подставит Python. Остальные два аргумента upper_case и max_length должны быть вручную указаны при вызове метода:
apple.get_title(True, 20)
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.
Как внутри одного класса обратиться к атрибутам другого класса?
Только начинаю постигать азы ООП, и уперся в следующую проблему: в методе receprtion класса Warehouse необходимо обратиться к атрибуту (например, model) другого класса. Если обращаться через class_name.attribute — пишет, что такого атрибута у класса нет. Корректно работает, только если обращаться напрямую к атрибуту созданного экземпляра класса. Но поскольку объектов будет больеше одного, то данный вариант не подходит. Каким образом возможно реализовать данный функционал?
class Warehouse: def __init__(self, sales, storage, repairs): self.sales = sales self.storage = storage self.repairs = repairs def reception(self): return f' принята на склад' class Equip: def __init__(self, model, quantity, price): self.model = model self.quantity = quantity self.price = price @classmethod def uniq_param(cls): pass class Xerox(Equip): def __init__(self, model, quantity, price): super().__init__(model, quantity, price) def uniq_param(self): return 'Ксерокс делает ксерографические копии' w = Warehouse('Отдел продаж', 'отдел хранения', 'отдел ремонта') x = Xerox(6525, 2, 1000) print(x.model) print(w.reception())
Отслеживать
задан 21 фев 2021 в 15:05
Artyom Lisenkov Artyom Lisenkov
1 2 2 бронзовых знака
Очевидно, функция приёма должна в аргументах ожидать то, что она принимает.
21 фев 2021 в 15:25
Дополнительно: Вы используете classmethod не для того, для чего он нужен. super().__init__(model, quantity, price) — __init__ метод только с этой строкой равносилен отсутствию __init__ метода.
21 фев 2021 в 15:27
Благодарю, заработало))
Классы и объекты. Атрибуты классов и объектов
Меня зовут Сергей Балакирев и на этом занятии мы с вами узнаем, как в Python определять классы, создавать объекты (экземпляры) этих классов, а также добавлять и удалять их атрибуты (то есть, данные).
Предположим, мы хотим определить класс для хранения координат точек на плоскости. Для начала я его запишу без какого-либо содержимого, только имя класса Point и все:
class Point: pass
Здесь оператор pass указывает, что мы в классе ничего не определяем. Также обратите внимание, что в соответствии со стандартом PEP8 имя класса принято записывать с заглавной буквы. И, конечно же, называть так, чтобы оно отражало суть этого класса. В дальнейшем я буду придерживаться этого правила.
Итак, у нас получилось простейшее определение класса с именем Point. Но в таком виде он не особо полезен. Поэтому я пропишу в нем два атрибута: color – цвет точек; circle – радиус точек:
class Point: color = 'red' circle = 2
Обратите внимание, переменные внутри класса обычно называются атрибутами класса или его свойствами. Я буду в дальнейшем использовать эту терминологию. Теперь в нашем классе есть два атрибута color и circle. Но, как правильно воспринимать эту конструкцию? Фактически, сам класс образует пространство имен, в данном случае с именем Point, в котором находятся две переменные color и circle. И мы можем обращаться к ним, используя синтаксис для пространства имен, например:
Point.color = 'black'
или для считывания значения:
Point.circle
(В консольном режиме увидим значение 2). А чтобы увидеть все атрибуты класса можно обратиться к специальной коллекции __dict__:
Point.__dict__
Здесь отображается множество служебных встроенных атрибутов и среди них есть два наших: color и circle.
Теперь сделаем следующий шаг и создадим экземпляры этого класса. В нашем случае для создания объекта класса Point достаточно после его имени прописать круглые скобки:
a = Point()
Смотрите, справа на панели в Python Console у нас появилась переменная a, через которую доступны два атрибута класса: color и circle.
Давайте создадим еще один объект этого класса:
b = Point()
Появилась переменная b, которая ссылается на новый объект (он расположен по другому адресу) и в этом объекте мы также видим два атрибута класса Point. По аналогии можно создавать произвольное количество экземпляров класса.
С помощью функции type мы можем посмотреть тип данных для переменных a или b:
type(a)
Видим, что это класс Point. Эту принадлежность можно проверить, например, так:
type(a) == Point
isinstance(a, Point)
То есть, имя класса здесь выступает в качестве типа данных. Но давайте детальнее разберемся, что же у нас в действительности получилось?
Во-первых, объекты a и b образуют свое пространство имен – пространство имен экземпляров класса и, во-вторых, не содержат никаких собственных атрибутов. Свойства color и circle принадлежат непосредственно классу Point и находятся в нем, а объекты a и b лишь имеют ссылки на эти атрибуты класса. Поэтому я не случайно называю их именно атрибутами класса, подчеркивая этот факт. То есть, атрибуты класса – общие для всех его экземпляров. И мы можем легко в этом убедиться. Давайте изменим значение свойства circle на 1:
Point.circle = 1
И в обоих объектах это свойство стало равно 1. Мало того, если посмотреть коллекцию __dict__ у объектов:
a.__dict__
то она будет пустой, так как в наших экземплярах отсутствуют какие-либо атрибуты. Но, тем не менее, мы можем через них обращаться к атрибутам класса:
a.color b.circle
Но, если мы выполним присваивание, например:
a.color = 'green'
То, смотрите, в объекте a свойство color стало ‘green’, а в b – прежнее. Почему? Дело в том, что мы здесь через переменную a обращаемся к пространству имен уже экземпляра класса и оператор присваивания в Python создает новую переменную, если она отсутствует в текущей локальной области видимости, то есть, создается атрибут color уже непосредственно в объекте a:
Мы можем в этом убедиться, если отобразим коллекцию __dict__ этого объекта:
a.__dict__
То есть, мы с вами создали локальное свойство в объекте a. Этот момент нужно очень хорошо знать и понимать. На этом принципе в Python построено формирование атрибутов классов и локальных атрибутов их экземпляров.
Добавление и удаление атрибутов класса
Кстати, по аналогии, мы можем создавать новые атрибуты и в классе, например, так:
Point.type_pt = 'disc'
Или то же самое можно сделать с помощью специальной функции:
setattr(Point, 'prop', 1)
Она создает новый атрибут в указанном пространстве имен (в данном случае в классе Point) с заданным значением. Если эту функцию применить к уже существующему атрибуту:
setattr(Point, 'type_pt', 'square')
то оно будет изменено на новое значение.
Если же мы хотим прочитать какое-либо значение атрибута, то достаточно обратиться к нему. В консольном режиме это выглядит так:
Point.circle
Но, при обращении к несуществующему атрибуту класса, например:
Point.a
возникнет ошибка. Этого можно избежать, если воспользоваться специальной встроенной функцией:
getattr(Point, 'a', False)
Здесь третий аргумент – возвращаемое значение, если атрибут не будет найден. Эту же функцию можно вызвать и с двумя аргументами:
getattr(Point, 'a')
Но тогда также будет сгенерирована ошибка при отсутствии указанного атрибута. Иначе:
getattr(Point, 'color')
она возвратит его значение. То есть, эта функция дает нам больше гибкости при обращении к атрибутам класса. Хотя на практике ей пользуются только в том случае, если есть опасность обращения к несуществующим атрибутам. Обычно, все же, применяют обычный синтаксис:
Point.color
Наконец, мы можем удалять любые атрибуты из класса. Сделать это можно, по крайней мере, двумя способами. Первый – это воспользоваться оператором del:
del Point.prop
Если повторить эту команду и попытаться удалить несуществующий атрибут, возникнет ошибка. Поэтому перед удалением рекомендуется проверять существование удаляемого свойства. Делается это с помощью функции hasattr:
hasattr(Point, 'prop')
Она возвращает True, если атрибут найден и False – в противном случае.
Также удалить атрибут можно с помощью функции:
delattr(Point, 'type_pt')
Она работает аналогично оператору del.
И, обратите внимание, удаление атрибутов выполняется только в текущем пространстве имен. Например, если попытаться удалить свойство color из объекта b:
del b.color
то получим ошибку, т.к. в объекте b не своих локальных свойств и удалять здесь в общем то нечего. А вот в объекте a есть свое свойство color, которое мы с вами добавляли:
a.__dict__
и его можно удалить:
del a.color
Смотрите, после удаления локального свойства color в объекте a становится доступным атрибут color класса Point с другим значение ‘black’. И это логично, т.к. если свойство не обнаруживается в локальной области, то поиск продолжается в следующей (внешней) области видимости. А это (для объекта a) класс Point. Вот этот момент также следует хорошо понимать при работе с локальными свойствами объектов и атрибутами класса.
Атрибуты экземпляров классов
Теперь, когда мы знаем, как создаются атрибуты, вернемся к нашей задаче формирования объектов точек на плоскости. Мы полагаем, что атрибуты color и circle класса Point – это общие данные для всех объектов этого класса. А вот координаты точек должны принадлежать его экземплярам. Поэтому для объектов a и b мы определим локальные свойства x и y:
a.x = 1 a.y = 2 b.x = 10 b.y = 20
То есть, свойства x, y будут существовать непосредственно в объектах, но не в самом классе Point:
В результате, каждый объект представляет точку с независимыми координатами на плоскости. А цвет и их размер – общие данные для всех объектов.
В заключение этого занятия отмечу, что в любом классе языка Python мы можем прописывать его описание в виде начальной строки, например, так:
class Point: "Класс для представления координат точек на плоскости" color = 'red' circle = 2
В результате, специальная переменная:
Point.__doc__
будет ссылаться на это описание. Обычно, при создании больших программ, в ключевых классах создают такие описания, чтобы в последующем было удобнее возвращаться к ранее написанному коду, корректировать его и использовать, не обращаясь к специальной документации.
Заключение
Итак, из этого занятия вы должны себе хорошо представлять, как определяются классы в Python и создаются объекты класса. Что из себя представляют атрибуты класса и атрибуты объектов, как они связаны между собой. Уметь обращаться к этим атрибутам, добавлять, удалять их, а также проверять существование конкретного свойства в классе или объекте класса.
Видео по теме
Концепция ООП простыми словами
#1. Классы и объекты. Атрибуты классов и объектов
#2. Методы классов. Параметр self
#3. Инициализатор __init__ и финализатор __del__
#4. Магический метод __new__. Пример паттерна Singleton
#5. Методы класса (classmethod) и статические методы (staticmethod)
#6. Режимы доступа public, private, protected. Сеттеры и геттеры
#7. Магические методы __setattr__, __getattribute__, __getattr__ и __delattr__
#8. Паттерн Моносостояние
#9. Свойства property. Декоратор @property
#10. Пример использования объектов property
#11. Дескрипторы (data descriptor и non-data descriptor)
#12. Магический метод __call__. Функторы и классы-декораторы
#13. Магические методы __str__, __repr__, __len__, __abs__
#14 Магические методы __add__, __sub__, __mul__, __truediv__
#15. Методы сравнений __eq__, __ne__, __lt__, __gt__ и другие
#16. Магические методы __eq__ и __hash__
#17. Магический метод __bool__ определения правдивости объектов
#18. Магические методы __getitem__, __setitem__ и __delitem__
#19. Магические методы __iter__ и __next__
#20. Наследование в объектно-ориентированном программировании
#21. Функция issubclass(). Наследование от встроенных типов и от object
#22. Наследование. Функция super() и делегирование
#23. Наследование. Атрибуты private и protected
#24. Полиморфизм и абстрактные методы
#25. Множественное наследование
#26. Коллекция __slots__
#27. Как работает __slots__ с property и при наследовании
#28. Введение в обработку исключений. Блоки try / except
#29. Обработка исключений. Блоки finally и else
#30. Распространение исключений (propagation exceptions)
#31. Инструкция raise и пользовательские исключения
#32. Менеджеры контекстов. Оператор with
#33. Вложенные классы
#34. Метаклассы. Объект type
#35. Пользовательские метаклассы. Параметр metaclass
#36. Метаклассы в API ORM Django
#37. Введение в Python Data Classes (часть 1)
#38. Введение в Python Data Classes (часть 2)
#39. Python Data Classes при наследовании
© 2023 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта