Свойства — Python: Введение в ООП
Что нам делать, если мы захотим иметь возможность получить еще и полное имя (имя+фамилию) объекта?
Мы могли бы в инициализаторе добавить строчку self.full_name = name + ‘ ‘ + surname . Но если впоследствии имя или фамилия поменяются, полное имя устареет, и его нужно будет не забыть поменять.
Также мы могли бы добавить метод с именем вроде get_full_name , который бы возвращал полное имя. Но и такой вариант не идеален, ведь нам хочется работать с полным именем, как с простым атрибутом-переменной.
Оказывается, что в Python есть средство, позволяющее получить атрибут, значение которого вычисляется динамически, то есть во время обращения к атрибуту. Речь идет о свойствах.
Добавим в класс свойство и посмотрим на его использование:
class Person: def __init__(self, name, surname): self.name = name self.surname = surname @property def full_name(self): return self.name + ' ' + self.surname tom = Person('Thomas', 'Smith') tom.full_name # 'Thomas Smith'
full_name выглядит как утка, то есть как атрибут! Но вычисляется динамически. И если мы поменяем name , то full_name также изменится:
tom.name = 'Alice' tom.full_name # 'Alice Smith'
Как вы можете видеть, свойство объявляется как метод без параметров (кроме self , естественно), декорированный с помощью property . Такой метод, возвращающий динамически вычисляемое значение, называется геттером (getter).
Сеттер
Атрибутам пространства имен можно присваивать значения, но что делать, если атрибут — свойство? Если вы попытаетесь присвоить значение свойству, у которого один лишь getter, вы получите ошибку «AttributeError: can’t set attribute».
Чтобы иметь возможность присвоить значение свойству, нужно использовать сеттер (setter). Сеттер — это тоже метод, который принимает новое значение для атрибута и как-то его обрабатывает. Чтобы метод стал сеттером, его тоже нужно соответствующим образом декорировать. Если у вас уже есть геттер, вы можете сделать так:
class Person: def __init__(self, name, surname): self.name = name self.surname = surname @property def full_name(self): return self.name + ' ' + self.surname # сеттер для свойства full_name @full_name.setter def full_name(self, new): self.name, self.surname = new.split(' ')
Теперь свойству full_name можно присваивать новое полное имя. В итоге оно будет разделено по пробелу на две части, первая из которых станет новым именем, а вторая — фамилией. Пример:
tom = Person('Thomas', 'Smith') tom.full_name # 'Thomas Smith' tom.full_name = 'Alice Cooper' tom.name # 'Alice' tom.surname # 'Cooper'
Сеттеры часто используют для того, чтобы проверить корректность нового значения или произвести какие-то его преобразования перед фактическим сохранением в другие атрибуты.
Делитер
Кроме геттеров и сеттеров, в Python существует еще один важный метод для управления атрибутами классов — это делитеры (deleter). Этот метод предоставляет дополнительный уровень контроля над тем, как атрибуты класса удаляются.
Делитер — это особый метод в классе, который вызывается при удалении атрибута. Как правило, удаление атрибута — это нечастая операция, и в большинстве случаев мы можем обойтись без явного определения делитера. Тем не менее, зная о его существовании, мы можем контролировать процесс удаления атрибутов, например, очищать связанные ресурсы или выполнять некоторую логику очистки.
В нашем примере с классом Person , если мы решим реализовать делитер для свойства full_name , то его можно использовать для удаления имени и фамилии. Давайте посмотрим, как это работает:
class Person: def __init__(self, name, surname): self.name = name self.surname = surname @property def full_name(self): return self.name + ' ' + self.surname @full_name.setter def full_name(self, new): self.name, self.surname = new.split(' ') @full_name.deleter def full_name(self): print("Удаляем имя и фамилию!") self.name = None self.surname = None
Теперь при удалении свойства full_name будет вызываться наш делитер, который устанавливает свойствам name и surname значение None , тем самым «удаляя» эти значения. Пример:
tom = Person('Thomas', 'Smith') tom.full_name # 'Thomas Smith' del tom.full_name # Удаляем имя и фамилию tom.name # None tom.surname # None
Использование делитера особенно полезно в случаях, когда необходимо управлять удалением важных или связанных данных, или выполнять освобождение ресурсов (как, например, закрытие файлов или сетевых соединений) в момент уничтожения объекта или его свойств.
Декоратор property
Если посмотреть в документацию к декоратору property , то можно увидеть такую сигнатуру:
property(fget=None, fset=None, fdel=None, doc=None)
Первые три аргумента позволяют задать getter, setter и deleter, а аргумент doc позволяет указать docstring. В такой форме property удобно использовать, когда вы уже имеете готовые функции, которые хотите просто «упаковать» в свойство:
def get_full_name(self): . def set_full_name(self, new): . class Person: . full_name = property( fget=get_full_name, fset=set_full_name, doc='A full name of person' )
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Чем отличаются классы Python от классов C++.
Осваивать новый язык легче, сравнивая его с уже знакомым. Правда, иногда такое сравнение может стать источником заблуждения, когда внешне похожие (одинаковые на первый взгляд) свойства двух разных языков на деле оказываются совершенно различными. Данный текст призван помочь избежать некоторых заблуждений тем, кто знаком с C++ и начал изучать Python.
Очевидные различия
Некоторые отличия сразу бросаются в глаза. Нет, речь не о синтаксисе, очевидно, что по синтаксису Python сильно отличается от C++.
Во первых, определимся с терминологией. Поля и методы в классах Python называются атрибутами. И это не просто разные слова, обозначающие одни и те же понятия. Ниже объясняется, что классы Python устроены совсем по другому и механизм доступа к атрибутам существенно отличается от используемого в C++.
Одно из важнейших отличий заключается в составе аргументов любого метода класса. В качестве первого аргумента указывается экземпляр класса, для которого вызван этот метод. Имя значения не имеет и может быть различным в разных методах, но традиционно используется » self «. Отсюда в частности следует, что любой метод должен иметь хотя бы один параметр. В первом приближении можно считать, что этот параметр является аналогом » this » в C++.
| self | this |
|---|---|
| Произвольный идентификатор | Зарезервированное слово |
| Указывается явно в списке параметров метода | Не указывается в списке параметров |
| Является ссылкой | Является указателем |
Второе отличие напрямую связано с первым. При использовании внутри метода, атрибуты не связываются автоматически с объектом, для которого вызван метод. Необходимо явно указать, к какому объекту относится атрибут, приписав к нему слева » self. » (здесь и далее под » self » мы будем подразумевать первый параметр метода, если не указано другое). Приведём небольшой пример:
Код на C++
class ExampleC < int a; public: void set_a(int sa) void set_abs_a(int sa) < if (sa<0) set_a(-sa); else set_a(sa); >>
Код на Python
class ExamplePy: def set_a(self, sa): self.a = sa def set_abs_a(self, sa): if saВероятно, кто-то скажет, что последняя строка функции set_a в питоновском примере ошибочна — атрибут a не описан в определении класса ExamplePy ! Однако, на самом деле это не ошибка, а проявление принципиального отличия переменных (не только атрибутов объектов) Python от переменных C++. В то время, как в C++ с именем переменной на этапе компиляции связывается область памяти и тип, переменные Python являются элементами специального словаря (вернее, различных словарей), где имя переменной является ключом, который связан со значением переменной. Выполняя оператор присваивания, Python связывает значение выражения с именем переменной. Если с этим именем уже было связано какое-либо значение, связь со старым значением разрывается.
Как это всё работает
А работает это совсем не так, как в C++. И основное отличие состоит в том, что если в C++ описание класса это объявление, то в Python это исполняемый код. Эффектно проиллюстрировать это можно, поместив определение класса в ветвь условного оператора:
if 1: class Example: def f(self): print "aaa" else: class Example: def f(self): print "bbb" e = Example() e.f()Попробуйте запустить эту программу сначала в приведённом здесь виде, а затем, заменив в условии 1 на 0.
Разберёмся, что здесь происходит. В результате выполнения определения класса создаётся объект с именем " Example ". Создание объекта e (экземпляра класса Example ) происходит путём обращения к объекту Example . То есть, класс в Python является объектом! Не удивляйтесь, в Python объектом является практически всё.
В C++ мы имели дело с полями и методами. Поля могли быть обычными (принадлежащими объекту) и статическими (принадлежащими классу). Методы так же были статические и обычные, последние разделялись на невиртуальные и виртуальные. В Python мы имеем дело с атрибутами класса и атрибутами объекта. Рассмотрим пример:
class my_class: "Some class" a = 10 def __init__(self, arg): self.b = arg def f(self): "Some method" print my_class.a, self.a, self.b c1 = my_class(1) c2 = my_class(2) c1.f() # 10 10 1 c2.f() # 10 10 2 c2.a = 20 c1.f() # 10 10 1 c2.f() # 10 20 1 my_class.a = 30 c1.f() # 30 30 1 c2.f() # 30 20 1Здесь мы определили класс my_class с атрибутами a , __init__ и f . На самом деле класс содержит ещё два атрибута: __module__ и __doc__ . Первый содержит название модуля, в котором определён класс ("__main__", если это основная программа), второй содержит строку с описанием модуля (в данном случае "Some class"). Атрибут a можно сравнить со статическим полем класса — он доступен любому объекту класса, но не принадлежит ни одному из них. Попытка присвоить новое значение атрибуту объекта с тем же именем не повлияет на атрибут класса, а приведёт к созданию атрибута объекта, который "заслонит" для этого объекта атрибут класса. Доступ к атрибуту класса можно получить с помощью конструкции имя_класса.имя_атрибута , в данном примере: my_class.a . Не забываем, что класс — это объект!
Атрибуты __init__ и f это методы класса, причём __init__ неявно вызывается при создании экземпляра класса, то есть играет роль конструктора. Все методы в Python виртуальные. В методе __init__ мы присваиваем значение атрибуту b , который, в отличии от a , уже является атрибутом объекта, а не класса. b уникален для каждого объекта и не имеет никакого отношения к классу.
Теперь рассмотрим как работает наш пример. Сначала создаётся класс с двумя методами и атрибутом a , равным 10. Затем создаются два экземпляра класса. При создании им передаётся параметр со значением 1 и 2 соответственно, который присваивается атрибуту b соответствующего класса. Вызов метода f для каждого класса даёт:
10 10 1 10 10 2После выполнения строки c2.a = 20 в экземпляре c2 появляется новый атрибут a , который заслоняет одноимённый атрибут класса. Однако последний по прежнему доступен через с помощью конструкции my_class.a . Теперь вызов метода f даёт:
10 10 1 10 20 2Наконец, мы меняем значение атрибута класса строкой my_class.a = 30 . Это изменение затрагивает всё экземпляры класса и на выходе мы получаем:
30 30 1 30 20 2Методы __init__ и f являются, так же как и a , атрибутами класса. Их отличие лишь в том, что это функции. Метод можно описать и вне класса, тогда в определении класса он не будет отличаться от атрибутов-данных.
def func(self, mult): print self.a*mult, self.b*mult class my_class: a = 5 g = func def __init__(self, b): self.b = b def f(self): print self.a, self.b c = my_class(3) c.f() c.g(10) my_class.h = lambda self,format:format % (self.a, self.b) print c.h('%d---%d') c1 = my_class(8) print c1.h('a=%d, b=%d') c1.h1 = lambda x: x*2 print c1.h1(2)Здесь мы определили функцию от двух аргументов и присвоили ссылку на неё атрибуту класса my_class с именем g . По внешнему виду описание атрибута g ничем не отличается от a , но g это метод, так как func является функцией. Как и следовало ожидать, метод g требует один аргумент — второй аргумент func .
Затем мы делаем и вовсе невообразимую вещь — присваиваем новому атрибуту объекта my_class ссылку на функцию. В результате создаётся ещё один метод, причём доступный как для новых, так и для ранее созданных экземпляров my_class ! Впрочем, сильно удивляться не стоит. В насквозь динамическом Python экземпляр класса содержит ссылку на породивший его класс. Каждый раз при вызове метода интерпретатор ищет в классе (и, если не находит, то ищет в классах, от которых этот класс унаследован) атрибут-функцию с указанным именем. После этого он вызывает её, передавая в качестве первого аргумента ссылку на экземпляр класса, а в качестве остальных — явно указанные при вызове метода аргументы.
Интересно, что если мы присвоим ссылку на функцию атрибуту объекта-экземпляра класса (а не атрибуту класса), то никакого метода мы не получим. Будет просто ещё одна ссылка на функцию. При вызове функции с помощью этой ссылки ей не передаётся никаких неявных аргументов и она ничего не будет знать об объекте, атрибут которого на неё ссылается.
Наследование: можно, но необязательно
- Реализовать в базовом классе свойства, общие для всех классов-потомков, что позволяет избежать копирования кода.
- Получить возможность использовать экземпляры (вернее, указатели на них) производных классов везде, где ожидается указатель на экземпляр базового класса, то есть реализовывать полиморфизм.
Приведём пример с наследованием. Опишем класс, который накапливает объекты в списке и может распечатать содержимое списка. Кроме того, создадим два класса-потомка, которые по разному реализуют вывод элементов списка:
class base: "Базовый класс" def __init__(self): "Инициализируем список" self.lst = [] def add(self, item): "Добавляем элемент" self.lst.append(item) def print_list(self): "Выводим список" for item in self.lst: self.print_item(item) class child1(base): "Производный класс" def __init__(self, first): "Переопределяем __init__" base.__init__(self) # Вызываем конструктор базового класса self.lst.append(first) # Добавляем свой код def print_item(self, item): "Вывод элемента" print item class child2(base): "Производный класс" def print_item(self, item): "Вывод элемента" print "item: %s" % item def print_list(self): "Переопределяем print_list" print "lenght of list = %d" % len(self.lst) # Добавляем свой код base.print_list(self) # Вызываем print_list из базового класса c1 = child1('head') c1.add(1) c1.add([5,'qwerty']) c1.print_list() print '------------' c2 = child2() c2.add(2) c2.add([10,'item']) c2.print_list()Сразу можно увидеть, что в методе print_list класса base вызывается не определённый в этом классе метод print_item . Это не ошибка, наличие этого метода будет необходимо только при выполнении print_list . Однако, если создать экземпляр класса base и вызвать для него print_list , возникнет исключение. Таким образом, класс base — абстрактный, он предназначен только для наследования, но не для создания экземпляров. При этом, в отличии от C++, нет необходимости описывать в классе base пустую функцию print_item . Встретив вызов self.print_item(item) , интерпретатор будет искать print_item в словаре класса, для объекта которого вызван print_list , а не найдя — в словаре(словарях) базового класса(классов). Если поиск завершится успехом, будет вызван найденный метод, иначе будет сгенерировано исключение. Разумеется, найденный метод должен принимать тот набор параметров, который передаётся при его вызове, иначе, опять же, будет сгенерировано исключение. Поскольку поиск производится по имени во время выполнения программы (позднее связывание), то все методы автоматически являются виртуальными.
Класс child1 переопределяет конструктор. Конструктор базового класса можно вызвать из произвольного места собственного конструктора.
Класс child2 переопределяет метод print_list , из которого явным образом вызывается метод базового класса, который, в свою очередь, вызывает метод print_item класса child2 . Обычный полиморфизм в действии.
Множественное наследование
В Python, так же как и в C++, доступно множественное наследование. Рассмотрим пример, где один класс наследуется от двух других, а те, в свою очередь, имеют общего предка. Таким образом, один класс дважды косвенно наследуется другим. В C++ в этом случае дважды наследуемый класс может включаться в состав "внука" в одном или в двух экземплярах, в зависимости от того, наследуется он прямыми потомками как виртуальный или не виртуальный. В Python все классы наследуются как виртуальные и входят в состав потомков в единственном экземпляре.
В нашем примере класс lister (самый старший в иерархии) может добавлять элементы в атрибут-список l (метод add ) и поэлементно передавать весь список не определённому в нём методу out_item (метод out_all ).
Класс filer может выводить данные из списка в файл, который открывается в конструкторе и закрывается в деструкторе.
Класс formater может выдавать элементы списка в указанном формате. Строка формата задаётся в конструкторе.
Наконец класс list_writer может записывать элементы списка в файл в указанном формате.
class lister: """Класс позволяет добавлять элементы в список и выводить все элементы. Для вывода элементов необходимо определить в производном классе метод out_item. """ def __init__(self): print "lister init:", if hasattr(self, 'l'): print "list already created" else: print "create empty list" self.l = [] def add(self, item): self.l.append(item) def out_all(self): for item in self.l: self.out_item(item) class filer(lister): """Класс позволяет выводить данные в файл. Файл открывается при создании экземпляра класса и закрывается при его удалении. """ def __init__(self, filename): print "filer init" lister.__init__(self) self.f = open(filename, 'w') def out_item(self, item): self.f.write(item) def __del__(self): print "close file" self.f.close() class formater(lister): """Класс позволяет получать данные в заданном формате. """ def __init__(self, format): print "formater init" lister.__init__(self) self.format = format def printf(self, item): return self.format % item def out_item(self, item): print self.printf(item) class list_writer(filer, formater): """Класс позволяет добавлять элементы и записывать всё элементы в указанный файл в заданном формате. """ def __init__(self, filename, format): print "list_writer init" filer.__init__(self, filename) formater.__init__(self, format) def out_item(self, item): filer.out_item(self, self.printf(item)) # Создаём экземпляр. Указываем имя файла и формат(будем работать с целыми числами). lw = list_writer("data.txt", "item: %d\n") # Добавляем элементы. lw.add(1) lw.add(2) lw.add(3) # Записываем элементы в файл. lw.out_all() # При завершении программы файл закрывается.В конструкторе класса list_writer вызываются конструкторы классов filer и formater , в каждом из которых вызывается конструктор класса lister . В результате, он вызывается дважды. Чтобы отследить вызов конструктора для уже проинициализированного экземпляра, в код конструктора включена проверка на существование атрибута l (функция hasattr). Если атрибут существует, повторная инициализация не производится.
list_writer init filer init lister init: create empty list formater init lister init: list already created close fileОбходимся без наследования
Допустим, нам нужно, в зависимости от ситуации, выводить строки в файл или сформировать из них одну длинную строку. Создадим два класса:
class to_file: "Вывод в файл" def __init__(self, f): self.f = f def write(self, line): self.f.write(line) class to_line: "Формирование длинной строки" def __init__(self): self.buf = "" def write(self, line): self.buf += line def get(self): return self.buf def writer(dest): dest.write("Первая строка\n") dest.write("Вторая строка\n") f = open("data.txt", 'w') tf = to_file(f) writer(tf) f.close() tl = to_line() writer(tl) print tl.get()Классы to_file и to_line совершенно не связаны друг с другом, ни один из них не наследуется прямо или косвенно от другого и они не имеют общего предка (точнее, ни один из них вообще не имеет предков). Однако, функция writer одинаково успешно работает с экземплярами обоих классов. Для этого достаточно, чтобы каждый класс содержал используемый функцией writer метод write , который в каждом из классов принимает одинаковый набор аргументов.
В приведённом примере метод write встречается не только у экземпляров классов to_file и to_line , но и у объекта f , возвращённого функцией open (ссылка на него записывается в self.f в конструктора класса to_file и используется в методе write ). А нельзя ли обойтись без класса to_file и передать в функцию writer сам объект f ? Никаких проблем:
f = open("data.txt", 'w') writer(f) f.close()Серьёзным (в отличии от "игрушечного", приведённого здесь) примером является стандартный модуль Python StringIO , реализующий файловый интерфейс для строк (программисты на C++ сразу вспомнят строковые потоки). Определённый в модуле класс StringIO не имеет предков, тем не менее, его экземпляры можно использовать везде, где можно использовать файловые объекты (возвращённые функцией open ).
Никакой защиты
Напоследок я, возможно, огорчу C++ программистов, решивших изучать Python. В этом языке отсутствует возможность запретить доступ к атрибутам класса или экземпляра. Никаких модификаторов "private", "protected" и "public" здесь не предусмотрено. Действуют два соглашения:
Соглашение первое, неформальное: если имя атрибута начинается с подчёркивания, этот атрибут не следует использовать напрямую. Выполнение этого соглашения полностью лежит на совести программиста.
Соглашение второе, формальное: если имя атрибута начинается с двух подчёркиваний и не заканчивается двумя подчёркиваниями, этот атрибут не удастся использовать напрямую обычным способом имя_объекта.имя_атрибута . Pyhton не позволяет выполнить такое обращение, однако оставляет возможность обратиться к атрибуту с помощью более сложной конструкции. Для этого нужно добавить слева к имени атрибута имя класса, предварённое символом подчёркивания. Пример:
class counter: def __init__(self): self.__a = 0 def __up(self): self.__a +=1 def get_a(self): self.__up() return self.__a c = counter() print c.get_a() # 1 print c.get_a() # 2 #c.__up() #print c.__a print c._counter__a # 2 c._counter__up() print c._counter__a # 3Если раскомментировать любую из двух закомментированных строк, возникнет исключение AttributeError .
В версии 2.2 появилась возможность контролировать доступ к атрибутам с помощью слотов
Заключение
При написании статьи подразумевалось использование версии 2.1 . Большая часть информации остаётся верной и для новых версий (2.2 и 2.3), однако в современных версиях появились новые возможности, в том числе связанные с классами.
- наследование от встроенных типов
- возможность создавать статические методы и методы класса
- слоты
- свойства (Properties) — стандартный способ подмены чтения, изменения и других действий с атрибутом вызовами соответствующих функций (тем, кто имел дело с C++ Builder, это должно быть знакомо).
Продвинутый Python: 9 важнейших аспектов при определении классов
По своей сути, Python является объектно-ориентированным языком программирования, вследствие чего он работает с данными и функциональностями, поддерживая различные объектно-ориентированные возможности. Например, все структуры данных — это объекты, в том числе и примитивные типы, такие как целые числа и строки, которые в других языках к таковыми не относятся. Функции также являются объектами и представляют из себя просто атрибуты других объектов, в которых они определены (например, класса или модуля).
И хотя вы можете использовать встроенные типы данных и писать множество функций, не создавая пользовательских классов, велика вероятность, что по мере роста проекта вам будет все труднее обслуживать код. Эти отдельные фрагменты не имеют общих тематик, и вам придется изрядно потрудиться, чтобы добиться взаимодействия между ними, несмотря на смежность большей части их информации.
В этих сценариях целесообразно определить собственные классы, что позволит сгруппировать схожую информацию и улучшить структурный дизайн проекта. Более того, в перспективе повысится и эффективность обслуживания базы кода, поскольку он будет уже менее фрагментирован. Однако все это осуществимо с одной оговоркой — необходимо правильно определить пользовательский класс, чтобы достигнутые в результате этого преимущества превосходили затраты ресурсов, обусловленные работой с ними.
В данной статье представлен обзор 9 актуальных и наилучших практик, которые стоит рассмотреть применительно к пользовательским классам.
1. Правильные имена
Определяя собственный класс, вы добавляете новое “дитя” в вашу базу кода, так что следует присвоить ему правильное и понятное имя. Несмотря на то, что в Python единственным ограничением при именовании являются правила допустимых имен переменных (например, они не могут начинаться с цифры), существуют ряд предпочтительных способов назвать класс.
- Используйте легко произносимые существительные. Эта рекомендация особенно актуальна при условии совместной работы над проектом. Вряд ли вам захочется оказаться на месте человека, которому во время презентации придется сказать: “В этом случае мы создаем экземпляр класса Zgnehst”. Кроме того, из правила о легко произносимом имени вытекает еще одно, согласно которому оно не должно быть длинным. Сложно представить случаи, когда бы вам потребовалось больше трех слов для определения имени класса. Одно слово — наилучший вариант, два — приемлемый, а три — предельно допустимое количество.
- Отражайте в имени суть содержащихся данных и предполагаемые функциональности. Все как в реальной жизни — мальчиков нарекают мужскими именами. Когда мы слышим имя Максим, то понимаем, что оно принадлежит мальчику. Этот принцип также применим к именам класса (или в целом любой другой переменной). Правило простое — не вводите людей в замешательство! Если вы работаете с информацией о студентах, то классу следует дать соответствующее имя— Student, а не KiddosAtCampus (Парни из универа), которое не несет должной смысловой нагрузки.
- Соблюдайте соглашения об именах. Для именования классов рекомендуется использовать верблюжий стиль (горбатый регистр), например так: GoodName. Далее приводится неполный список неприемлемых имен класса: goodName, Good_Name, good_name и GOodnAme. Следование общепринятым правилам написания имен позволит прояснить ваши намерения. В итоге при чтении кода ни у кого не возникнет сомнения, что объект с именем GoodName является классом.
Существуют также правила и соглашения об именовании атрибутов и функций. Далее я кратко расскажу о них по мере возможности, но в основном принципы те же самые. Просто запомните главное правило: Не вводите людей в замешательство!
2. Явные атрибуты экземпляров
В большинстве случаев наша цель — определить собственный метод инициализации экземпляра, т. е. __init__ . В нем мы устанавливаем начальное состояние создаваемых экземпляров класса. Однако Python не накладывает ограничений относительно того, где в пользовательском классе мы можем определить атрибуты экземпляра. Иначе говоря, вы можете определить дополнительные атрибуты в операциях, следующих за созданием экземпляра. Рассмотрим возможный сценарий в следующем примере кода.
class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def verify_registration_status(self):
status = self.get_status()
self.status_verified = status == "registered"
def get_guardian_name(self):
self.guardian = "Goodman"
def get_status(self):
# получает статус регистрации из базы данных
status = query_database(self.first_name, self.last_name)
return status
Как показано в примере, можно создать экземпляр класса Student, указав имя и фамилию студента. Далее при вызове метода экземпляра, а именно verify_registration_status , будет установлен атрибут статуса Student. Однако применение такого шаблона нежелательно, поскольку, распространяя различные атрибуты экземпляра по всему классу, вы не даете классу понять, какие данные содержит объект экземпляра. Поэтому будет лучше разместить атрибуты в метод __init__ , чтобы при чтении кода была возможность в одном месте ознакомиться со структурой данных вашего класса. Обратимся к примеру улучшенного метода инициализации.
class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
self.status_verified = None
self.guardian = None
Если какие-то атрибуты экземпляров нельзя установить изначально, то можно это сделать с помощью значений плейсхолдера, например None . Хотя это и не столь важно, но подобное изменение также помогает предотвратить возможную ошибку, когда вы забываете вызвать методы экземпляра для установки требуемых атрибутов, следствием чего является AttributeError ( ‘Student’ object has no attribute ‘status_verified’ ).
В соответствие с правилами именования атрибуты следует называть, используя строчные буквы и с учетом стиля змеиного регистра, согласно которому составные слова при написании соединяются символом нижнего подчеркивания. Более того, все имена должны передавать суть содержащихся в них данных. Например, вместо fn предпочтительно использовать first_name .
3. Используем свойства, но не увлекаемся
Некоторые учатся программированию на Pyhton, уже владея другими объектно-ориентированными языками, например Java, и привыкли создавать геттеры и сеттеры для атрибутов экземпляров. В Python этот шаблон можно воспроизвести с помощью декоратора property, и в следующем примере мы рассмотрим основной способ его использования для реализации геттеров и сеттеров.
class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def name(self):
print("Getter for the name")
return f" "
@name.setter
def name(self, name):
print("Setter for the name")
self.first_name, self.last_name = name.split()
Как только свойство создается, его можно применять в качестве обычных атрибутов, используя точечную нотацию, хотя внутренне оно реализуется с помощью функций.
>>> student = Student("John", "Smith")
. print("Student Name:", student.name)
. student.name = "Johnny Smith"
. print("After setting:", student.name)
.
Getter for the name
Student Name: John Smith
Setter for the name
Getter for the name
After setting: Johnny Smith
Скорее всего, вам известно, что в число преимуществ реализации свойств входят проверка правильной установки значений (например, используется ли строка, а не целое число) и доступ только для чтения (без реализации метода setter). Однако использовать их следует в разумных пределах. Такое чрезмерное количество свойств в пользовательском классе, как в нижеприведенном примере, собьет с толку кого угодно!
class Student:
def __init__(self, first_name, last_name):
self._first_name = first_name
self._last_name = last_name
@property
def first_name(self):
return self._first_name
@property
def last_name(self):
return self._last_name
@property
def name(self):
return f" "
В большинстве случаев эти свойства вполне заменимы атрибутами экземпляров, вследствие чего мы можем получить к ним доступ и установить их напрямую. В Python предпочтительно использовать атрибуты вместо создания свойств за исключением тех ситуаций, когда вы непосредственно заинтересованы в упомянутых выше преимуществах, таких как проверка значений.
4. Определение содержательных строковых представлений
Функции, содержащие двойные символы нижнего подчеркивания до и после имени, относятся к особым или магическим методам Python. Они используются в специальных случаях для основных операций интерпретатора, включая и ранее рассмотренный метод __init__ . Для создания правильных строковых представлений пользовательского класса необходимы два метода, __repr__ и __str__ , благодаря которым читающие ваш код получат более интуитивно понятную информацию о классах.
Главное их отличие в том, что метод __repr__ определяет строку, с помощью которой вы можете пересоздать объект, вызвав eval(repr(“the repr”)) , тогда как строка, определяемая методом __str__ , является более описательной и предоставляет больше возможностей для кастомизации. Иначе говоря, строка, используемая в методе __repr__ , предназначена для просмотра разработчиками, а строка в методе __str__ — для обычных пользователей. Рассмотрим следующий пример реализации строковых представлений.
class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def __repr__(self):
return f"Student(, )"
def __str__(self):
return f"Student: "
Обратите внимание, что при реализации метода __repr__ f-строка использует !r , в результате чего эти строки отображаются в кавычках, поскольку они необходимы для создания экземпляра с правильно отформатированными строками. Без !r -форматирования строка будет выглядеть как Student(John, Smith) , что будет неверным способом создания экземпляра Student. Посмотрим, в каком виде предстают строки в этих реализациях. Если быть более конкретным, то метод __repr__ вызывается при обращении к объекту в интерактивном режиме интерпретатора, а метод __str__ — по умолчанию при выводе объекта.
>>> student = Student("David", "Johnson")
>>> student
Student('David', 'Johnson')
>>> print(student)
Student: David Johnson
5.Статические методы, методы экземпляра и класса
В классе мы можем определить 3 вида методов: статические, методы экземпляра и класса. Вы должны решить, какие методы вам нужны для интересующих вас функциональностей. С этой целью предлагаю несколько общих рекомендаций.
В случаях, когда речь идет об отдельных объектах экземпляра, например при необходимости обновления его конкретных атрибутов или обращения к ним, следует задействовать методы экземпляра. Их сигнатура выглядит следующим образом: def do_something(self): , где аргумент self относится к объекту экземпляра, которые вызывает метод.
В других же случаях следует применять статические методы или методы класса. И тот, и другой можно легко определить с помощью соответствующих декораторов: staticmethod и classmethod . Отличаются эти методы тем, что метод классов позволяет вам обратиться к атрибутам класса или обновить их, а статические методы не зависят от экземпляров или самого класса. Типичный пример метода класса — обеспечение удобного способа создания экземпляра, тогда как статический метод может быть просто вспомогательной функцией. В следующем коде приводятся примеры разных видов методов.
class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def begin_study(self):
print(f" begins studying.")
@classmethod
def from_dict(cls, name_info):
first_name = name_info['first_name']
last_name = name_info['last_name']
return cls(first_name, last_name)
@staticmethod
def show_duties():
return "Study, Play, Sleep"
Подобным же образом можно создать и атрибуты класса. В отличие от ранее рассмотренных атрибутов экземпляра они совместно используются всеми его объектами и должны отражать некоторые характеристики, независимые от отдельных объектов экземпляра.
6. Инкапсуляция при помощи приватных атрибутов
Создавая пользовательские классы для своих проектов, вы должны принимать во внимание инкапсуляцию, особенно если планируете, что их будут использовать и другие. Когда функциональности класса разрастаются, некоторые функции или атрибуты применимы лишь для обработки данных внутри класса. Иначе говоря, эти функции не будут вызываться за пределами класса, и другие его пользователи не проявят ни малейшего интереса к деталям их реализации. В подобных сценариях вам следует прибегнуть к инкапсуляции.
Для ее применения важно соблюдать соглашение, согласно которому перед атрибутами и функциями ставится одно или два нижних подчеркивания. Тонкое отличие состоит в том, что обладающие одиночным подчеркиванием считаются защищенными, а те, у кого двойное подчеркивание — приватными, что подразумевает правку имени после его создания.
По сути, называя атрибуты и функции подобным образом, вы говорите IDE, например PyCharm, что к ним нельзя получить доступ вне класса, хотя в Python настоящих приватных атрибутов не существует, т. е. при желании можно к ним обратиться.
class Student:
def get_mean_gpa(self):
grades = self._get_grades()
gpa_list = Student._converted_gpa_from_grades(grades)
return sum(gpa_list) / len(gpa_list)
def _get_grades(self):
# получает баллы из базы данных
grades = [99, 100, 94, 88]
return grades
@staticmethod
def _converted_gpa_from_grades(grades):
# преобразует баллы в GPA (средний балл)
gpa_list = [4.0, 4.0, 3.7, 3.4]
return gpa_list
Данный фрагмент кода содержит простой пример инкапсуляции. Допустим, мы хотим узнать средний балл студента (GPA), что становится возможным с помощью метода get_mean_gpa . Пользователю не нужно знать, как он вычисляется, так что мы можем защитить сопутствующие методы, поставив нижнее подчеркивание перед именами функций.
Ключевая идея этой превосходной практики заключается в том, что вы раскрываете минимальное число общедоступных API, необходимых пользователю для применения вашего кода. В то же время методы, используемые только внутренне, следует сделать защищенными или приватными.
7. Разделение задач и уменьшение зацепления
По мере разработки проекта вы обнаружите, что имеете дело всё с большим числом данных, при этом ваш класс может стать громоздким, если вы привязаны только к нему одному. Рассмотрим проблему на основе примера класса Student. Предположим, что учащиеся обедают в школе, и у каждого из них есть свой счет для оплаты питания. Теоретически мы работаем с данными, связанными с балансом на счете, и функциональностями внутри класса Student, как показано в следующем примере.
class Student:
def __init__(self, first_name, last_name, student_id):
self.first_name = first_name
self.last_name = last_name
self.student_id = student_id
def check_account_balance(self):
account_number = get_account_number(self.student_id)
balance = get_balance(account_number)
return balance
def load_money(self, amount):
account_number = get_account_number(self.student_id)
balance = get_balance(account_number)
balance += amount
update_balance(account_number, balance)
Данный пример иллюстрирует псевдокод по проверке баланса счета и его пополнения, при этом обе эти операции реализованы в классе Student. А теперь представьте, что число операций со счетом может увеличиться. Например, добавятся приостановка обслуживания потерянной карты, объединение счетов, и реализация всех из них повлечет за собой всё большее разрастание класса Student, так что со временем обслуживать его станет сложнее. Вместо этого вам следует изолировать эти задачи и снять с класса Student ответственность за связанные со счетом функциональности — в этом суть шаблона проектирования decoupling (уменьшения зацепления).
class Student:
def __init__(self, first_name, last_name, student_id):
self.first_name = first_name
self.last_name = last_name
self.student_id = student_id
self.account = Account(self.student_id)
def check_account_balance(self):
return self.account.get_balance()
def load_money(self, amount):
self.account.load_money(amount)
class Account:
def __init__(self, student_id):
self.student_id = student_id
# получает дополнительную информацию из базы данных
self.balance = 400
def get_balance(self):
# Теоретически student.account.balance сработает, но на всякий случай
# нам необходимо добавить шаги для проверки, такие как запрос базы данных,
# и еще раз убедиться, что база актуальна
return self.balance
def load_money(self, amount):
# получает баланс из базы данных
self.balance += amount
self.save_to_database()
Этот код показывает, как можно спроектировать структуры данных с помощью дополнительного класса Account. Как видно, мы переместили в него все связанные со счетом операции. Для извлечения необходимых данных класс Student будет обрабатывать функциональность, обращаясь к информации в классе Account. Если мы захотим реализовать больше функций, относящихся к классу, то сможем просто обновить класс Account.
Основная суть этого шаблона проектирования — предоставить конкретным классам возможность решать отдельные задачи. Благодаря этому они становятся компактнее, тем самым облегчая последующие изменения, поскольку число компонентов кода будет уменьшаться.
8. __slots__ для оптимизации
Если ваш класс преимущественно используется в качестве контейнеров только для хранения данных, вы можете рассмотреть __slots__ для оптимизации его производительности. Это позволит не только увеличить скорость доступа к атрибутам, но и экономно расходовать память, что будет как нельзя кстати, если вам потребуется создавать тысячи или более объектов экземпляра. Причина в том, что атрибуты экземпляра для обычного класса хранятся в управляемом изнутри словаре. А вот при задействовании __slots__ они будут сохранены с помощью массивоподобных структур данных, внутренне реализованных с использованием C, что гарантирует оптимизацию их производительности с гораздо большей эффективностью. Перед вами пример использования __slots__ для определения класса:
class StudentRegular:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
class StudentSlot:
__slots__ = ['first_name', 'last_name']
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
Данный код показывает простой пример реализации __slots__ в классе. Точнее говоря, вы составляете последовательность из всех атрибутов, которая произведет в хранилище данных сопоставление один-к-одному, ускорив доступ и снизив затраты памяти. Как мы недавно отметили, обычные классы для доступа к атрибутам используют словари, что не касается случаев с реализацией __slots__ . Это демонстрирует следующий пример кода, где в классе со __slots__ отсутствует __dict__:
>>> student_r = StudentRegular('John', 'Smith')
>>> student_r.__dict__
>>> student_s = StudentSlot('John', 'Smith')
>>> student_s.__dict__
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'StudentSlot' object has no attribute '__dict__'
Однако применение __slots__ сопровождается одним побочным эффектом — оно препятствует динамическому созданию дополнительных атрибутов. Некоторые предлагают использовать его в качестве механизма контроля наличия атрибутов в классе, но это не отвечает его назначению.
9. Документация
Последний, но не менее значимый раздел мы посвятим документации класса. Важно осознать, что факт ее написания не заменит вам код. Тонны документации не повысят его производительность и не улучшат читаемость. Если вам приходится полагаться на строки документации для пояснения кода, то, скорее всего, у него проблемы. Я уверен, что код должен говорить сам за себя. Следующий пример демонстрирует ошибку, которую могут допустить некоторые программисты — ненужные комментарии, компенсирующие плохой код. В данном случае речь идет о бессмысленных именах переменных.
# количество расчетных часов
a = 6
# почасовая оплата
b = 100
# общая сумма
c = a * b
# Альтернатива приведенной выше версии без комментариев
billable_hours = 6
hourly_rate = 100
total_charge = billable_hours * hourly_rate
Это совсем не значит, что я против написания комментариев и строк документации — все зависит от конкретных случаев. Если ваш код используется несколькими людьми или по нескольким поводам (например, вы один обращаетесь к коду, но по нескольку раз), то стоит написать к нему хорошие комментарии. Они могут помочь вам или вашим коллегам прочитать код, но никто не должен предполагать, что ваш код делает именно то, что сказано в комментариях. Иначе говоря, написание хорошего кода — это всегда приоритетная задача, о которой нельзя забывать.
Если отдельные фрагменты вашего кода предназначены для конечных пользователей, то необходимо написать строки документации, поскольку эти люди не знакомы с используемой базой кода. Все, что они хотят знать — это как применить интересующий их API, и строки документации в данном случае создадут основу для меню справки. Таким образом, на вас, как на программисте, лежит ответственность за предоставление понятных инструкций по использованию ваших программ.
Заключение
В данной статье был проведен обзор важных факторов, которые следует учитывать при определении собственных классов. Если вы только начинаете изучать Python или программирование в целом, то, возможно, вы до конца не поймете все рассмотренные выше аспекты, но это нормально. Чем больше вы программируете, тем больше осознаете, как важно иметь в виду эти принципы при определении классов. Работая с ними, применяйте данные практики постоянно, так как благодаря хорошему проектированию вы будете тратить все меньше и меньше времени на разработку.
- Python 3: 3 функции, которые следует помнить
- Python: 5 ошибок в применении охвата списка
- Пошаговое построение логистической регрессии в Python
В чем отличие в присвоении атрибута класса в конструкторе и нет
В первом случае переменная a является атрибутом класса, и доступна и как W.a и как:
w = W() w.a
Во втором же случае переменная создаётся в момент инициализации объекта, а значит принадлежит объекту и доступна только как:
w = W() w.a
Основная разница заключается в том, как распределяется память. В первом случае память на хранение данной переменной выделяется при объявлении класса, во втором же случае память выделяется для каждого из объектов класса. Используется и то и другое, всё зависит от задачи.
Из фишек - можно например изменять значение сразу для всех объектов класса:
class A(): b = 1 a = A() b = A() a.b # результат 1 b.b # результат 1 # Когда меняем значение для члена класса A.b = 5 # Оно изменяется для всех объектов класса b.b # результат 5 a.b # результат 5
Отслеживать
ответ дан 19 окт 2015 в 7:40
Алексей Стародубцев Алексей Стародубцев
649 1 1 золотой знак 4 4 серебряных знака 15 15 бронзовых знаков
Очень подробно! Все яснопонятно! Спасибо вам!
19 окт 2015 в 17:02
Немного расширю ваш пример:
class T: def __init__(self): self.a = 10 self.b = [1, 2, 3] c = 20 d = [15]
Когда интерпретатор встречает этот код, он выполняет тело класса и все локальные переменные сохраняет как атрибуты этого класса.
Таким образом, мы имеем класс T , у которого есть три атрибута (не считая автоматические): c , d и __init__ (объявление функции тоже создаёт локальную переменную!):
>>> T.__dict__ < '__init__': , 'c': 15, 'd': [20], # . >
Когда мы создаём экземпляр класса, выполняется метод __init__ класса, в который передаётся заготовка создаваемого экземпляра. Таким образом, в строке
t = T()
Мы получаем экземпляр t класса T , который имеет два атрибута: a и b :
>>> t.__dict__
Что происходит дальше, когда мы пытаемся получить доступ к атрибутам?
Например, имеем три строки кода:
print(t.a) # 10 print(t.c) # 20 print(T.c) # 20 # print(T.a) # error: у класса нет такого атрибута
В первой строке интерпретатор посмотрит в атрибуты переменной t , найдёт среди них имя a и вернёт его значение.
Во второй строке происходит следующее. Интерпретатор не может найти атрибут c среди атрибутов объекта t , поэтому обращается к объекту класса, экземпляром которого является этот объект. У класса уже имеется нужный атрибут, поэтому на этом поиск завершается, возвращается значение 20 .
Третья строка выполняется аналогично первой, с условием что атрибут ищется сразу у класса.
Четвёртая строка выполнится с ошибкой, потому что атрибуты экземпляров не распространяются на классы.
Какие за счёт этого возникают эффекты?
Все атрибуты класса доступны всем экземплярам. Мы можем изменять атрибуты или заменять их значения, и это отразится на всех экземплярах класса:
t1 = T() t2 = T() print(t1.d, t2.d) # [15] [15] # изменим атрибут класса, пользуясь тем, что список является изменяемым типом t1.d.append(16) print(t1.d, t2.d) # [15, 16] [15, 16] # создадим новый атрибут уровня класса T.x = "Hello!" print(t1.x, t2.x) # Hello! Hello!
Важно помнить, что атрибуты экземпляра класса скрывают атрибуты класса:
t1.d = [-1, -2] print(t1.d, t2.d, T.d) # [-1, -2] [15, 16] [15, 16]
Т.е. в строке t1.d = [-1, -2] не изменяется значение атрибута класса, а создаётся новый атрибут у объекта t1 . Поэтому изменить значение неизменяемого атрибута класса через экземпляр этого класса не получится. Если требуется подобная операция, стоит изменять атрибут класса.
В итоге имеем следующее: при использовании атрибутов класса стоит проявлять осторожность с изменяемыми объектами (изменение в одном экземпляре затронет все остальные), а также со скрытием атрибутов класса атрибутами экземпляра класса.
С учётом этих замечаний обычно рекомендуется создавать все атрибуты в методе __init__ класса, а не в самом классе.
Иногда в самом классе хранят константные значения, относящиеся ко всем экземплярам, например, значения по умолчанию.