Ультимейт-курс по 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:

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:

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

Регулярные выражения

Регулярки - это вообще лучшее, что есть в программировании. Они предоставляют безграничную валидацию любой строки, которую пользователь передаст программе. Это фантастика просто.
Учить регуляркам я не буду. Это долго и скучно, НО СТОИТ ТОГО! Посмотрите вот это:

Для работы с регулярными выражениями в Пайтоне используется встроенный модуль 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.