Зачем нужны классы в python
Перейти к содержимому

Зачем нужны классы в python

  • автор:

Перестаньте писать классы

Фото Джэка Дидриха из профиля на G+

Признак того, что объект не должен быть классом — если в нём всего 2 метода, и один из них — инициализация, __init__. Каждый раз видя это, подумайте: «наверное, мне нужна просто одна функция».

Каждый раз когда из написанного класса вы создаёте всего один экземпляр, используете только раз и тут же выбрасываете, следует думать: «ой, надо бы это отрефакторить! Можно сделать проще, намного проще!»

Перевод доклада Джэка Дидриха, одного из ключевых разработчиков языка Питон. Доклад прозвучал 9 марта 2012 на конференции PyCon US.

Все из вас читали Дзэн Питона, наверное много раз. Вот несколько пунктов из него:

  • Простое лучше сложного
  • Плоское лучше вложенного
  • Важна читаемость
  • Если программу трудно объяснить, она плохая
  • Если программу легко объяснить, возможно, она хорошá

Написал этот текст Тим Питерс. Он умнее и вас, и меня. Сколько вы знаете людей, в честь которых назвали алгоритм сортировки? Вот такой человек написал Дзэн Питона. И все пункты гласят: «Не делай сложно. Делай просто.» Именно об этом и пойдёт речь.

Итак, в первую очередь, не делайте сложно, там, где можно сделать проще. Классы очень сложны или могут быть очень сложны. Но мы всё равно делаем сложно, даже стараясь делать проще. Поэтому в этом докладе мы прочитаем немного кода и узнаем, как заметить, что идём неверным путём, и как выбраться обратно.

На своей работе я говорю коллегам: «Я ненавижу код, и хочу чтобы его было как можно меньше в нашем продукте.» Мы продаём функционал, не код. Покупатели у нас не из-за кода, а из-за широкого функционала. Каждый раз, когда код удаляется, это хорошо. Нас четверо, и в последний год мы перестали считать количество строк в продукте, но продолжаем вводить новый функционал.

Классы

Из этого доклада вам в первую очередь нужно запомнить вот этот код. Это крупнейшее злоупотребление классами, встречающееся в природе.

class Greeting(object): def __init__(self, word='Hello'): self.word = word def greet(self, name): return '%s, %s!' % (self.word, name) >>> greeting = Greeting('Hola') >>> greeting.greet('Jorge') Hola, Jorge!

Это не класс, хотя он похож на класс. Имя — существительное, «приветствие». Он принимает аргументы и сохраняет их в __init__. Да, выглядит как класс. У него есть метод, читающий состояние объекта и делающий что-то ещё, как в классах. Внизу написано, как этим классом пользуются: создаём экземпляр Приветствия и затем используем это Приветствие чтобы сделать что-то ещё.

Но это не класс, или он не должен быть классом. Признак этого — в нём всего 2 метода, и один из них — инициализация, __init__. Каждый раз видя это, подумайте: «наверное, мне нужна просто одна функция».

Каждый раз когда из написанного класса вы создаёте всего один экземпляр, используете только раз и тут же выбрасываете, следует думать: «ой, надо бы это отрефакторить! Можно сделать проще, намного проще!»

def greet(name): ob = Greeting('превед') print ob.greet(name) return

Эта функция состоит из 4 строк кода. А вот как можно сделать то же самое всего за 2 строки:

def greet(greeting, name): return '%s, %s!' % (greeting, name) import functools greet = functools.partial(greet, 'превед') greet('красавчик')

Если вы всё время вызываете функцию с тем же первым аргументом, стандартной библиотеке есть инструмент! functools.partial. Вот посмотрите в код выше: добавляете аргумент, и результат можно вызывать многократно.

Не знаю, у скольких из вас диплом в ИТ, у меня он есть. Я учил такие понятия как

— разделение полномочий
— уменьшение связанности кода
— инкапсуляция
— изоляция реализации

С тех пор как я закончил вуз, 15 лет я этих терминов не употреблял. Слыша эти слова, знайте, вас дурят. Эти термины сами по себе не требуются. Если их используют, люди имеют в виду совершенно разное, что только мешает разговору.

Пример: брюки превращаются.

Многие из вас пользуются в повседневной работе сторонними библиотеками. Каждый раз когда надо пользоваться чужим кодом, первое, что нужно сделать — прочитать его. Ведь неизвестно, что там, какого качества, есть ли у них тесты и так далее. Нужно проверить код прежде чем включать его. Иногда читать код бывает тяжко.

Сторонняя библиотека API, назовём её ShaurMail, включала 1 пакет, 22 модуля, 20 классов и 660 строк кода. Мне пришлось всё это прочитать прежде чем включить в продукт. Но это был их официальный API, поэтому мы пользовались им. Каждый раз когда приходили обновления API, приходилось просматривать диффы, потому что было неизвестно, что они поменяли. Вы посылали патчи — а в обновлении они появились?

660 строк кода, 20 классов — это многовато, если программе нужно только дать список адресов электронной почты, текст письма и узнать, какие письма не доставлены, и кто отписался.

Что такое злоупотребление классами? Часто люди думают, что им понадобится что-то в будущем.… Не понадобится. Напишите всё, когда потребуется. В библиотеке ШаурМаил есть модуль ШаурХэш, в котором 2 строки кода:

class ShaurHash(dict): pass

Кто-то решил, что позже понадобится надстройка над словарём. Она не понадобилась, но везде в коде остались строки, как первая:

my_hash = ShaurMail.ShaurHash.ShaurHash(id='cat') d = dict(id='cat') d =

Вторая и третья строки кода — никому не нужно объяснять их. Но там везде повторялась эта мантра «ШаурМаил-ШаурХэш-ШаурХэш». Троекратное повторение слова «Шаур» — ещё один признак излишества. От повторений всем только вред. Вы раздражаете пользователя, заставляя его писать «Шаур» три раза. (Это не настоящее имя компании, а вымышленное.)

Потом они уволили этого парня и наняли того, кто знал, что делает. Вот вторая версия API:

class API: def __init__(self, key): self.header = dict(apikey=key) def call(self, method, params): request = urllib2.Request( self.url + method[0] + '/' + method[1], urllib.urlencode(params), self.header ) try: response = json.loads(urllib2.urlopen(request).read()) return response except urllib2.HTTPError as error: return dict(Error=str(error))

В той было 660 строк, в этой — 15. Всё, что делает этот код — пользуется методами стандартной библиотеки. Он читается целиком, легко, за секунды, и можно сразу понять, что он делает. Кстати, в нём был ещё набор тестов из 20 строк. Вот как надо писать. Когда они обновляли API, я мог прочесть изменения буквально за пару секунд.

Но и здесь можно заметить проблему. В классе два метода, и один из них — __init__. Авторы этого не скрывали. Второй метод — call, «вызвать». Вот как этим API пользоваться:

ShaurMail.API(key='СЕКРЕТНЫЙ КЛЮЧ').call(('mailing', 'statistics'), )

Строка длинная, поэтому мы делаем алиас и вызываем его многократно:

ShaurMail.request = ShaurMail.API(key='СЕКРЕТНЫЙ КЛЮЧ').call ShaurMail.request(('mailing', 'statistics'), )

Заметьте, мы пользуемся этим классом как функцией. Ею он и должен быть. Если видите подобное, знайте, класс тут не нужен. Поэтому я послал им третью версию API:

ShaurMail_API = url = 'https://api.shaurmail.com/%s/%s' ShaurMail_API_KEY = 'СЕКРЕТНЫЙ КЛЮЧ' def request(noun, verb, **params): headers = request = urllib2.Request(ShaurMail_API % (noun, verb), urllib.urlencode(params), headers) return json.loads(urllib2.urlopen(request).read())

Он вообще не создаёт файлов в нашем проекте, потому что я вставил его в тот модуль, где он используется. Он делает всё, что делал 15-строковый API, и всё, что делал 660-строковый API.

Вот с чего мы начали и к чему пришли:

  • 1 пакет + 20 модулей => 1 модуль
  • 20 классов => 1 класс => 0 классов
  • 130 методов => 2 метода => 1 функция
  • 660 строк кода => 15 строк => 5 строк

Легче пользоваться, легче писать, никому не надо выяснять, что происходит.

Стандартная библиотека

Кто пришёл из языка Java, возможно, считает, что пространства имён нужны для таксономии. Это неверно. Они нужны чтобы предотвратить совпадения имён. Если у вас глубокие иерархии пространств, это никому ничего не даёт. ShaurMail.ShaurHash.ShaurHash — всего лишь лишние слова, которые людям надо помнить и писать.

В стандартной библиотеке Питона пространство имён очень неглубокое, потому что вы либо помните, как называется модуль, либо надо смотреть в документации. Ничего хорошего если надо выяснять цепочку, в каком пакете искать, в каком пакете в нём, в каком пакете дальше, и как называется модуль в нём. Нужно просто знать имя модуля.

К нашему стыду, вот пример из нашего же кода, и те же грехи видно и здесь:

services.crawler.crawlerexceptions.ArticleNotFoundException

Пакет, в котором модуль из 2 строк, класс исключения и «pass». Чтобы использовать это исключение, надо дважды написать «crawler», дважды слово «exception». Имя ArticleNotFoundException само себя повторяет. Так не надо. Если вы называете исключения, пусть это будет EmptyBeer, BeerError, BeerNotFound, но BeerNotFoundError — это уже много.

Можно просто пользоваться исключениями из стандартной библиотеки. Они понятны всем. Если только вам не нужно выловить какое-то специфическое состояние, LookupError вполне подойдёт. Если вы получили отлуп по почте, всё равно придётся его читать, поэтому неважно, как называется исключение.

Кроме того, исключения в коде обычно идут после raise и except, и всем сразу понятно, что это исключения. Поэтому не нужно добавлять «Exception» в название.

В стандартной библиотеке Питона есть и ржавые детали, но она — очень хороший пример организации кода:

  • 200 000 строк кода
  • 200 модулей верхнего уровня
  • в среднем по 10 файлов в пакете
  • 165 исключений

10 файлов в пакете — это много, но только из-за некоторых сторонних проектов, добавленных в библиотеку, где были пакеты из всего 2 файлов. Если вам вздумается создать новое исключение, подумайте лучше, ведь в стандартной библиотеке обошлись 1 исключением на 1200 строк кода.

Я не против классов в принципе. Классы бывают нужны — когда много меняющихся данных и связанных с ними функций. Однако в каждодневной работе такое бывает нечасто. Регулярно приходится работать со стандартной библиотекой, а там уже есть подходящие классы. За вас их уже написали.

Единственное исключение в библиотеке Питона — модуль heapq. Heap queue, «очередь в куче» — это массив, который всегда отсортирован. В модуле heapq десяток методов, и они все работают с той же «кучей». Первый аргумент всегда остаётся тем же, что значит, здесь действительно напрашивается класс.

heapify(data) pushleft(data, item) popleft(data) pushright(data, item) popright(data)

Каждый раз, когда нужно пользоваться heapq, я беру реализацию этого класса из своего инструментария.

class Heap(object): def __init__(self, data=None, key=lambda x: None): self.heap = data or [] heapq.heapify(self.heap) self.key = key def pushleft(self, item): if self.key: item = (self.key(item), item) heapq.pushleft(self.heap, item) def popleft(self): return heapq.popleft(self.heap)[1]
Классы разрастаются как сорняки

Состояние OAuth в Питоне — неважное. Опять же, есть сторонние библиотеки, и прежде чем использовать в своём проекте, их нужно прочесть.

Я пытался использовать сокращатель урлов от Гугла: мне нужно было взять урлы и просто сократить их. У Гугла есть проект, в котором 10 000 строк кода. 115 модулей и 207 классов. Я написал отповедь об этом в Гугле+, но мало кто её видел, а Гвидо (Ван Россум — прим. пер.) прокомментировал: «Я снимаю с себя ответственность за гугловский код API.» 10 000 строк кода — там же обязательно найдётся какая-нибудь дрянь вроде ШаурМэйла. Вот, например, класс Flow («поток»), от которого наследуют другие.

class Flow(object): pass class Storage(object): def put(self, data): _abstract() def get(self): _abstract() def _abstract(): raise NotImplementedError

Он пустой. Но у него есть свой модуль, и каждый раз читая наследующий его класс, надо сходить, проверить тот файл и снова убедиться, что тот класс пуст. Кто-то глядел в будущее и решил: «Напишу-ка я 3 строчки кода сейчас, чтобы в будущем эти 3 строчки не менять.» И отнял время у всех, кто читает его библиотеку. Есть ещё класс Хранилище, (Storage) который почти ничего не делает. В нём правильно обрабатываются ошибки, используя стандартные исключения, но им делают алиасы, и опять же нужно ходить читать их код, чтобы выяснить, как это работает.

Чтобы внедрить OAuth2 мне понадобилась неделя. Пару дней заняло чтение десяти тысяч строк кода, после чего я стал искать другие библиотеки. Нашёл python-oauth2. Это вторая версия python-oauth, но она на самом деле не умеет работать с OAuth2, что не сразу удалось выяснить. Впрочем, эта библиотека немного лучше гугловской: только 540 строк и 15 классов.

Я переписал её ещё проще и назвал python-foauth2. 135 строк кода и 3 класса, и то всё равно много, я не достаточно её отрефакторил. Вот один из этих трёх классов:

class Error(Exception): pass
Жизнь

Последний пример. Все вы видели игру «Жизнь» Конвэя, даже если не знаете её имени. Есть клетчатое поле, каждый ход вы считаете для каждой клетки соседние, и в зависимости от них она будет либо живой, либо мёртвой. И получаются такие красивые устойчивые узоры, как планер: клетки впереди оживают, а сзади умирают, и планер как будто летит по полю.

Игра «жизнь» очень проста: поле и пара правил. Мы задаём эту задачу на собеседовании, потому что если вы не умеете такого — нам не о чем разговаривать. Многие сразу же говорят «Клетка — существительное. Класс надо.» Какие свойства в этом классе? Место, живая или нет, состояние в следующий ход, всё? Ещё есть соседи. Потом начинают описывать поле. Поле — это множество клеток, поэтому это сетка, у неё метод «подсчитать», который обсчитывает клетки внутри.

class Cell(object): def __init__(self, x, y, alive=True): self.x = x self.y = y self.alive = alive self.next = None def neigbors(self): for i, j in itertools.product(range(-1, 2), repeat=2): if (i, j) != (0, 0): yield (self.x + i, self.y + j) class Board(object): def __init__(self): self.cells = <> # < (x, y): Cell() >def advance(self): for (x, y), cell in self.cells.items(): alive_neighbors = len(cell.neighbors) cell.next = (alive_neighbors == 3 or (alive_neighbors == 2 and cell.alive))

На этом месте надо сказать «стоп»: у нас есть класс Поле, в котором 2 метода: __init__ и «сделать ход». В нём одно свойство — словарь, значит со словарём и надо работать. Заметьте, что не надо хранить соседей точки, они уже и так есть в словаре. Живая точка или нет — это просто булево значение, поэтому будем хранить координаты только живых клеток. А раз в словаре хранятся только True, нужен не словарь а просто множество (set) координат. Наконец, новое состояние не нужно, можно просто заново создать список живых клеток.

def neigbors(point): x, y = point for i, j in itertools.product(range(-1, 2), repeat=2): if any((i, j)): yield (x + i, y + j) def advance(board): newstate = set() recalc = board | set(itertools.chain(*map(neighbors, board))) for point in recalc: count = sum((neigh in board) for neigh in neighbors(point)) if count == 3 or (count == 2 and point in board): newstate.add(point) return newstate glider = set([(0, 0), (1, 0), (2, 0), (0, 1), (1, 2)]) for i in range(1000): glider = advance(glider) print glider

Получается очень простая, сжатая реализация игры. Двух классов тут не надо. Внизу — координаты планера, их вставляют в поле, и планер летит. Всё. Это полная реализация игры «жизнь».

Резюме

1. Если вы видите класс с двумя методами, включая __init__, это не класс.
2. Не создавайте новых исключений, если они не нужны (а они не нужны).
3. Упрощайте жёстче.

От переводчика: в комментариях я вижу, что многие восприняли доклад как полное отрицание ООП. Это ошибка. Пункт 1 из итогов чётко говорит, что такое не класс. Классы нужны, но суть доклада — в том, что не нужно ими злоупотреблять.

  • Python
  • Проектирование и рефакторинг

Объектно-ориентированное программирование. Классы и объекты

Python 3 логотип

Сегодня мы поговорим об объектно-ориентированном программировании и о его применении в python.

Объектно-ориентированное программирование (ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов.

Класс — тип, описывающий устройство объектов. Объект — это экземпляр класса. Класс можно сравнить с чертежом, по которому создаются объекты.

Python соответствует принципам объектно-ориентированного программирования. В python всё является объектами — и строки, и списки, и словари, и всё остальное.

Но возможности ООП в python этим не ограничены. Программист может написать свой тип данных (класс), определить в нём свои методы.

Это не является обязательным — мы можем пользоваться только встроенными объектами. Однако ООП полезно при долгосрочной разработке программы несколькими людьми, так как упрощает понимание кода.

Приступим теперь собственно к написанию своих классов на python. Попробуем определить собственный класс:

  Теперь мы можем создать несколько экземпляров этого класса:
       File 

Классу возможно задать собственные методы:

И напоследок еще один пример:

Для вставки кода на Python в комментарий заключайте его в теги

  • Модуль csv - чтение и запись CSV файлов
  • Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
  • Онлайн-обучение Python: сравнение популярных программ
  • Книги о Python
  • GUI (графический интерфейс пользователя)
  • Курсы Python
  • Модули
  • Новости мира Python
  • NumPy
  • Обработка данных
  • Основы программирования
  • Примеры программ
  • Типы данных в Python
  • Видео
  • Python для Web
  • Работа для Python-программистов
  • Сделай свой вклад в развитие сайта!
  • Самоучитель Python
  • Карта сайта
  • Отзывы на книги по Python
  • Реклама на сайте

Объектно-ориентированное программирование

Python имеет множество встроенных типов, например, int, str и так далее, которые мы можем использовать в программе. Но также Python позволяет определять собственные типы с помощью классов . Класс представляет некоторую сущность. Конкретным воплощением класса является объект.

Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики Человек может выполнять некоторые действия - ходить, бегать, думать и т.д. То есть это представление, которое включает набор характеристик и действий, можно назвать классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие - другое имя. И реально существующий человек будет представлять объект этого класса.

Класс определяется с помощью ключевого слова class :

class название_класса: атрибуты_класса методы_класса

Внутри класса определяются его атрибуты, которые хранят различные характеристики класса, и методы - функции класса.

Создадим простейший класс:

class Person: pass

В данном случае определен класс Person, который условно представляет человека. В данном случае в классе не определяется никаких методов или атрибутов. Однако поскольку в нем должно быть что-то определено, то в качестве заменителя функционала класса применяется оператор pass . Этот оператор применяется, когда синтаксически необходимо определить некоторый код, однако мы не хотим его, и вместо конкретного кода вставляем оператор pass.

После создания класса можно определить объекты этого класса. Например:

class Person: pass tom = Person() # определение объекта tom bob = Person() # определение объекта bob

После определения класса Person создаются два объекта класса Person - tom и bob. Для создания объекта применяется специальная функция - конструктор , которая называется по имени класса и которая возвращает объект класса. То есть в данном случае вызов Person() представляет вызов конструктора. Каждый класс по умолчанию имеет конструктор без параметров:

tom = Person() # Person() - вызов конструктора, который возвращает объект класса Person

Методы классов

Методы класса фактически представляют функции, которые определенны внутри класса и которые определяют его поведение. Например, определим класс Person с одним методом:

class Person: # определение класса Person def say_hello(self): print("Hello") tom = Person() tom.say_hello() # Hello

Здесь определен метод say_hello() , который условно выполняет приветствие - выводит строку на консоль. При определении методов любого класса следует учитывать, что все они должны принимать в качестве первого параметра ссылку на текущий объект, который согласно условностям называется self . Через эту ссылку внутри класса мы можем обратиться к функциональности текущего объекта. Но при самом вызове метода этот параметр не учитывается.

Используя имя объекта, мы можем обратиться к его методам. Для обращения к методам применяется нотация точки - после имени объекта ставится точка и после нее идет вызов метода:

объект.метод([параметры метода])

Например, обращение к методу say_hello() для вывода приветствия на консоль:

tom.say_hello() # Hello

В итоге данная программа выведет на консоль строку "Hello".

Если метод должен принимать другие параметры, то они определяются после параметра self , и при вызове подобного метода для них необходимо передать значения:

class Person: # определение класса Person def say(self, message): # метод print(message) tom = Person() tom.say("Hello METANIT.COM") # Hello METANIT.COM

Здесь определен метод say() . Он принимает два параметра: self и message. И для второго параметра - message при вызове метода необходимо передать значение.

self

Через ключевое слово self можно обращаться внутри класса к функциональности текущего объекта:

self.атрибут # обращение к атрибуту self.метод # обращение к методу

Например, определим два метода в классе Person:

class Person: def say(self, message): print(message) def say_hello(self): self.say("Hello work") # обращаемся к выше определенному методу say tom = Person() tom.say_hello() # Hello work

Здесь в одном методе - say_hello() вызывается другой метод - say() :

self.say("Hello work")

Поскольку метод say() принимает кроме self еще параметры (параметр message), то при вызове метода для этого параметра передается значение.

Причем при вызове метода объекта нам обязательно необходимо использовать слово self , если мы его не используем:

def say_hello(self): say("Hello work") # ! Ошибка

То мы столкнемся с ошибкой

Конструкторы

Для создания объекта класса используется конструктор. Так, выше когда мы создавали объекты класса Person, мы использовали конструктор по умолчанию, который не принимает параметров и который неявно имеют все классы:

tom = Person()

Однако мы можем явным образом определить в классах конструктор с помощью специального метода, который называется __init__() (по два прочерка с каждой стороны). К примеру, изменим класс Person, добавив в него конструктор:

class Person: # конструктор def __init__(self): print("Создание объекта Person") def say_hello(self): print("Hello") tom = Person() # Создание объекта Person tom.say_hello() # Hello

Итак, здесь в коде класса Person определен конструктор и метод say_hello() . В качестве первого параметра конструктор, как и методы, также принимает ссылку на текущий объект - self. Обычно конструкторы применяются для определения действий, которые должны производиться при создании объекта.

Теперь при создании объекта:

tom = Person()

выполняется вызов конструктора __init__() из класса Person, который выведет на консоль строку "Создание объекта Person".

Атрибуты объекта

Атрибуты хранят состояние объекта. Для определения и установки атрибутов внутри класса можно применять слово self . Например, определим следующий класс Person:

class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека tom = Person("Tom") # обращение к атрибутам # получение значений print(tom.name) # Tom print(tom.age) # 1 # изменение значения tom.age = 37 print(tom.age) # 37

Теперь конструктор класса Person принимает еще один параметр - name. Через этот параметр в конструктор будет передаваться имя создаваемого человека.

Внутри конструктора устанавливаются два атрибута - name и age (условно имя и возраст человека):

def __init__(self, name): self.name = name self.age = 1

Атрибуту self.name присваивается значение переменной name. Атрибут age получает значение 1.

Если мы определили в классе конструктор __init__, мы уже не сможем вызвать конструктор по умолчанию. Теперь нам надо вызывать наш явным образом опреледеленный конструктор __init__, в который необходимо передать значение для параметра name:

tom = Person("Tom")

Далее по имени объекта мы можем обращаться к атрибутам объекта - получать и изменять их значения:

print(tom.name) # получение значения атрибута name tom.age = 37 # изменение значения атрибута age

В принципе нам необязательно определять атрибуты внутри класса - Python позволяет сделать это динамически вне кода:

class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека tom = Person("Tom") tom.company = "Microsoft" print(tom.company) # Microsoft

Здесь динамически устанавливается атрибут company, который хранит место работы человека. И после установки мы также можем получить его значение. В то же время подобное определение чревато ошибками. Например, если мы попытаемся обратиться к атрибуту до его определения, то программа сгенерирует ошибку:

tom = Person("Tom") print(tom.company) # ! Ошибка - AttributeError: Person object has no attribute company

Для обращения к атрибутам объекта внутри класса в его методах также применяется слово self:

class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека def display_info(self): print(f"Name: Age: ") tom = Person("Tom") tom.display_info() # Name: Tom Age: 1

Здесь определяется метод display_info(), который выводит информацию на консоль. И для обращения в методе к атрибутам объекта применяется слово self: self.name и self.age

Создание объектов

Выше создавался один объект. Но подобным образом можно создавать и другие объекты класса:

class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека def display_info(self): print(f"Name: Age: ") tom = Person("Tom") tom.age = 37 tom.display_info() # Name: Tom Age: 37 bob = Person("Bob") bob.age = 41 bob.display_info() # Name: Bob Age: 41

Здесь создаются два объекта класса Person: tom и bob. Они соответствуют определению класса Person, имеют одинаковый набор атрибутов и методов, однако их состояние будет отличаться.

При выполнении программы Python динамически будет определять self - он представляет объект, у которого вызывается метод. Например, в строке:

tom.display_info() # Name: Tom Age: 37

Это будет объект tom

bob.display_info()

Это будет объект bob

В итоге мы получим следующий консольный вывод:

Name: Tom Age: 37 Name: Bob Age: 41

Что такое метод класса в Python и зачем нужен

Альтернативный конструктор (несколько конструкторов) класса в Python

Сразу начнем с примера простого класса, который содержит обычный метод и метод класса:

class MyClass: def method(self): return 'instance method called', self @classmethod def classmethod(cls): return 'class method called', cls 

Как работают методы класса в Python?

В Python, методы класса отмечаются декоратором @classmethod , следовательно в приведенном выше примере, метод класса будет определен в функции classmethod() .

Как можно заметить, метод класса вместо того, чтобы принимать аргумент self , принимает аргумент cls . При вызове метода этот аргумент указывает на сам класс, а не на экземпляр класса.

Поскольку метод класса имеет доступ только к аргументу cls , он не может изменять состояние экземпляра объекта. Для этого потребуется доступ к аргументу self . НО все же методы класса могут изменять состояние класса, которое применяется ко всем экземплярам класса.

MyClass() настроен таким образом, что реализация каждого метода возвращает кортеж для отслеживания, что происходит, и к каким частям класса или объекта метод может получить доступ.

Вот что происходит, когда мы вызываем метод экземпляра:

>>> obj = MyClass() >>> obj.method() # ('instance method called', ) # Можно передать объект `obj` экземпляра вручную, # для получения того же результата >>> MyClass.method(obj) # ('instance method called', ) 

Этот кусок кода подтвердил, что метод экземпляра класса имеет доступ к экземпляру объекта, напечатанному как через аргумент self .

Кстати, методы экземпляра также могут получить доступ к самому классу через атрибут self.__class__ . Это делает методы экземпляра мощными с точки зрения ограничений доступа - они могут изменять состояние как экземпляра объекта, так и самого класса.

Попробуем вызвать метод класса:

>>> obj.classmethod() # ('class method called', ) 

Вызов метода класса obj.classmethod() показал, что он не имеет доступа к объекту , а только к объекту , представляющему сам класс (в Python все является объектом, даже сами классы).

Обратите внимание, как Python автоматически передает класс в качестве первого аргумента функции, когда вызывается MyClass.classmethod() . Вызов метода в Python через точечную нотацию запускает это поведение. Параметр self в методах экземпляра работает точно так же.

Теперь посмотрим, что происходит, когда попытаться вызвать эти методы в самом классе - без предварительного создания экземпляра объекта:

>>> MyClass.classmethod() # ('class method called', ) >>> MyClass.method() # Traceback (most recent call last): # File "", line 1, in # TypeError: method() missing 1 required positional argument: 'self' 

Из примера видно, что можно нормально вызвать метод класса MyClass.classmethod() , но попытка вызвать метод экземпляра MyClass.method() завершилась ошибкой TypeError . Этого следовало ожидать, ведь экземпляр объекта не создан, что означает, что Python не может заполнить аргумент self и следовательно, вызов не выполняется.

Для чего нужны методы класса в Python?

Следующие примеры кода должны сделать понимание метода класса более ясным. Далее рассмотрим пример класса, имеющего дело с информацией о дате (это будет шаблон):

class Date: def __init__(self, day=0, month=0, year=0): self.day = day self.month = month self.year = year def string_to_db(self): return f'self.year>-self.month>-self.day>' 

Этот класс, очевидно, можно использовать для хранения информации об определенных датах, без информации о часовом поясе (предположим, что все даты представлены в формате UTC).

Здесь есть конструктор __init__ , типичный инициализатор экземпляров классов Python, который получает аргументы как типичный метод экземпляра, имея первый необязательный аргумент self , который содержит ссылку на вновь созданный экземпляр.

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

Предположим, что мы хотим создать много экземпляров класса Date() с информацией о дате, поступающей из внешнего источника, в виде строки с форматом dd.mm.yyyy . Предположим, нужно сделать это в разных местах исходного кода проекта.

Итак, что для этого необходимо сделать:

  • Выполнить синтаксический анализ строки, получить день, месяц и год как три целочисленные переменные или кортеж из трех элементов, включающий эти переменные.
  • Создать экземпляр Date , передав эти значения в конструктор класса при его создании.

Это будет выглядеть так:

>>> string_date = '10.10.2020' >>> day, month, year = map(int, string_date.split('.')) >>> date = Date(day, month, year) >>> date.string_to_db() # '2020-10-10' 

Для выполнения этой задачи, например C++ или Java может реализовать такую ​​возможность с перегрузкой метода __init__ , но в Python перегрузка методов отсутствует. Вместо нее необходимо использовать метод класса (декоратор @classmethod).

Создадим еще один "конструктор".

class Date(object): def __init__(self, day=0, month=0, year=0): self.day = day self.month = month self.year = year @classmethod def from_string(cls, date_as_string): day, month, year = map(int, date_as_string.split('.')) date1 = cls(day, month, year) return date1 def string_to_db(self): return f'self.year>-self.month>-self.day>' 

Более подробно о перегрузке смотрите в материале "Перегрузка методов в Python".

Обратите внимание, как используется аргумент cls в методе класса from_string() вместо прямого вызова конструктора класса Date() .

Теперь создать новый экземпляр Date , так же, можно следующим образом:

>>> date1 = Date.from_string('30.12.2020') >>> date1.string_to_db() # '2020-12-30' >>> date2 = Date.from_string('01.01.2021') >>> date2.string_to_db() # '2021-1-1' 

Рассмотрим приведенную выше реализацию чтобы понять, какие преимущества здесь есть:

  • Реализован синтаксический анализ строки даты в одном месте, и теперь его можно использовать повторно.
  • Инкапсуляция здесь отлично работает. Если вы думаете, что можете реализовать синтаксический анализ строк как единственную функцию в другом месте, это решение намного лучше соответствует парадигме ООП.
  • cls - это объект, который содержит сам класс, а не его экземпляр. Это довольно круто, потому что, если мы наследуем класс Date , для всех дочерних элементов также будет определен метод класса from_string() .

Используя методы класса, можно добавить столько альтернативных конструкторов, сколько необходимо. Такое поведение может сделать интерфейс создаваемых классов самодокументированным (до определенной степени конечно) и упростить их использование.

  • ОБЗОРНАЯ СТРАНИЦА РАЗДЕЛА
  • Пространство имен и область видимости в классах
  • Определение классов
  • Объект класса и конструктор класса
  • Создание экземпляра класса
  • Метод экземпляра класса
  • Что такое метод класса и зачем нужен
  • Что такое статический метод в классах Python и зачем нужен
  • Атрибуты класса и переменные экземпляра класса
  • Кэширование методов экземпляра декоратором lru_cache
  • Закрытые/приватные методы и переменные класса Python
  • Наследование классов
  • Множественное наследование классов
  • Абстрактные классы
  • Перегрузка методов в классе Python
  • Что такое миксины и как их использовать
  • Класс Python как структура данных, подобная языку C
  • Создание пользовательских типов данных
  • Специальные (магические) методы класса Python
  • Базовая настройка классов Python магическими методами
  • Настройка доступа к атрибутам класса Python
  • Дескриптор класса для чайников
  • Протокол дескриптора класса
  • Практический пример дескриптора
  • Использование метода .__new__() в классах Python
  • Специальный атрибут __slots__ класса Python
  • Специальный метод __init_subclass__ класса Python
  • Определение метаклассов metaclass
  • Эмуляция контейнерных типов в классах Python
  • Другие специальные методы класса
  • Как Python ищет специальные методы в классах
  • Шаблон проектирования Фабрика и его реализация

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *