Наконец, по-настоящему понять глобальные переменные и побочные эффекты

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

Что такое функция?

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

Концепция функции довольно проста, как только вы ее поймете. Функция — это просто черный ящик, в который вы помещаете некоторые данные и который дает вам определенные результаты. Но в этом определении мы предполагаем нечто совершенно неочевидное.

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

Например, представьте, что вы собираетесь завтракать и жаждете французского тоста. Что бы вы сделали?

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

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

french_toast = toaster(bread)

В этой самой ситуации тостер может изменить только «состояние» вашего хлеба.

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

Но Python — это не реальная жизнь

Что беспокоит, так это то, что в Python (и во многих других языках программирования) все не так просто.

В Python следующий «код» будет абсолютно правильным:

def toaster(bread) -> toast :
	...
	make_your_hair_blue()
	...
	return toast

Таким образом, вызов функции toaster может привести к неожиданным результатам.

Но как это?

Это может быть неочевидно, но эта функция make_your_hair_blue несет в себе много интересного для обсуждения.

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

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

Другими словами, наш черный ящик (тостер) может что-то изменить вне себя.

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

hair_color = "brown"
bread = "Not toasted"
def make_your_hair_blue():
	global hair_color
	hair_color = "blue"
def toaster(bread) -> bread :
	print("Toasting the bread")
	make_your_hair_blue()
	return "Toasted"
toasted_bread = toaster(bread)
print(toasted_bread) # Toasted
print(hair_color) # blue

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

global hair_color

А это многое значит.

Глобальное ключевое слово (или как сделать вещи более сложными, чем они уже есть)

Что делает эту строку интересной, так это наличие ключевого слова global.

Его роль довольно проста. Он просто сообщает Python, что у нас будет переменная с именем hair_color в нашей функции. Мало того, он также говорит Python, что эта переменная не должна быть новой, она должна фактически соответствовать переменной, уже существующей вне функции.

Таким образом, когда значение hair_color изменяется, мы фактически меняем значение внешней переменной.

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

В информатике мы называем это «побочным эффектом», и наша функция действительно имеет побочный эффект.



Побочные эффекты

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

Так что же нам делать? Должны ли мы полностью избегать побочных эффектов в нашем коде?

Ну никто не может дать строгий ответ.

Очевидный способ избавиться от этой проблемы — просто полностью удалить ключевое слово «global» и запретить побочные эффекты.

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

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

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

Использование объектно-ориентированного программирования для решения нашей проблемы с побочными эффектами?

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

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

Например, в нашей истории с тостером: тостер — это объект, хлеб, тост — это объекты, даже вы и ваши волосы — объекты.

Итак, мы концептуализировали нашу ситуацию с объектами, вот что мы могли получить:

class Human:
	def __init__(self):
		self.hair_color = "Brown"
	def make_hair_blue(self):
		self.hair_color = "Blue"
class Bread:
	def __init__(self):
		self.toasted = False
	def toast(self):
		self.toasted = True
bread = Bread()
you = Human()
print(you.hair_color) # Brown
print(bread.toasted) # False
bread.toast()
print(you.hair_color) # Brown
print(bread.toasted) # True

В этом примере вы можете видеть, что я выбрал два класса: класс Human (это вы) и класс Bread (это ваш будущий вкусный французский тост). Как видите, чтобы поджарить хлеб, мне пришлось использовать метод хлеба, метод toast.

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

Это означает, что каждый класс владеет данными, с которыми он работает.

Почему методы лучше, чем прямое присвоение?

Но я мог бы поступить иначе. Что, если я напишу это вместо этого:

# ...
bread = Bread()
you = Human()
print(you.hair_color) # Brown
print(bread.toasted) # False
# replace bread.toast() with
bread.toasted = True
print(you.hair_color) # Brown
print(bread.toasted) # True

Если вы попытаетесь запустить его, это сработает. Это допустимый код на Python, и ничто не мешает вам это сделать.

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

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

Вот что вы могли бы написать:

class Bread:
	def __init__(self):
		self.toasted = False
	def toast(self):
		self.toasted = True
		print("The toast is ready!")

Таким образом, единственное, что нужно изменить, — это определение класса, в остальном коде менять нечего:

# ...
bread = Bread()
print(bread.toasted) # False
bread.toast() # The toast is ready!
print(bread.toasted) # True

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

# ...
bread = Bread()
print(bread.toasted) # False
bread.toasted = True
print("The toast is ready!")
print(bread.toasted) # True

Итак, первая причина, по которой вам следует использовать метод, заключается в том, чтобы избежать повторения кода. Просто соблюдайте принцип СУХОЙ (не повторяйтесь).

Инкапсуляция

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

Если вы ленивы (мы сказали, что ленивы), вы, вероятно, не хотите заморачиваться со сложными деталями реализации, а скорее поджарите свой хлеб.

В этом сценарии наличие метода toast очень важно. Потому что вы не хотите возиться с функциями turn_on_toaster или make_sure_toast_is_ready.

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



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

C++, например, позволяет выбирать между public и private для определения «доступности» метода и переменных вашего класса.

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

Мы что-то решили?

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

class Bread:
	def __init__(self):
		self.toasted = False
	def toast(self):
		you.make_hair_blue()
		self.toasted = True

Так на самом деле мы ничего не решили, тостер все еще может изменить цвет волос?

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

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

Использование класса часто является хорошим способом получить четкое представление о том, к чьим данным вы обращаетесь. Потому что, как мы уже говорили ранее, класс отвечает за данные, которые он использует.

Что, если мне придется использовать глобальное ключевое слово?

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

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

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

Представьте, что у вас есть простая программа, которая считывает данные из потока и возвращает обработанные данные:

bytes_processed = 0
input_stream = connectToInputStream()
output_stream = connectToOutputStream()
def read_data():
	return input_stream.read(256)
def write_data(data):
	output_stream.write(data)
def process_data(data):
	global bytes_processed
	...
	bytes_processed += 1
	return new_data
while True:
	data = read_data()
	data = process_data(data)
	write_data(data)

Проблема в том, что мы используем ключевое слово global, а это признак плохо спроектированной программы. Вместо этого мы могли бы инкапсулировать все в «глобальный» класс:

class DataProcessingServer:
	def __init__(self, in_stream, out_stream):
		self.bytes_processed = 0
		self.input_stream = in_stream
		self.output_stream = out_stream
	def read_data(self):
		return self.input_stream.read(256)
	def write_data(self, data):
		self.output_stream.write(data)
	def process_data(self, data):
		...
		self.bytes_processed += 1
		return new_data
	def run(self):
		while True:
			data = self.read_data()
			data = self.process_data(data)
			self.write_data(data)
server = DataProcessingServer(
	connectToInputStream(),
	connectToOutputStream()
)
server.run()

Я переписал программу, чтобы использовать более удобный для ООП способ моделирования ситуации. Теперь все данные, используемые DataProcessingServer, фактически принадлежат классу, и если кто-то обращается к атрибуту сервера, он будет знать, что данные, которые он использует, принадлежат серверу.

Спасибо, что прочитали, и до скорой встречи!

Стань участником среды, чтобы читать все мои следующие истории, а также истории других!

Увидимся в следующей истории 👋

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.