# Классы

Классы - это ядро Python. Они дают широкие возможности, но их легко неправильно использовать. В этой главе я расскажу про некоторые трюки в работе с классами и сделаю несколько предостережений. Давайте начнем!

## Переменные экземпляра и класса

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

Основное различие:

* Переменные экземпляров предназначены для данных уникальных для каждого

  объекта
* Переменные класса - для общих для всех экземпляров класса данных

Посмотрим на пример:

```python
class Cal(object):
    # pi - переменная класса
    pi = 3.142

    def __init__(self, radius):
        # self.radius - переменная экземпляра
        self.radius = radius

    def area(self):
        return self.pi * (self.radius ** 2)

a = Cal(32)
a.area()
# Вывод: 3217.408
a.pi
# Вывод: 3.142
a.pi = 43
a.pi
# Вывод: 43

b = Cal(44)
b.area()
# Вывод: 6082.912
b.pi
# Вывод: 3.142
b.pi = 50
b.pi
# Вывод: 50
```

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

```python
class SuperClass(object):
    superpowers = []

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

    def add_superpower(self, power):
        self.superpowers.append(power)

foo = SuperClass('foo')
bar = SuperClass('bar')
foo.name
# Вывод: 'foo'

bar.name
# Вывод: 'bar'

foo.add_superpower('fly')
bar.superpowers
# Вывод: ['fly']

foo.superpowers
# Вывод: ['fly']
```

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

## Классы нового стиля

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

* Классы старого стиля ничему не наследуют
* Классы нового стиля наследуют `object`

Базовый пример:

```python
class OldClass():
    def __init__(self):
        print('Я старый класс')

class NewClass(object):
    def __init__(self):
        print('Я новый модный класс')

old = OldClass()
# Вывод: Я старый класс

new = NewClass()
# Вывод: Я новый модный класс
```

Наследование от `object` дает новым классам доступ к определенной *магии*. Например, вы можете использовать оптимизацию `__slots__`, вызов `super()` и дескрипторы. Резюме? Всегда используйте классы нового стиля.

**Примечание:** в Python 3 все классы нового стиля. Не важно наследует ли ваш класс `object` или нет. Тем не менее, хорошей идеей будет явно прописывать наследование.

## Магические методы

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

### `__init__`

Это конструктор класса. Конструктор вызывается каждый раз при создании экземпляра класса. Например:

```python
class GetTest(object):
    def __init__(self):
        print('Приветствую!')

    def another_method(self):
        print('Я другой метод, который не вызывается автоматически')

a = GetTest()
# Вывод: Приветствую!

a.another_method()
# Вывод: Я другой метод, который не вызывается автоматически
```

Как вы видите `__init__` вызывается при создании экземпляра класса. Вы также можете передавать аргументы конструктору для инициализации экземпляра:

```python
class GetTest(object):
    def __init__(self, name):
        print('Приветствую! {0}'.format(name))

    def another_method(self):
        print('Я другой метод, который не вызывается автоматически')

a = GetTest('Yasoob')
# Output: Приветствую! Yasoob

# Попробуем создать экземпляр без аргумента "name"
b = GetTest()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 2 arguments (1 given)
```

Надеюсь в общих чертах логика работы `__init__` понятна.

### `__getitem__`

Реализация метода `__getitem__` в классе позволяет использовать на его экземплярах `[]` оператор. Пример:

```python
class GetTest(object):
    def __init__(self):
        self.info = {
            'name':'Yasoob',
            'country':'Pakistan',
            'number':12345812
        }

    def __getitem__(self,i):
        return self.info[i]

foo = GetTest()

foo['name']
# Вывод: 'Yasoob'

foo['number']
# Вывод: 12345812
```

Без реализации `__getitem__` вы бы получили следующую ошибку:

```
>>> foo['name']

Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: 'GetTest' object has no attribute '__getitem__'
```
