Ультимейт-курс по Python
Гайд от Матвея 🥸
https://ra1nbow.xyz
[!info] Github
https://github.com/ra1nbow1
По многочисленным просьбам пишу продолжение для тех, кто знает основы и хочет учиться дальше.
filter()
filter(func, iterator) применяет другую функцию к заданному итерируемому объекту (список, строка, словарь и так далее), проверяя, нужно ли сохранить конкретный элемент или нет. Простыми словами, она отфильтровывает то, что не проходит и возвращает все остальное.
def f(x):
return x % 2 == 0
print(list(filter(f, [1, 2, 3, 4]))) # [2, 4]
Добавили функцию f(x), которая определяет четность числа x. Результат этой функции он чисел 2 и 4 - True, поэтому они и остались в отфильтрованном списке.
Словари
Словари (в других языках их называют объектами) - гениальная и самая мультифункциональная вещь! Это неупорядоченные коллекции произвольных объектов с доступом по ключу. Их иногда ещё называют ассоциативными массивами или хеш-таблицами.
Создание словарей
d1 = {}
d2 = dict()
d3 = dict(x=1, y=2) # ключ x: значение 1; ключ y: значение 2
print(d1, d2, d3) # {} {} {'x': 1, 'y': 2}
Добавление и извлечение данных
Данные в словаре существуют в виде ключ:значение.
d = {'x': 1, 'y': 2}
# Получаем значение ключа x
print(d['x']) # 1
# Добавляем ключ со значением
d['z'] = 3
print(d) # {'x': 1, 'y': 2, 'z': 3}
Значение ключа можно изменять таким же образом:
d = {'x': 1, 'y': 2}
d['x'] = 3
print(d) # {'x': 3, 'y': 2}
Методы словарей
dict.clear() # очищает словарь
dict.copy() # возвращает копию словаря
dict.get(key[, default]) # возвращает значение ключа, но если его нет, не дает ошибку, а возвращает default (по умолчанию None)
dict.items() # возвращает пары (ключ, значение)
dict.keys() # возвращает ключи в словаре
dict.values() # возвращает значения в словаре.
dict.pop(key[, default]) # удаляет ключ и возвращает значение. Если ключа нет, возвращает default (по умолчанию дает ошибку)
dict.update([x]) # обновляет словарь, добавляя пары (ключ, значение) из x. Существующие ключи перезаписываются. Возвращает None (не новый словарь!)
Сортировка словарей
Часто значения, находящиеся в словарях, необходимо сортировать. Словари отлично поддаются сортировке, если знать несколько вещей.
Рассмотрим задачу: отсортировать словарь, где ключами являются цифры, а значениями - строки, по ключам.
dictionary = {5: 'a', 2: 'b', 4: 'c', 1: 'd', 3: 'e', 0: 'f'}
def get_first_item(t):
return t[0]
keys_and_values = dictionary.items() # [(5, 'a'), (2, 'b'), (4, 'c'), (1, 'd'), (3, 'e'), (0, 'f')]
print(sorted(keys_and_values, key=get_first_item))
# [(0, 'f'), (1, 'd'), (2, 'b'), (3, 'e'), (4, 'c'), (5, 'a')]
get_first_item() возвращает первый элемент кортежа. .items() возвращает массив кортежей, в каждом из которых лежит ключ и его значение. Таким образом, мы реализовали сортировку по специальному правилу - по первому элементу в кортеже.
Можно было вовсе не указывать такое “особое правило сортировки”, так как, если сортировать нужно массивы, кортежи и другие итерируемые элементы, сортировка происходит по первому элементу по умолчанию.
Обратная задача - отсортировать словарь, где ключами являются строки, а значениями - цифры, по значениям.
dictionary = {'a': 5, 'b': 2, 'c': 4, 'd': 1, 'e': 3, 'f': 0}
def get_second_item(t):
return t[1]
keys_and_values = dictionary.items() # [('a', 5), ('b', 2), ('c', 4), ('d', 1), ('e', 3), ('f', 0)]
print(sorted(keys_and_values, key=get_second_item))
# [('f', 0), ('d', 1), ('b', 2), ('e', 3), ('c', 4), ('a', 5)]
Здесь наоборот - get_second_item() возвращает второй элемент кортежа, и в итоге сортировка производится по второму элементу.
Лямбда-функции
Лямбда-функции - просто компактная запись обычных. По функционалу они ничем не отличаются. Используются скорее для указания каких-то небольших блоков кода для итерации по элементам, например, в map() или filter(). В таких функциях не нужен return.
def f1(a, b):
return a + b
x = lambda a, b: a + b
print(f1(1, 2)) # 3
print(x(1, 2)) # 3
Изменим последнюю задачу, добавим в нее лямбда-функцию:
dictionary = {'a': 5, 'b': 2, 'c': 4, 'd': 1, 'e': 3, 'f': 0}
keys_and_values = dictionary.items() # [('a', 5), ('b', 2), ('c', 4), ('d', 1), ('e', 3), ('f', 0)]
print(sorted(keys_and_values, key=lambda t: t[1]))
# [('f', 0), ('d', 1), ('b', 2), ('e', 3), ('c', 4), ('a', 5)]
Префиксные операторы
У них много юзкейсов:
- Использование
***для передачи аргументов в функцию; - Использование
**и*для сбора переданных в функцию аргументов; - Использование
*для принятия только именованных аргументов; - Использование
**при распаковке кортежей; - Использование
**для распаковки итерируемых объектов в список/кортеж; - Использование
*для распаковки словарей в другие словари.
Рассмотрим основные:
Звёздочки для распаковки в аргументы функции
При вызове функции можно использовать оператор * для распаковки итерируемого объекта в аргументы вызова:
fruits = ['lemon', 'pear', 'watermelon', 'tomato']
print(fruits[0], fruits[1], fruits[2], fruits[3]) # lemon pear watermelon tomato
print(*fruits) # lemon pear watermelon tomato
Строка print(*fruits) передаёт все элементы списка fruits в вызов print()как отдельные аргументы, поэтому нам даже не нужно знать, сколько элементов в списке.
Здесь оператор * — не просто синтаксический. Без фиксированной длины списка было бы невозможно передать элементы итерируемого объекта как отдельные аргументы, не используя *.
Оператор **делает что-то похожее, только с именованными аргументами. Он позволяет взять словарь с парами ключ-значение и распаковать его в именованные аргументы в вызове функции:
date_info = {'year': "2022", 'month': "12", 'day': "09"}
filename = "{year}-{month}-{day}.txt".format(**date_info) # '2022-12-09.txt'
Звёздочки для упаковки аргументов, переданных в функцию
При определении функции можно использовать * , чтобы собрать переменное количество позиционных аргументов, переданных в функцию. Они помещаются в кортеж:
def f(*x):
return x
print(f(1)) # (1,)
print(f(1, 2)) # (1, 2)
Эта функция принимает любое количество аргументов.
Звёздочки для распаковки
fruits = ['lemon', 'pear', 'watermelon', 'tomato']
first, second, *remaining = fruits
print(remaining) # ['watermelon', 'tomato']
first, *remaining = fruits
print(remaining) # ['pear', 'watermelon', 'tomato']
first, *middle, last = fruits
print(middle) # ['pear', 'watermelon']
print(*fruits) # lemon pear watermelon tomato
Классы
Здесь немного про классы. Подробная инфа в следующем разделе (ссылка внизу).
Класс определяется с помощью ключевого слова class:
class название_класса:
атрибуты_класса
методы_класса
Например:
class Person:
pass
В данном случае определен класс Person, который условно представляет человека. В данном случае в классе не определяется никаких методов или атрибутов. Однако поскольку в нем должно быть что-то определено, то в качестве заменителя функционала класса применяется оператор pass. Этот оператор применяется, когда синтаксически необходимо определить некоторый код, однако мы не хотим его, и вместо конкретного кода вставляем оператор pass.
После создания класса можно определить объекты этого класса. Например:
class Person:
pass
tom = Person() # определение объекта tom
bob = Person() # определение объекта bob
После определения класса Person создаются два объекта класса Person - tom и bob. Для создания объекта применяется специальная функция - конструктор, которая называется по имени класса и которая возвращает объект класса. То есть в данном случае вызов Person()представляет вызов конструктора. Каждый класс по умолчанию имеет конструктор без параметров. Person() - вызов конструктора, который возвращает объект класса Person.
Методы классов
Методы класса фактически представляют функции, которые определенны внутри класса и которые определяют его поведение. Например, определим класс Person с одним методом:
class Person: # определение класса Person
def say_hello(self):
print("Привет")
tom = Person()
tom.say_hello() # Привет
Здесь определен метод say_hello(), который условно выполняет приветствие - выводит строку. При определении методов любого класса следует учитывать, что все они должны принимать в качестве первого параметра ссылку на текущий объект, который называется self. Через эту ссылку внутри класса мы можем обратиться к функциональности текущего объекта. Но при самом вызове метода этот параметр не учитывается.
Используя имя объекта, мы можем обратиться к его методам. Для обращения к методам применяется нотация точки - после имени объекта ставится точка и после нее идет вызов метода: объект.метод([параметры метода])
Если метод должен принимать другие параметры, то они определяются после параметра self, и при вызове подобного метода для них необходимо передать значения:
class Person:
def say(self, message): # метод
print(message)
tom = Person()
tom.say("Добрый вечер") # Добрый вечер
Здесь определен метод say(). Он принимает два параметра: self и message. И для второго параметра - message при вызове метода необходимо передать значение.
self
Через ключевое слово self можно обращаться внутри класса к функциональности текущего объекта:
self.атрибут # обращение к атрибуту
self.метод # обращение к методу
Например, определим два метода в классе Person:
class Person:
def say(self, message):
print(message)
def say_hello(self):
self.say("Привет") # обращаемся к выше определенному методу say
tom = Person()
tom.say_hello() # Привет
Конструкторы
Для создания объекта класса используется конструктор. Так, выше когда мы создавали объекты класса Person, мы использовали конструктор по умолчанию, который не принимает параметров и который неявно имеют все классы: tom = Person().
Мы можем явным образом определить в классах конструктор с помощью специального метода, который называется **__init__()**. К примеру, изменим класс Person, добавив в него конструктор:
class Person:
# конструктор
def __init__(self):
print("Создание объекта Person")
def say_hello(self):
print("Привет")
tom = Person() # Создание объекта Person
tom.say_hello() # Привет
Теперь при создании объекта будет производится вызов конструктора __init__()
из класса
Person, который выведет на консоль строку "Создание объекта Person".
Атрибуты объекта
Атрибуты хранят состояние объекта. Для определения и установки атрибутов внутри класса можно применять слово self. Например, определим следующий класс Person:
class Person:
def __init__(self, name):
self.name = name # имя человека
self.age = 18 # возраст человека
tom = Person("Tom")
# обращение к атрибутам
# получение значений
print(tom.name) # Tom
print(tom.age) # 1
# изменение значения
tom.age = 37
print(tom.age) # 37
Если мы определили в классе конструктор init, мы уже не сможем вызвать конструктор по умолчанию. Теперь нам надо вызывать опреледеленный конструктор init, в который необходимо передать значение для параметра name: tom = Person("Tom").
В принципе нам необязательно определять атрибуты внутри класса. Пайтон позволяет сделать это динамически вне класса:
class Person:
def __init__(self, name):
self.name = name # имя
self.age = 18 # возраст
tom = Person("Tom")
tom.company = "Yandex"
print(tom.company) # Yandex
Для обращения к атрибутам объекта внутри класса в его методах также применяется слово self:
class Person:
def __init__(self, name):
self.name = name # имя
self.age = 18 # возраст
def display_info(self):
print(f"Name: {self.name} Age: {self.age}")
tom = Person("Tom")
tom.display_info() # Name: Tom Age: 18
Создание объектов
Подобным образом можно создавать и другие объекты класса:
class Person:
def __init__(self, name):
self.name = name # имя
self.age = 18 # возраст
def display_info(self):
print(f"Name: {self.name} Age: {self.age}")
tom = Person("Tom")
tom.age = 20
tom.display_info() # Name: Tom Age: 20
bob = Person("Bob")
bob.age = 30
bob.display_info() # Name: Bob Age: 30
Здесь создаются два объекта класса Person: tom и bob. Они соответствуют определению класса Person, имеют одинаковый набор атрибутов и методов, однако их состояние будет отличаться.
При выполнении программы Python динамически будет определять self - он представляет объект, у которого вызывается метод.
Модули
Про работу со встроенными модулями math, itertools, functools… рассказывать не буду. Поговорим о том, как создавать свои.
Создадим файл message.py:
hello = "Hello everyone"
def print_message(text):
print(f"Message: {text}")
Для использования модуля его надо импортировать с помощью оператора import, после которого указывается имя модуля: import message.
Чтобы обращаться к функциональности модуля, нам нужно получить его пространство имен. По умолчанию оно будет совпадать с именем модуля, то есть в нашем случае также будет называться message.
Получив пространство имен модуля, мы сможем обратиться к его функциям по схеме
пространство_имен.функция
В основном файле программы используем данный модуль:
import message # подключаем модуль message
# выводим значение переменной hello
print(message.hello) # Hello everyone
# обращаемся к функции print_message
message.print_message("Hello work") # Message: Hello work
Подключение функциональности модуля в глобальное пространство имен
Другой вариант настройки предполагает импорт функциональности модуля в глобальное пространство имен текущего модуля с помощью ключевого слова from:
from message import print_message
# обращаемся к функии print_message из модуля message
print_message("Hello work") # Message: Hello work
# переменная hello из модуля message не доступна, так как она не импортирована
Если необходимо импортировать в глобальное пространство имен весь функционал, то вместо названий отдельных функций и переменных можно использовать *****:
from message import *
print_message("Hello work") # Message: Hello work
print(hello) # Hello everyone
Псевдонимы (алиасы)
При импорте модуля и его функциональности мы можем установить для них псевдонимы. Для этого применяется ключевое слово as, после которого указывается псевдоним.
import message as m # модуль message ассоциируется с псевдонимом m
# выводим значение переменной hello
print(m.hello) # Hello everyone
# обращаемся к функции print_message
m.print_message("Hello work") # Message: Hello work
Подобным образом можно установить псевдонимы для отдельной функциональности модуля:
from message import print_message as display
from message import hello as welcome
print(welcome) # Hello everyone - переменная hello из модуля message
display("Hello work") # Message: Hello work - функция print_message из модуля message
Имена модулей
В примере выше файл, который является главным, использует модуль message.py. При запуске главного файла программа выполнит всю необходимую работу. Однако, если мы запустим отдельно модуль message.py сам по себе, то ничего в консоли не увидим. Ведь модуль message просто определяет функцию и переменную и не выполняет никаких других действий. Но мы можем сделать так, чтобы модуль message.py мог использоваться как сам по себе, так и подключаться в другие модули.
При выполнении модуля среда определяет его имя и присваивает его глобальной переменной __name__. Если модуль является запускаемым, то его имя равно __main__. Если модуль используется в другом модуле, то в момент выполнения его имя аналогично названию файла без расширения .py. И мы можем это использовать. Изменим содержимое файла message.py:
hello = "Hello everyone"
def print_message(text):
print(f"Message: {text}")
def main():
print_message(hello)
if __name__ == "__main__":
main()
В данном случае в модуль message.py добавлена функция main. И мы можем сразу запустить файл message.py отдельно от всех и протестировать код.
Следует обратить внимание на вызов функции main: if __name__ == "__main__": main()
Переменная __name__ указывает на имя модуля. Для главного модуля, который непосредственно запускается, эта переменная всегда будет иметь значение __main__ вне зависимости от имени файла.
Поэтому, если запускать скрипт message.py отдельно, сам по себе, то Пайтон присвоит переменной __name__ значение __main__, далее в выражении if вызовет функцию main из этого же файла.
Однако если мы будем запускать другой скрипт, а этот - message.py - будем подключать в качестве вспомогательного, для message.py переменная __name__ будет иметь значение message. И соответственно метод main в файле message.py не будет работать.
Данный подход с проверкой имени модуля является более рекомендуемым подходом, чем просто вызов метода main.
Генераторы
Генераторы - это те же функции, но возвращающие результаты сразу после их получения. Это позволяет выигрывать в памяти и времени работы программы.
Обычная функция выглядит так:
def f(x, y):
res = []
for i in range(x, y):
res.append(i)
return res
print(f(1, 10)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Функция запускает цикл от 1 до 9 и записывает в список все числа, после чего возвращает этот список. Сначала заполнила список, потом отдала.
Перепишем эту функцию через генератор с помощью ключевого слова yield, вместоreturn:
def f(x, y):
for i in range(x, y):
yield i
# генератор вернет generator object, поэтому распакуем его
print(*f(1, 10)) # 1 2 3 4 5 6 7 8 9
В данном случает функция тоже запустила цикл, но возвращала числа по мере их обработки. Мы не сохраняли их никуда, а сразу передавали.
Можно получить следующее значение генератора, используя функцию next():
def f(x, y):
for i in range(x, y):
yield i
x = f(1, 10)
print(next(x)) # 1
print(next(x)) # 2
Длину объекта генератора получить нельзя, потому что она всегда динамическая.
Так же, генераторами называют циклы, записанные в строку, но это как-то странно:
x = [i for i in range(1, 10]
# генератор :)
Обработка исключений
Для обработки исключений используется конструкция **try - except**.
В блоке try указывается код, который попытается выполнится, а в except - то, что будет, если возникнет ошибка.
try:
x = 1 / 0
except:
x = 0
print(x) # 0
Делить на ноль нельзя, поэтому возникнет исключение (ошибка). Компилятор будет искать обработчик и наткнется на except.
Мы можем обрабатывать конкретный вид ошибки. С полным списком можно ознакомиться здесь. Например, деление на ноль выдаст ZeroDivisionError. Обработаем ее:
try:
x = 1 / 0
except ZeroDivisionError:
x = 0
print(x) # 0
Здесь конкретно указан обработчик для ZeroDivisionError. Ошибки других типов пойманы не будут. Для упрощения отладки все-таки указывают конкретные виды ошибок, чтобы сразу понять, что вызвало прерывание работы программы.
Если расширять тему обработки исключений, нужно упомянуть про else и finally.
else выполнится, если исключения не было, и все выполнилось как нужно. finally выполнится в любом случае.
try:
x = 1 / 0
except ZeroDivisionError:
x = 0
else:
print('Все окей')
finally:
print('Была ошибка, не было ошибки - я так и так выведу эту строку')
print(x) # 0
Регулярные выражения
Регулярки - это вообще лучшее, что есть в программировании. Они предоставляют безграничную валидацию любой строки, которую пользователь передаст программе. Это фантастика просто.
Учить регуляркам я не буду. Это долго и скучно, НО СТОИТ ТОГО! Посмотрите вот это:
- https://regexlearn.com - самый лучший интерактивный гайд
- https://regex101.com - удобный тестер регулярок
Для работы с регулярными выражениями в Пайтоне используется встроенный модуль re.
Основные функции Regex
regexp - сокращение от regular expression (регулярное выражение).
Подавляющее большинство функций re принимают в качестве аргументов само выражение и строку.
re.split(regexp, string) # сплитит строку по заданному шаблону
re.match(regexp, string) # находит вхождение фрагмента в начале строки
re.search(regexp, ststringoke) # находит первое вхождение фрагмента в любом месте и возвращает объект match
re.span(regexp, string) # возвращает кортеж, содержащий начальную и конечную позиции искомого фрагмента
re.string # вернет строку, переданную в функцию re.search()
re.<re_method>.group() # возвращает фрагмент строки, в котором было обнаружено совпадение
re.findall(regexp, string) # находит все вхождения фрагмента, в любом месте
re.sub(regexp, string) # заменяет фрагмент в соответствии с шаблоном
re.compile(regexp, string) # создает объект из регулярного выражения
import re
print(re.findall(r'[0-9]', '1a2b3c4d')) # ['1', '2', '3', '4']
print(re.match(r'[a-z]', '1a2b3c4d')) # None
Декораторы
Кастомные декораторы, как правило, не пользуются особой популярностью. По сути, это функция, которая оборачивает другую функцию.
Декораторы являются неотъемлемой частью разных библиотек и фреймворков, но если реально хочется попробовать написать свои, почитайте это 😄 Все последовательно и доступно.
На этом, как бы, Пайтон заканчивается, ну или я что-то посчитал не особо важным. Предлагаю ООП на Python.