Ссылки и объекты

Представление переменных в языке Python сильно отличается от традиционных языков программирования: Pascal, C, Java. Любая сущность, с которой работает интерпретатор, является объектом. Функции, например, также являются объектами и в некотором смысле ничем не отличаются от чисел, кроме того, что у функций есть операции, нетипичные для чисел (например, вызов функции при помощи операции ()).

У каждого объекта есть свой тип (это также называется классом): int, str, set, function и т.д.

Переменные в свою очередь являются ссылками на объекты: в любой момент времени переменная связана с каким-то объектом в памяти. Если мы выполняем присваивание:

a = 5

то имя переменной a связывается с объектом типа int, в котором хранится значение 5.

Если теперь выполнить другое присваивание:

a = 'hello'

то старая связь разрывается, создается новый объект типа str, в котором хранится значение 'hello', и переменная a связывается с этим объектом.

Если же теперь выполнить операцию:

def a():
    pass

то в памяти создается объект типа function, хранящий тело функции из одной инструкции pass, и переменная a теперь связывается с этой функцией.

Если же справа от оператора = поставить имя другого объекта:

b = a

то второй объект связывается с тем же объектом, с которым был связан первый объект: теперь b и a становятся ссылками на один и тот же объект и являются неразличимыми, пока для одного из них связь не будет разорвана при помощи оператора =

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

a is b

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

a = 'a' * 1000
b = 'aa' * 500

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

a = 2 + 3
b = 1 + 4

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

Такое повторное использование применяется только для небольших чисел, строк и объектов типа bool, поскольку для больших объектов проверка того, что объект уже хранится в памяти требует много времени.

Создание новых типов данных

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

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

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

В этом случае придется поставать так:

print('Имя:', Vasya[0])
print('Балл:', Vasya[1])

Но элементы кортежей не имеют собственных названий (то есть вовсе неочевидно, балл хранится именно в элементе кортежа с индексом 1). Удобней было бы обозначить атрибуты каждого объекта какими-то осмысленными идентификаторами и далее работать с этими идентификаторами. Пример:

class Person:
    pass

Vasya = Person()
Vasya.name = 'Василий'
Vasya.score = 4

В этом примере мы объявляем новый класс объектов: Person. Вызов Person() создаёт новый объект и возвращает ссылку на него, которая сохраняется в переменной Vasya. То, что получилось, называется экземпляром класса или объектом. Далее объекту Vasya устанавливается два атрибута: name (типа str) и score (типа int).

Поля и методы

Таким образом, объекты классов представляют собой новые типы данный, объединяющие несколько атрибутов (полей). Атрибуты могут быть произвольными типами данных: числами, строками, списками, множествами, словарями, другими классами. Обращение к атрибуту какого-либо объекта осуществляется при помощи dot-нотации: имя_класса.имя_атрибута.

Помимо полей у классов бывают методы: функции, которые можно применять к экземплярам класса. Например, у списков есть метод sort. Вызов метода также осуществляется при помощи dot-нотации, например: A.sort().

Можно рассматривать методы, как функции, у которых первым параметром является экземпляр класса. Методы так и объявляются: как функции внутри описания класса, первым параметром которой является экземпляр класса. По соглашению, эта ссылка должна называться self. Вот пример объявления класса Person и метода print, выводящего информацию о полях name и score:

    def print(self):
        print(self.name, self.score)

Теперь для вызова метода print для объекта Vasya нужно вызвать Vasya.print(). При этом не нужно задавать первый параметр self: в качестве этого параметра автоматически будет передан объект, для которого был вызван метод.

Методы могут принимать дополнительные параметры, как и обычные функции. Эти параметры описываются после параметра self.

Стандартные методы

Наш метод print предполагает, что у объекта есть поля name и score, иначе он завершится с ошибкой. Хочется быть уверенным, что у любого объекта класса Person есть эти поля. Для этого проще всего создать эти поля при создании объекта, т.е. при вызове функции Person. Для этого можно использовать конструктор: метод, который автоматически вызывается при создании объекта. Конструктором является метод с именем __init__:

class Person:
    def __init__(self):
        self.name = ''
        self.score = 0

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

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

class Person:
    def __init__(self, name, score):
        self.name = name
        self.score = score

В данном случае мы используем одинаковые имена (name, score) для обозначения передаваемых параметров и полей класса. Это сделано для удобства — имена могут и различаться.

Теперь мы сможем создавать новый объект с заданными полями так: Person('Иванов', 5).

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

class Person:
    def __init__(self, name='', score=0):
        self.name = name
        self.score = score

Теперь мы можем вызывать конструктор как с параметрами (Person('Иванов', 5)), так и без параметров (Person()), в последнем случае параметрам будут переданы значения «по умолчанию», указанные в описании конструктора.

Есть и другие стандартные методы, которые можно определить в описании класса.

Метод __repr__ должен возвращать текстовую строку, содержащую код (на языке Python), создающий объект, равный данному. Естественно, метод __repr__ должен содержать вызов конструктора, которому передаются в качестве параметров все строки исходного объекта, то есть он должен возвращать строку вида "Person('Иванов', 5)"

Пример метода __repr__ (для экономии места опустим описание конструктора __init__):

class Person:
    def __repr__(self):
        return "Person('" + self.name + "', " + self.score + ")"

Таким образом, метод __repr__ возвращает строку с описанием объекта, которое может быть воспринято итерпретатором языка Питон.

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

class Person:
    def __str__(self):
        return self.name + ' ' + str(self.score)

Метод __str__ будет вызываться, когда вызывается функция str от данного объекта, например, str(Vasya). То есть создавая метод __str__ вы даете указание Питону, как преобразовывать данный объект к типу str.

Поскольку функция print использует именно функцию str для вывода объекта на экран, то определение метода __str__ позволит выводить объекты на экран удобным способом: при помощи print.

Переопределение стандартных операций

Рассмотрим класс Point (точка), используемый для представления точек (или радиус-векторов) на координатной плоскости. У точки два естественных поля-координаты: x и y. Если рассматривать точку как радиус-вектор, то хотелось бы определить для точек операцию +, чтобы точки можно было складывать столь же удобно, как и числа или строки. Например, чтобы

A = Point(1, 2)
B = Point(3, 4)
C = A + B

Для этого необходимо перегрузить операцию +: определить функцию, которая будет использоваться, если операция + будет вызвана для объекта класса Point. Для этого нужно определить метод __add__ класса Point, у которого два параметра: неявная ссылка self на экземпляр класса, для которого она будет вызвана (это левый операнд операции +) и явная ссылка other на правый операнд:

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

Теперь при вызове оператора A + B Питон вызовет метод A.__add__(B), то есть вызовет указанный метод, где self = A, other = B.

Аналогично можно определить и оставшиеся операции. Полезной для переопределения является операция <. Она должна возвращать логическое значение True, если левый операнд меньше правого или False в противном случае (также в том случае, если объекты равны). Для переопределения этого операнда нужно определить метод __lt__ (less tthan):

class Point:
    def __lt__(self, other):
        return self.x < other.x or self.x == other.x and self.y < other.y

В этом примере оператор вернет True, если у левого операнда поле x меньше, чем у правого операнда, а также если поля x у них равны, а поле y меньше у левого операнда.

После определения оператора <, появляется возможность упорядочивать объекты, используя этот оператор. Теперь можно сортировать списки объектов при помощи метода sort() или функции sorted, а также находить максимум и минимум, при этом будет использоваться именно определенный оператор сравнения <.

Функции type и isinstance, параметры различных типов

В конструктор можно передавать параметры различных типов. Например, удобно инициализировать точку не только двумя числами, но и строкой, в которой через пробел записаны два числа (такая строка может быть считана со стандартного ввода), списком или кортежем. То есть передаваемые конструктору аргументы могут быть разного типа (int, float, str, list, tuple). Конструктор должен выполнять различные действия для параметров различного типа, для этого нужно уметь проверять принадлежность объекту какому-либо классу.

Эту задачу можно решить при помощи функций type и isinstance. Функция type возвращает класс, к которому принадлежит объект. Например:

if type(a) == int:
    print('a -  целое число')
elif type(a) == str:
    print('a - строка')

Можно также использовать и функцию isinstance, у которой два параметра: объект и класс. Функция возвращает True, если объект принадлежит классу или False в противном случае. Пример:

if isinstance(a, int):
    print('a -  целое число')
elif isinstance(a, str):
    print('a - строка')
Разница между этими способами в том, что функция type возвращает класс объекта буквально, и сравнение типов требует полного совпадения. Функция isinstance же проверяет тип «по смыслу»: экземпляры более узкоспецифичных классов (производных классов) обычно являются экземплярами базового класса. Кроме того, эту проверку можно переопределить. А ещё функция isinstance позволяет проверять принадлежность любому из перечисленных классов:
isinstance({1,2,3}, (tuple, list, dict, set))  # True
isinstance([1,2,3], (tuple, list, dict, set))  # True
isinstance((1,2,3), (tuple, list, dict, set))  # True
Перед тем, как начать создавать свои новые классы, разберёмся с уже реализованными в Python примерами.

Числа с плавающей точкой

В языке Python реализована длинная арифметика. Это означает, что целые числа в вычисления могут быть сколь угодны большими, длина чисел ограничена только объёмом доступной памяти. Для дробных чисел типа float используется представление в виде чисел с плавающей точкой. Каждое число типа float хранится в виде \(\pm a\cdot 2^b\). Числа типа float обеспечивают точность в 15-17 десятичных цифр и масштабы в диапазоне примерно от 10−308 до 10308. Число \(a\) называется мантиссой, число \(b\) — экспонентой или порядком. На хранение экспоненты отводится 11 бит, на хранение мантиссы — 52 бита. Ещё один бит отводится на хранение знака числа. В итоге получается 64 бита или 8 байт.

Из-за такого представления чисел (а сложно придумать что-то более удачное по комбинации точности и скорости) можно наткнуться на странное.

A: Ассоциативность сломалась

Для чисел a = 11111111111111113, b = -11111111111111111, c = 2.9 выведите отношение (a+b)+c к a+(b+c).
IDE

B: Дистрибутивность сломалась

Для чисел u = 200000000000000000, v = -600, w = 600.0003 выведите разность u * (v+w) и (u*v) + (u*w).
IDE

C: Рекуррентное соотношение Мюллера

Дана функция $$f(x, y) = 108 - \displaystyle\frac{815-1500/x}{y}.$$ Рекуррентная последовательность вычисляется по следующему правилу: $x_0 = 4$, $x_1=4{.}25$, $x_n = f(x_{n-2}, x_{n-1})$. По числу $n$ выведите значение $x_n$. К какому числу стремится данная последовательность?
0
4
5
4.855700712568563
IDE
Я уже решил успешно сдал третью задачу.
К сожалению, 100 даже близко не является правильным ответом в предыдущей задаче. На самом деле последовательность сходится к 5.
Эта последовательность известна под именем «Рекуррентное соотношение Мюллера» и синтезирована специально для демонстрации того, как быстро и драматически ошибки округления чисел с плавающей запятой приводят к катастрофическим результатам, при правильных (ну, неправильных) условиях.
Многие знают, что математика чисел с плавающей запятой может пойти не так в некоторых ситуациях, но отмахиваются от проблемы, считая, что это может произойти только если в вычислениях участвуют очень большие или очень маленькие числа, либо когда накапливается огромное число ошибок. Это упражнение с невинно выглядящими константами и небольшим количеством итераций прекрасно демонстрирует, что ошибки округления могут иметь существенное влияние даже помимо экстремальных чисел. На самом деле это очень интересная последовательность. При вычислениях с любой сколь угодно высокой, но конечной точностью эта последовательность сойдётся к 100.
Для того, чтобы в этом убедиться, а также для того, чтобы освоить класс десятичных чисел Decimal и класс дробей Fraction мы решим следующие задачи.

Класс Decimal

В тех случаях, когда необходимо гарантировать точность операций с дробными десятичными числами, или когда нужно вычислить некоторое нецелое число с большой точностью, на помощь придёт класс Decimal десятичных чисел с фиксированной точностью. Для начала работы с ним нужно сказать
from decimal import *

В языке Python имена переменных и имена функцию принято именовать в виде слов_с_подчёркиваниями_маленькими_буквами. Для классов принято использовать то, что называется CamelCase. Таким образом в модуле decimal класс называется Decimal. У класса Decimal реализован конструктор __init__, который умеет принимать на вход целое число, число с плавающей точкой или строку:

from decimal import *
>>> a = Decimal(3)
>>> b = Decimal(3.12)
>>> c = Decimal('1.23')
>>> print(repr(a), repr(b), repr(c))
Decimal('3') Decimal('3.12000000000000010658141036401502788066864013671875') Decimal('1.23')
>>> print(str(a), str(b), str(c))
3 3.12000000000000010658141036401502788066864013671875 1.23
>>> print(a, b, c)
3 3.12000000000000010658141036401502788066864013671875 1.23
Тут мы сразу видим многое из длинного текста в верху страницы. Для работы функции repr() в классе реализован метод __repr__(), который выводит строку, из которой можно назад создать в точности такой же объект. Для работы функции str и print (которая её использует), в классе реализован метод __str__(), выводящий объект в удобной для человека форме.
А ещё мы видим, что число с плавающей точкой 3.12 не совсем такое, каким кажется на первый взгляд. Поэтому нецелое десятичное число нужно создавать только из строчки, но никак не из float.

Идём дальше, к математическим операциям. Пример:

>>> from decimal import *
>>> a = Decimal('1.03')
>>> b = a + 4
>>> c = a * 23
>>> d = a / 179
>>> print(a, b, c, d)
1.03 5.03 23.69 0.005754189944134078212290502793
>>> print(repr(a), repr(b), repr(c), repr(d))
Decimal('1.03') Decimal('5.03') Decimal('23.69') Decimal('0.005754189944134078212290502793')
Здесь мы видим, что числа класса Decimal можно складывать и умножать на целые числа. Операции с типом float не сработают: разработчики так защищают нас от ошибок, которые мы непременно получим из-за таких операций. Для того, чтобы работало описанное в предыдущем примере, в классе Decimal реализованы методы __sum__(), __mul__() и __truediv__(). На самом деле в классе Decimal переопределены все методы, связанные с работой с классом как с числом. Благодаря этому числами довольно удобно пользоваться.

D: Рекуррентное соотношение Мюллера — 2

Проделайте всё то же, что и в третьей задаче, только пользуйтесь только числами класса Decimal. По числу $n$ выведите значение $x_n$ .
0
4
20
4.9897059157620938291040004
IDE
По умолчанию числа имеют точность 28 знаков после запятой. Однако можно поставить почти сколь угодно большую точность. Для этого необходимо выполнить команду вида
getcontext().prec = 50

E: Рекуррентное соотношение Мюллера — 3

Проделайте всё то же, что и в предыдущей задаче задаче, только пользуйтесь только числами класса Decimal, в которых используется 100 знаков после запятой. По числу $n$ выведите значение $x_n$ .
0
4
65
4.9999999999999927663426308945434368463937208210588061900248797114539024338023055209601725256405658
IDE

F: Число e

Некоторые в листке про число e сумели доказать замечательную формулу: $$ e = 1 + \frac{1}{1!} + \frac{1}{2!} + \frac{1}{3!} + \frac{1}{4!} + \ldots $$ С её помощью можно вычислить это число с любой точностью.
Дано число n. Выведите число e с первыми n знаками после запятой.
3
2.718
20
2.71828182845904523536
Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Нужно взять точность знаков на 10-20 больше, чем требуется. И суммировать, пока очередное слагаемое не станет нулём. (Вы ведь не будете при этом считать $n!$?) После этого полученный результат превратить в строку и взять от неё первые $n+2$ символа.

IDE

Класс Fraction

Для работы с рациональными числами на помощь придёт класс Fraction. Для начала работы с ним нужно сказать
from fractions import *

У класса Fraction реализован конструктор __init__, который умеет принимать на вход пару целых чисел (числитель и знаменатель), число типа float (берегись ошибки!), число типа Decimal или даже строку:

>>> from fractions import *
>>> Fraction(1, 2)
Fraction(1, 2)
>>> Fraction(-153, -272)
Fraction(9, 16)
>>> Fraction(2.5)
Fraction(5, 2)
>>> from decimal import Decimal
>>> Fraction(Decimal('1.1'))
Fraction(11, 10)
>>> Fraction('9/16')
Fraction(9, 16)
>>> Fraction(' -3/7 ')
Fraction(-3, 7)
>>> Fraction(3.12)
Fraction(3512807709348987, 1125899906842624)
>>> str(Fraction(' -3/7 '))
'-3/7'
>>> str(Fraction(4,2))
'2'
Снова мы видим многое из длинного текста в верху страницы. Для работы функции repr() в классе реализован метод __repr__(), который выводит строку, из которой можно назад создать в точности такой же объект. Для работы функции str и print (которая её использует), в классе реализован метод __str__(), выводящий объект в удобной для человека форме.
А ещё мы видим, что число с плавающей точкой 3.12 не совсем такое, каким кажется на первый взгляд. Поэтому нецелое десятичное число нужно создавать только из строчки, но никак не из float.

Идём дальше, к математическим операциям. Пример:

>>> from fractions import *
>>> Fraction(5, 16) - Fraction(1, 4)
Fraction(1, 16)
>>> Fraction(1, 16) * Fraction(3, 16)
Fraction(3, 256)
>>> Fraction(3, 16) / Fraction(1, 8)
Fraction(3, 2)
>>> 3 + Fraction(3,-12)
Fraction(11, 4)
>>> Fraction('4/9') - 49
Fraction(-437, 9)
>>> 6 / Fraction(1,6)
Fraction(36, 1)
>>> Fraction(1,2) ** 8
Fraction(1, 256)
>>> Fraction(1, 8) ** Fraction(1, 2)
0.3535533905932738
Здесь мы видим, что числа класса Fraction можно складывать, умножать, делить и возводить в степени. Для того, чтобы работало описанное в предыдущем примере, в классе Fraction реализованы методы __sum__(), __mul__() и __truediv__(), и т.д. Метод __pow__() реализован так, что при возведение в целую степень получается число типа Fraction, в противном случае получается float.

Для того, чтобы добраться до числителя и знаменателя у объектов класса есть атрибуты numerator и denominator:

>>> from fractions import *
>>> f = Fraction(221, 234) + Fraction(1, 2)
>>> f.numerator
13
>>> f.denominator
9

G: Рекуррентное соотношение Мюллера — 4

Проделайте всё то же, что и в третьей задаче, только пользуйтесь только числами класса Fraction. По числу $n$ выведите значение $x_n$. (Докажите уже наконец, что предел этой последовательности равен 5).
0
4
4
1684/353
IDE
У объектов типа Fraction есть крайне полезный метод limit_denominator(). Результатом его работы является рациональная дробь, самая близкая к данному числу из тех, чей знаменатель не превосходит переданного параметра:
>>> from fractions import Fraction
>>> Fraction('3.1415926535897932').limit_denominator(10)
Fraction(22, 7)
>>> Fraction('3.1415926535897932').limit_denominator(100)
Fraction(311, 99)
>>> Fraction('3.1415926535897932').limit_denominator(1000)
Fraction(355, 113)
>>> Fraction('3.1415926535897932').limit_denominator(10000)
Fraction(355, 113)
>>> Fraction('3.1415926535897932').limit_denominator(100000)
Fraction(312689, 99532)
>>> Fraction('3.1415926535897932').limit_denominator(1000000)
Fraction(3126535, 995207)
>>> Fraction('3.1415926535897932').limit_denominator(10000000)
Fraction(5419351, 1725033)
Это позволяет получить очень точные рациональные приближения иррациональных чисел.

H: Корень из двух

Известно представление корня из двойки в виде цепной дроби: $$\sqrt{2} = 1 + \cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{2 + \ddots}}}$$ Для данного положительного n выведите значение такой дроби, в которой n двоек. В следующей строчке выведите отличие данной дроби от корня из 2, вычисленного с точностью 100 знаков после запятой (отличие нужно получить в формате float). Для вычисления корня используйте метод sqrt() чисел типа Decimal.
1
3/2 0.08578643762690495
6
239/169 1.2378941142386079e-05
Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Для того, чтобы вычесть из числа типа Decimal дробь, потребуется первое превратить в дробь. А модуль разности для наглядности нужно превратить во float.

IDE

I: Число e — 2

Для данного положительного n выведите дробь, наиболее близкую к числу e среди тех, чей знаменатель не превосходит n. В следующей строчке выведите отличие данной дроби от числа e, вычисленного с точностью 100 знаков после запятой (отличие нужно получить в формате float).
1
3 0.28171817154095474
6
11/4 0.031718171540954763
IDE