Объекты и ссылки
В 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
Возможно, вы помните, что списки не содержат сами объекты. Они содержат ссылки на объекты. Один и тот же объект может появляться в одном списке несколько раз, и на него можно ссылаться несколько раз как внутри списка, так и вне его. Посмотрим на пример:
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"))
Если на один и тот же объект есть несколько ссылок, не имеет значения, какая из них используется:
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)Инструмент визуализации помогает разобраться в примере выше:
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)
Экземпляр того же класса как аргумент метода
Ниже у нас есть еще одна версия класса 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:Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.