Part 9

Объекты и ссылки

В Python каждое значение является объектом. Любой объект, созданный на основе класса, который вы определили сами, работает точно так же, как любой "обычный" объект Python. Например, объекты можно хранить в списке:

from datetime import date

class CompletedCourse:

    def __init__(self, course_name: str, credits: int, completion_date: date):
        self.name = course_name
        self.credits = credits
        self.completion_date = completion_date


if __name__ == "__main__":
    # Here we create some completed courses and add these to a list 
    completed = []

    maths1 = CompletedCourse("Mathematics 1", 5, date(2020, 3, 11))
    prog1 = CompletedCourse("Programming 1", 6, date(2019, 12, 17))

    completed.append(maths1)
    completed.append(prog1)

    # Let's add a couple more straight to the list
    completed.append(CompletedCourse("Physics 2", 4, date(2019, 11, 10)))
    completed.append(CompletedCourse("Programming 2", 5, date(2020, 5, 19)))

    # Go through all the completed courses, print out their names 
    # and sum up the credits received
    credits = 0
    for course in completed:
        print(course.name)
        credits += course.credits

    print("Total credits received:", credits)
Пример вывода

Mathematics 1 Programming 1 Physics 2 Programming 2 Total credits received: 20

Loading
Loading

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

class Product:
    def __init__(self, name: str, unit: str):
        self.name = name
        self.unit = unit


if __name__ == "__main__":
    shopping_list = []
    milk = Product("Milk", "litre")

    shopping_list.append(milk)
    shopping_list.append(milk)
    shopping_list.append(Product("Cucumber", "piece"))
9 1 1

Если на один и тот же объект есть несколько ссылок, не имеет значения, какая из них используется:

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

    def __str__(self):
        return self.name

dogs = []
fluffy = Dog("Fluffy")
dogs.append(fluffy)
dogs.append(fluffy)
dogs.append(Dog("Fluffy"))

print("Dogs initially:")
for dog in dogs:
    print(dog)

print("The dog at index 0 is renamed:")
dogs[0].name = "Pooch"
for dog in dogs:
    print(dog)

print("The dog at index 2 is renamed:")
dogs[2].name = "Fifi"
for dog in dogs:
    print(dog)
Пример вывода

Dogs initially: Fluffy Fluffy Fluffy The dog at index 0 is renamed: Pooch Pooch Fluffy The dog at index 2 is renamed: Pooch Pooch Fifi

Ссылки с индексами 0 и 1 в списке указывают на один и тот же объект. Любую из этих ссылок можно использовать для доступа к объекту. Ссылка с индексом 2 указывает на другой объект, хотя на вид с тем же содержимым. Изменение содержимого этого второго объекта не влияет на первый.

Оператор is используется для проверки того, указывают ли две ссылки ровно на один и тот же объект, а оператор == сообщает, одинаково ли содержимое объектов. Следующий пример, надеемся, прояснит разницу:

list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

print(list1 is list2)
print(list1 is list3)
print(list2 is list3)

print()

print(list1 == list2)
print(list1 == list3)
print(list2 == list3)
Пример вывода

False True False

True True True

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

class Student:
    def __init__(self, name: str, cr: int):
        self.name = name
        self.cr = cr

if __name__ == "__main__":
    # The key in this dictionary is the student number, 
    # and the value is an object of type Student
    students = {}
    students["12345"] = Student("Saul Student", 10)
    students["54321"] = Student("Sally Student", 67)

Инструмент визуализации помогает разобраться в примере выше:

9 1 2

self или без self?

До сих пор мы лишь поверхностно затрагивали использование имени параметра self. Давайте подробнее посмотрим, когда его следует использовать, а когда нет.

Ниже у нас есть простой класс, который позволяет создать объект словаря, содержащий некоторые слова:

class Vocabulary:
    def __init__(self):
        self.words = []

    def add_word(self, word: str):
        if not word in self.words:
            self.words.append(word)

    def print_vocabulary(self):
        for word in sorted(self.words):
            print(word)

vocab = Vocabulary()
vocab.add_word("python")
vocab.add_word("object")
vocab.add_word("object-oriented programming")
vocab.add_word("object")
vocab.add_word("nerd")

vocab.print_vocabulary()
Пример вывода

nerd object object-oriented programming python

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

Добавим в определение нашего класса новый метод. Метод longest_word(self) возвращает одно из самых длинных слов в словаре.

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

class Vocabulary:
    def __init__(self):
        self.words = []

    # ...

    def longest_word(self):
        # define two helper variables
        self.longest = ""
        self.length_of_longest = 0

        for word in self.words:
            if len(word) > self.length_of_longest:
                self.length_of_longest = len(word)
                self.longest = word

        return self.longest

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

class Vocabulary:
    def __init__(self):
        self.words = []

    # ...

    def longest_word(self):
        # define two helper variables
        self.helper = ""
        self.helper2 = 0

        for word in self.words:
            if len(word) > self.helper2:
                self.helper2 = len(word)
                self.helper = word

        return self.helper

Когда переменная объявляется с именем параметра self, она становится атрибутом объекта. Это значит, что переменная будет существовать столько же, сколько существует объект. В частности, переменная продолжит существовать и после того, как метод, объявивший ее, завершит выполнение. В примере выше это совершенно не нужно, потому что вспомогательные переменные должны использоваться только внутри метода longest_word(self). Поэтому объявлять вспомогательные переменные с именем параметра self здесь не очень хорошая идея.

Кроме того, что такие переменные продолжают существовать после своего "срока годности", использование self для создания новых атрибутов там, где они не нужны, может приводить к сложным ошибкам в коде. Особенно это касается атрибутов с общими именами вроде self.helper, которые затем используются в разных методах: они могут вызывать неожиданное поведение, которое трудно отследить.

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

class Vocabulary:
    def __init__(self):
        self.words = []
        # define helper variables
        self.helper = ""
        self.helper2 = ""
        self.helper3 = ""
        self.helper4 = ""

    # ...

    def longest_word(self):
        for word in self.words:
            # above the helper variables were all assigned string values
            # the following will not work because the type of helper2 is wrong
            if len(word) > self.helper2:
                self.helper2 = len(word)
                self.helper = word

        return self.helper

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

Итак, если вам нужны вспомогательные переменные для использования внутри одного метода, правильный способ — объявлять их без self. Чтобы код было легче понимать, используйте также информативные имена переменных:

class Vocabulary:
    def __init__(self):
        self.words = []

    # ...

    def longest_word(self):
        # the correct way of declaring helper variables 
        # for use within a single method
        longest = ""
        length_of_longest = 0

        for word in self.words:
            if len(word) > length_of_longest:
                length_of_longest = len(word)
                longest = word

        return longest

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

Объекты как аргументы функций

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

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

class Student:
    def __init__(self, name: str, student_number: str):
        self.name = name
        self.student_number = student_number

    def __str__(self):
        return f"{self.name} ({self.student_number})"

# the type hint here uses the name of the class defined above
def change_name(student: Student):
    student.name = "Saul Student"

# create a Student object
steve = Student("Steve Student", "12345")

print(steve)
change_name(steve)
print(steve)
Пример вывода

Steve Student (12345) Saul Student (12345)

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

from random import randint, choice

class Student:
    def __init__(self, name: str, student_number: str):
        self.name = name
        self.student_number = student_number

    def __str__(self):
        return f"{self.name} ({self.student_number})"

# This function creates and returns a new Student object.
# It randomly selects values for the name and the student number.
def new_student():
    first_names = ["Mark","Mindy","Mary","Mike"]
    last_names = ["Javanese", "Rusty", "Scriptor", "Pythons"]

    # randomly determine the name
    name = choice(first_names) + " " + choice(last_names)

    # randomly determine the student number
    student_number = str(randint(10000,99999))

    # Create and return a Student object
    return Student(name, student_number)

if __name__ == "__main__":
    # Call the function five times and store the results in a list
    students = []
    for i in range(5):
        students.append(new_student())

    # Print out the results
    for student in students :
        print(student)

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

Пример вывода

Mary Rusty (78218) Mindy Rusty (80068) Mike Pythons (70396) Mark Javanese (83307) Mary Pythons (45149)

Объекты как аргументы методов

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

class Person:
    def __init__(self, name: str, height: int):
        self.name = name
        self.height = height

class Attraction:
    def __init__(self, name: str, min_height: int):
        self.visitors = 0
        self.name = name
        self.min_height = min_height

    def admit_visitor(self, person: Person):
        if person.height >= self.min_height:
            self.visitors += 1
            print(f"{person.name} got on board")
        else:
            print(f"{person.name} was too short :(")

    def __str__(self):
        return f"{self.name} ({self.visitors} visitors)"

Класс Attraction содержит метод admit_visitor, который принимает объект типа Person как аргумент. Если посетитель достаточно высокого роста, его допускают на аттракцион, и количество посетителей увеличивается. Классы можно протестировать так:

rollercoaster = Attraction("Rollercoaster", 120)
jared = Person("Jared", 172)
alice = Person("Alice", 105)

rollercoaster.admit_visitor(jared)
rollercoaster.admit_visitor(alice)

print(rollercoaster)
Пример вывода

Jared got on board Alice was too short :( Rollercoaster (1 visitors)

Loading
Loading

Экземпляр того же класса как аргумент метода

Ниже у нас есть еще одна версия класса Person:

class Person:
    def __init__(self, name: str, year_of_birth: int):
        self.name = name
        self.year_of_birth = year_of_birth

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

def older_than(person1: Person, person2: Person):
    if person1.year_of_birth < person2.year_of_birth:
        return True
    else:
        return False

muhammad = Person("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Person("Blaise Pascal", 1623)
grace = Person("Grace Hopper", 1906)

if older_than(muhammad, pascal):
    print(f"{muhammad.name} is older than {pascal.name}")
else:
    print(f"{muhammad.name} is not older than {pascal.name}")

if older_than(grace, pascal):
    print(f"{grace.name} is older than {pascal.name}")
else:
    print(f"{grace.name} is not older than {pascal.name}")
Пример вывода

Muhammad ibn Musa al-Khwarizmi is older than Blaise Pascal Grace Hopper is not older than Blaise Pascal

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

class Person:
    def __init__(self, name: str, year_of_birth: int):
        self.name = name
        self.year_of_birth = year_of_birth

    # NB: type hints must be enclosed in quotation marks if the parameter
    # is of the same type as the class itself!
    def older_than(self, another: "Person"):
        if self.year_of_birth < another.year_of_birth:
            return True
        else:
            return False

Здесь объект, для которого вызывается метод, обозначается как self, а другой объект Person — как another.

Помните, что вызов метода отличается от вызова функции. Метод прикреплен к объекту через точечную нотацию:

muhammad = Person("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Person("Blaise Pascal", 1623)
grace = Person("Grace Hopper", 1906)

if muhammad.older_than(pascal):
    print(f"{muhammad.name} is older than {pascal.name}")
else:
    print(f"{muhammad.name} is not older than {pascal.name}")

if grace.older_than(pascal):
    print(f"{grace.name} is older than {pascal.name}")
else:
    print(f"{grace.name} is not older than {pascal.name}")

Слева от точки находится сам объект, который внутри определения метода обозначается как self. В скобках находится аргумент метода — объект, обозначенный как another.

Вывод программы ровно такой же, как и в реализации с функцией выше.

В завершение — скорее косметическое замечание: структура if...else в методе older_than по большому счету не нужна. Значение булева выражения в условии уже является ровно тем же истинностным значением, которое возвращается. Поэтому метод можно упростить:

class Person:
    def __init__(self, name: str, year_of_birth: int):
        self.name = name
        self.year_of_birth = year_of_birth

    # NB: type hints must be enclosed in quotation marks if the parameter 
    # is of the same type as the class itself!
    def older_than(self, another: "Person"):
        return self.year_of_birth < another.year_of_birth

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

class Person:
    # ...

    # this would cause an error, as Person must be enclosed in quotation marks
    def older_than(self, another: Person):
        return self.year_of_birth < another.year_of_birth:
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.