Python objects and classes

Введение

Большинству python-разработчикам приходится регулярно писать такие классы:

Уже на этом примере видна избыточность. Идентификаторы title и author используются несколько раз. Реальный класс же будет ещё содержать переопределенные методы и .

Модуль содержит декоратор . С его использованием аналогичный код будет выглядеть так:

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

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

Что же вы получаете в результате? Вы автоматически получаете класс, с реализованными методами , , и . Кроме того, это будет обычный класс и вы можете наследоваться от него или добавлять произвольные методы.

Вызов дескрипторов класса.

Дескриптор может быть вызван непосредственно по имени метода. Например .

Кроме того, чаще всего дескриптор вызывается автоматически при доступе к атрибутам. Например, в ищет в словаре объекта . Если определяет метод , то вызывается в соответствии с правилами приоритета, перечисленными ниже.

Детали вызова зависят от того, является ли объектом или классом.

Для объектов механизм вызова находится в , который преобразует в . Реализация работает через цепочку приоритетов, которая дает дескрипторам данных приоритет над переменными экземпляра, переменным экземпляра приоритет над дескрипторами без данных и назначает самый низкий приоритет , если это предусмотрено.

Для классов механизм вызова находится в , который преобразует в . В чистом Python это выглядит так:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

Важно помнить следующее:

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

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

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

Устойчивость объектов[править]

Объекты всегда имеют своё представление в памяти компьютера и их время жизни не больше времени работы программы. Однако зачастую необходимо сохранять данные между запусками приложения и/или
передавать их на другие компьютеры.
Одним из решений этой проблемы является устойчивость объектов (англ. object persistence) которая достигается с помощью хранения представлений объектов (сериализацией) в виде байтовых последовательностей и их последующего восстановления (десериализация).

Модуль является наиболее простым способом «консервирования» объектов в Python.

Следующий пример показывает, как работает сериализация и десериализация:

# сериализация
>>> import pickle
>>> p = set()
>>> pickle.dumps(p)
'c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.'

# де-сериализация
>>> import pickle
>>> p = pickle.loads('c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.')
>>> print p
set()

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

В основе сериализации объекта стоит представление его состояния. По умолчанию состояние объекта — это все, что записано в его полях. Пользовательские классы могут управлять сериализацией, предоставляя состояние объекта явным образом (методы , и др.).

На стандартном для Python механизме сериализации построена работа модуля (shelve (англ. глаг.) — ставить на полку; сдавать в архив). Модуль предоставляет функцию
. Объект, который она возвращает, работает аналогично словарю, но объекты сериализуются и сохраняются в файле:

>>> import shelve
>>> s = shelve.open("myshelve.bin")
>>> s'abc' = 1, 2, 3
>>> s.close()
# .....
>>> s = shelve.open("myshelve.bin")
>>> s'abc'
1, 2, 3

Сериализация — не единственная возможная, и подходит не всегда. Для сериализации, не зависящей от языка программирования, можно использовать, например, XML.

Создание логируемого декоратора

Возможно, вам потребуется логировать того, что делает ваша функция. Большую часть времени логинг будет встроен внутри вашей функции. Однако, бывают случаи, когда вам нужно сделать это на уровне функции, что бы получить представление о потоке программы или, возможно, для следования тем или иным условиям бизнеса, таким как аудит. Посмотрим на небольшой декоратор, который мы можем использовать для записи названия любой функции и того, что она делает:

Python

# -*- coding: utf-8 -*-
import logging

def log(func):
«»»
Логируем какая функция вызывается.
«»»

def wrap_log(*args, **kwargs):
name = func.__name__
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)

# Открываем файл логов для записи.
fh = logging.FileHandler(«%s.log» % name)
fmt = ‘%(asctime)s — %(name)s — %(levelname)s — %(message)s’
formatter = logging.Formatter(fmt)
fh.setFormatter(formatter)
logger.addHandler(fh)

logger.info(«Вызов функции: %s» % name)
result = func(*args, **kwargs)
logger.info(«Результат: %s» % result)
return func

return wrap_log

@log
def double_function(a):
«»»
Умножаем полученный параметр.
«»»
return a*2

if __name__ == «__main__»:
value = double_function(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# -*- coding: utf-8 -*-

importlogging

deflog(func)

«»»

    Логируем какая функция вызывается.
    «»»

defwrap_log(*args,**kwargs)

name=func.__name__

logger=logging.getLogger(name)

logger.setLevel(logging.INFO)

# Открываем файл логов для записи.

fh=logging.FileHandler(«%s.log»%name)

fmt=’%(asctime)s — %(name)s — %(levelname)s — %(message)s’

formatter=logging.Formatter(fmt)

fh.setFormatter(formatter)

logger.addHandler(fh)

logger.info(«Вызов функции: %s»%name)

result=func(*args,**kwargs)

logger.info(«Результат: %s»%result)

returnfunc

returnwrap_log
 
 

@log

defdouble_function(a)

«»»

    Умножаем полученный параметр.
    «»»

returna*2

if__name__==»__main__»

value=double_function(2)

Этот небольшой скрипт содержит функцию log, которая принимает функцию как единственный аргумент. Мы создаем объект логгер, а название лог файла такое же, как и у функции. После этого, функция log будет записывать, как наша функция была вызвана и что она возвращает, если возвращает.

В чем суть функционального программирования?

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

  • Функции являются объектами первого класса (First Class Object). Это означает, что с функциями вы можете работать, также как и с данными: передавать их в качестве аргументов другим функциям, присваивать переменным и т.п.
  • В функциональных языках не используются переменные (как именованные ячейки памяти), т.к. там нет состояний, а т.к. нет переменных, то и нет операции присваивания, как это понимается в императивном программировании.
  • Рекурсия является основным подходом для управления вычислениями, а не циклы и условные операторы.
  • Используются функции высшего порядка (High Order Functions). Функции высшего порядка – это функций, которые могут в качестве аргументов принимать другие функции.
  • Функции являются “чистыми” (Pure Functions) – т.е. не имеют побочных эффектов (иногда говорят: не имеют сайд-эффектов).
  • Акцент на том, что должно быть вычислено, а не на том, как вычислять.
  • Фокус на работе с контейнерами (хотя это спорное положение).

Python не является функциональным языком программирования, но его возможностей хватает, чтобы разрабатывать программы в функциональном стиле. 

Аргумент self

Рассмотрим зачем нужен и что означает self в функциях Python. Как можно было заметить, единственным атрибутом для метода из класса является ключевое слово self. Помещать его нужно в каждую функцию чтобы иметь возможность вызвать ее на текущем объекте. Также с помощью этого ключевого слова можно получать доступ к полям класса в описываемом методе. Self таким образом заменяет идентификатор объекта.

class Dog:
    name = "Charlie"
    noise = "Woof!"
    def makeNoise(self):
        print(self.name + " says: " + self.noise + " " + self.noise)
dog = Dog()
dog.makeNoise()

Charlie says: Woof! Woof!

Вверху представлен класс Dog, описывающий собаку. Он обладает полями name (имя) со стартовым значением «Charlie» и noise (шум), содержащим звук, который издает животное. Метод makeNoise заставляет собаку лаять, выдавая соответствующее сообщение на экран. Для этого в функции print используется получение доступа к полям name и noise. Далее необходимо создать экземпляр класса Dog и вызвать на нем makeNoise.

Примеры работы в функциональном стиле

Функции, как объекты первого класса

Ниже представлен код, который демонстрирует возможность Python, в части того, что функцию можно присвоить переменной и использовать ее:

>>> def mul5(value):
        return value*5

>>> v1 = 3
>>> f1 = mul5
>>> f1(v1)
15

Переменной f1, в качестве значения, присваивается функция mul5, после этого появляется возможность вызвать ее как функцию.

Рекурсия

Рекурсия – это вызов функции из нее же самой. Для начала приведем пример не рекурсивной функции, которая считает факториал:

def fact_iter(n):
   if n == 0 or n == 1:
       return 1
   else:
       prod = 1
       for i in range(1, n + 1):
           prod *= i
       return prod

print(fact_iter(5))

В качестве результата получим число 120.

Перепишем ее через рекурсию:

def fact_rec(n):
   if n == 0 or n == 1:
       return 1
   else:
       return n * fact_rec(n - 1)

print(fact_rec(5))

Результат получим аналогичный первому. Заметим, что такая реализация не является эффективной по памяти, о том почему это так, и как сделать правильно см “О рекурсии и итерации“

Функции высшего порядка

Функции высшего порядка принимают в качестве аргументов другие функции. В стандартную библиотеку Python входит достаточно много таких функции, в качестве примера приведем функцию map. Она принимает функцию и список, применяет функцию к каждому элементу списка и возвращает новый модифицированный список.

>>> fn = lambda x: x**2
>>> print(list(map(fn, )))

Чистые функции

Чистые функции – это функции, которые не имеют побочных эффектов. В Python это не выполняется. Необходимо самостоятельно следить за тем, чтобы функция была чистой.

Например, следующая функция модифицирует данные, которые в нее передаются:

def fun_not_clear(data):
    if len(data) > 0:
        data += 10

    return data*2

d1 = 
d2 = fun_not_clear(d1)

>>> print(d1)


>>> print(d2)

В функцию fun_not_clear был передан массив данных, над которым был проведен ряд модификаций и сформирован новый массив. При этом исходный массив тоже был изменен. Это пример побочного эффекта, который можно получить в Python. Более корректный вариант будет выглядеть так:

def fun_clear(data):
    data = data
    if len(data) > 0:
        data += 10

    return data*2

d1 = 
d2 = fun_clear(d1)

>>> print(d1)


>>> print(d2)

В следующих статьях будут более подробно описаны аспекты функционального программирования, которые можно использовать в Python

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

Конструктор классов с наследованием

class Person:

    def __init__(self, n):
        print('Person Constructor')
        self.name = n


class Employee(Person):

    def __init__(self, i, n):
        print('Employee Constructor')
        super().__init__(n)  # same as Person.__init__(self, n)
        self.id = i


emp = Employee(99, 'Pankaj')
print(f'Employee ID is {emp.id} and Name is {emp.name}')

Выход:

Employee Constructor
Person Constructor
Employee ID is 99 and Name is Pankaj
  • Мы обязаны вызвать конструктор суперкласса.
  • Мы можем использовать функцию super() для вызова функции конструктора суперкласса.
  • Мы также можем использовать имя суперкласса для вызова его метода init().

Возвращение итерируемых объектов вместо списков

Как уже упоминалось в разделе о , некоторые функции и методы в Python 3 теперь возвращают итерируемые объекты — вместо списков в Python 2.

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

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

Python 2

Python

print ‘Python’, python_version()

print range(3)
print type(range(3))

1
2
3
4

print’Python’,python_version()

printrange(3)

printtype(range(3))

Shell

Python 2.7.6

<type ‘list’>

1
2
3

Python2.7.6

,1,2

<type’list’>

Python 3

Python

print(‘Python’, python_version())

print(range(3))
print(type(range(3)))
print(list(range(3)))

1
2
3
4
5

print(‘Python’,python_version())

print(range(3))

print(type(range(3)))

print(list(range(3)))

Shell

Python 3.8.1
range(0, 3)
<class ‘range’>

1
2
3
4

Python3.8.1

range(,3)

<class’range’>

,1,2

Другие популярные функции, которые не возвращают списки в Python 3:

  • метод словаря
  • метод словаря
  • метод словаря

9.8. Iterators¶

By now you have probably noticed that most container objects can be looped over
using a statement:

for element in 1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one'1, 'two'2}:
    print(key)
for char in "123"
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

This style of access is clear, concise, and convenient. The use of iterators
pervades and unifies Python. Behind the scenes, the statement
calls on the container object. The function returns an iterator
object that defines the method which accesses
elements in the container one at a time. When there are no more elements,
raises a exception which tells the
loop to terminate. You can call the method
using the built-in function; this example shows how it all works:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

Having seen the mechanics behind the iterator protocol, it is easy to add
iterator behavior to your classes. Define an method which
returns an object with a method. If the class
defines , then can just return :

class Reverse
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 
            raise StopIteration
        self.index = self.index - 1
        return self.dataself.index

Изменение полей объекта

В Python объекту можно не только переопределять поля и методы, унаследованные от класса, также можно добавить новые, которых нет в классе:

>>> l.test = "hi"
>>> B.test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'B' has no 
attribute 'test'
>>> l.test
'hi'

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

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

>>> class User:
...     def setName(self, n):
...             self.name = n
...     def getName(self):
...             try:
...                     return self.name
...             except:
...                     print("No name")
... 
>>> first = User()
>>> second = User()
>>> first.setName("Bob")
>>> first.getName()
'Bob'
>>> second.getName()
No name

Подобные методы называют сеттерами (set – установить) и геттерами (get – получить).

Дескрипторы данных в классе (дескрипторы атрибутов).

Вызов функции — это краткий способ построения дескриптора данных, который запускает вызовы функций при доступе к атрибуту.

Его подпись:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

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

class C
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

Чтобы увидеть, как функция реализована с точки зрения , вот чистый эквивалент на Python:

class Property
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None
            return self
        if self.fget is None
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Встроенная функция помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибутам, а затем последующие изменения требуют вмешательства метода.

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

class Cell
    . . .
    def getvalue(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value
    value = property(getvalue)

Классно, неправда ли?

Список всех членов класса

функция может быть использована для получения списка членов класса:

Например:

Обычно ищут только «немагических» участников. Это можно сделать с помощью простого понимания , в котором перечислены члены, имена которых не начиная с :

Предостережения:

Классы можно определить метод. Если этот метод существует вызова будем называть , в противном случае Python будет пытаться создать список членов класса. Это означает, что функция dir может иметь неожиданные результаты

Две цитаты важности из официальной документации питона :

Если объект не содержит каталог ( как правило ), функция пытается все возможное , чтобы собрать информацию из атрибута Dict объекта, если он определен, и от его типа объекта. Полученный список не обязательно является полным, и может быть неточным , если объект имеет собственный GetAttr ().

Примечание: Поскольку реж () поставляется в первую очередь для удобства использования в интерактивной командной строке, он пытается поставить интересный набор имен больше , чем он пытается поставить строго или последовательно определенный набор имен, и его детальное поведение может измениться по релизы. Например, атрибуты метакласса отсутствуют в списке результатов, когда аргумент является классом.

Вызов методов через рефлексию

И в Java, и в Python имеются механизмы для вызова методов через рефлексию.
В вышеприведенном Java-примере вместо возвращения значения true в случае, если свойство найдено, можно было вызвать метод напрямую. Вспомним, что getDeclaredMethods() возвращает массив объектов типа Method. Объект Method сам содержит метод invoke(), который вызывает Method. В строке 7 вместо возвращения значения true, когда найден метод, можно вернуть method.invoke(object).

Эта возможность существует также и в Python. Однако, поскольку Python не делает различий между функциями и атрибутами, нужно специально искать сущности, которые можно вызвать:

Методы Python проще в управлении и вызове, чем в Java. Нижеприведенный код найдет метод объекта str() и вызовет его через рефлексию:

В данном примере проверяется каждый атрибут, возвращаемый функцией dir(). Мы получаем значение атрибута объекта, используя getattr(), и проверяем при помощи callable(), является ли оно вызываемой функцией. Если это так, то можно проверить, является ли его имя str (), и затем вызвать его.

Что такое self?

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

Python

class Vehicle(object):
«»»docstring»»»

def __init__(self, color, doors, tires):
«»»Constructor»»»
self.color = color
self.doors = doors
self.tires = tires

def brake(self):
«»»
Stop the car
«»»
return «Braking»

def drive(self):
«»»
Drive the car
«»»
return «I’m driving!»

if __name__ == «__main__»:
car = Vehicle(«blue», 5, 4)
print(car.color)

truck = Vehicle(«red», 3, 6)
print(truck.color)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

classVehicle(object)

«»»docstring»»»

def__init__(self,color,doors,tires)

«»»Constructor»»»

self.color=color

self.doors=doors

self.tires=tires

defbrake(self)

«»»

        Stop the car
        «»»

return»Braking»

defdrive(self)

«»»

        Drive the car
        «»»

return»I’m driving!»

if__name__==»__main__»

car=Vehicle(«blue»,5,4)

print(car.color)

truck=Vehicle(«red»,3,6)

print(truck.color)

Условия оператора if в данном примере это стандартный способ указать Пайтону на то, что вы хотите запустить код, если он выполняется как автономный файл. Если вы импортировали свой модуль в другой скрипт, то код, расположенный ниже проверки if не заработает. В любом случае, если вы запустите этот код, вы создадите два экземпляра класса автомобиля (Vehicle): класс легкового и класс грузового. Каждый экземпляр будет иметь свои собственные атрибуты и методы. Именно по этому, когда мы выводи цвета каждого экземпляра, они и отличаются друг от друга. Причина в том, что этот класс использует аргумент self, чтобы указать самому себе, что есть что. Давайте немного изменим класс, чтобы сделать методы более уникальными:

Python

class Vehicle(object):
«»»docstring»»»

def __init__(self, color, doors, tires, vtype):
«»»Constructor»»»
self.color = color
self.doors = doors
self.tires = tires
self.vtype = vtype

def brake(self):
«»»
Stop the car
«»»
return «%s braking» % self.vtype

def drive(self):
«»»
Drive the car
«»»
return «I’m driving a %s %s!» % (self.color, self.vtype)

if __name__ == «__main__»:
car = Vehicle(«blue», 5, 4, «car»)
print(car.brake())
print(car.drive())

truck = Vehicle(«red», 3, 6, «truck»)
print(truck.drive())
print(truck.brake())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

classVehicle(object)

«»»docstring»»»

def__init__(self,color,doors,tires,vtype)

«»»Constructor»»»

self.color=color

self.doors=doors

self.tires=tires

self.vtype=vtype

defbrake(self)

«»»

        Stop the car
        «»»

return»%s braking»%self.vtype

defdrive(self)

«»»

        Drive the car
        «»»

return»I’m driving a %s %s!»%(self.color,self.vtype)

if__name__==»__main__»

car=Vehicle(«blue»,5,4,»car»)

print(car.brake())

print(car.drive())

truck=Vehicle(«red»,3,6,»truck»)

print(truck.drive())

print(truck.brake())

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

Python

car braking
I’m driving a blue car!
I’m driving a red truck!
truck braking

1
2
3
4

car braking

I’m driving a blue car!

I’mdrivingared truck!

truck braking

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

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

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

Adblock
detector