Skip to content

Latest commit

 

History

History
2215 lines (1613 loc) · 152 KB

Python.md

File metadata and controls

2215 lines (1613 loc) · 152 KB

Python

Содержание:

Разница 2 и 3 версии python

В Python 2 print был оператором: print "Hello, world" В Python 3 print - функция: print ("Hello, world")

В Python 2 были две функции: range - возвращает список; xrange - возвращает итератор
В Python 3 есть только функция range, и она возвращает итератор

В Python 2 при делении целых чисел возвращает целоче число
В Python 3 при делении целых чисел возвращает вещественное число

Магические методы

  • Так как в 3 Питоне различий между строкой и юникодом больше нет, __unicode__ исчез, а появился __bytes__ (который ведёт себя так же как __str__ и __unicode__ в 2.7) для новых встроенных функций построения байтовых массивов.
  • Так как деление в 3 Питоне теперь по-умолчанию «правильное деление», __div__ больше нет.
  • __coerce__ больше нет, из-за избыточности и странного поведения.
  • __cmp__ больше нет, из-за избыточности.
  • __nonzero__ было переименовано в __bool__ .
  • next у итераторов был переименован в __next__ .

ООП

(наверх)

SOLID

S - Принцип единственной ответственности (single responsibility principle)
Для каждого класса должно быть определено единственное назначение. Не должно возникать God object, который занимается всем в программе.

O - Принцип открытости/закрытости (open–closed principle)
«программные сущности … должны быть открыты для расширения, но закрыты для модификации». Мы должны иметь возможность добавлять функциональность.

L - Принцип подстановки Барбары Лисков (Liskov substitution principle)
«объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы». У объекта есть тип, но сами классы выстраиваются в иерархию классов. Подтип типа всю старую функциональность должен выполнять.

I - Принцип разделения интерфейса (interface segregation principle)
«много интерфейсов, специально предназначенных для клиентов, лучше, чем один интерфейс общего назначения». Интерфейс - способ провзаимодействовать с какой-то программной действующей функцией.

D - Принцип инверсии зависимостей (dependency inversion principle) «Зависимость на Абстракциях. Нет зависимости на что-то конкретное».

Что такое классы?

(наверх)

Класс — тип, описывающий устройство объектов. Объект — это экземпляр класса.

class C: 
  pass
имя_объекта = имя_класса()

У класса может не быть тела.

Простейший пример класса:

class Rectangle:
  default_color = "green" # статический атрибут
  def __init__(self, width, height): # конструктор класса
    self.width = width # динамический атрибут
    self.height = height # динамический атрибут

В python нет возможности сделать несколько конструкторов.

Магические методы

(наверх)

Если какой-то идентификатор начинается с двух подчёркиваний, дальше пишется что-либо, потом снова два подчёркивания, то это спец метод.

Какие магические методы и для чего используются?

class FileObject:
  def __init__(self, filepath='~', filename='sample.txt'): # Обёртка для файлового объекта, чтобы быть уверенным в том, что файл будет закрыт при удалении.
    self.file = open(join(filepath, filename), 'r+') # Открыть файл filename в filepath в режиме чтения и записи
  def __del__(self):
    self.file.close()
    del self.file

Метод, который будет вызван при инициализации объекта.
Это первый метод, который будет вызван при инициализации объекта. Он принимает в качестве параметров класс и потом любые другие аргументы, которые будут переданы в __init__ . __new__ используется весьма редко, но иногда бывает полезен, в частности, когда класс наследуется от неизменяемого (immutable) типа, такого как кортеж (tuple) или строка.

__new__(cls, [...])

Инициализатор класса. Самый базовый магический метод, __init__ . С его помощью мы можем инициализировать объект.

__init__(self, [...]) 

Деструктор объекта

__del__

Самый базовый из методов сравнения __cmp__(self, other) . Он, в действительности, определяет поведение для всех операторов сравнения (>, ==, !=, итд.), но не всегда так, как вам это нужно (например, если эквивалентность двух экземпляров определяется по одному критерию, а то что один больше другого по какому-нибудь другому). __cmp__ должен вернуть отрицательное число, если self < other , ноль, если self == other , и положительное число в случае self > other . Но, обычно, лучше определить каждое сравнение, которое вам нужно, чем определять их всех в __cmp__ . Но __cmp__ может быть хорошим способом избежать повторений и увеличить ясность, когда все необходимые сравнения оперируют одним критерием.

  • __eq__(self, other) Определяет поведение оператора равенства, ==.

  • __ne__(self, other) Определяет поведение оператора неравенства, !=.

  • __lt__(self, other) Определяет поведение оператора меньше, <.

  • __gt__(self, other) Определяет поведение оператора больше, >.

  • __le__(self, other) Определяет поведение оператора меньше или равно, <=.

  • __ge__(self, other) Определяет поведение оператора больше или равно, >=.

Унарные операторы и функции

Унарные операторы и функции имеют только один операнд — отрицание, абсолютное значение, и так далее.

  • __pos__(self) Определяет поведение для унарного плюса (+some_object)

  • __neg__(self) Определяет поведение для отрицания(-some_object)

  • __abs__(self) Определяет поведение для встроенной функции abs().

  • __invert__(self) Определяет поведение для инвертирования оператором ~. Для объяснения что он делает смотри статью в Википедии о бинарных операторах.

  • __round__(self, n) Определяет поведение для встроенной функции round(). n это число знаков после запятой, до которого округлить.

  • __floor__(self) Определяет поведение для math.floor(), то есть, округления до ближайшего меньшего целого.

  • __ceil__(self) Определяет поведение для math.ceil(), то есть, округления до ближайшего большего целого.

  • __trunc__(self) Определяет поведение для math.trunc(), то есть, обрезания до целого.

Обычные арифметические операторы

  • __add__(self, other) Сложение.

  • __sub__(self, other) Вычитание.

  • __mul__(self, other) Умножение.

  • __floordiv__(self, other) Целочисленное деление, оператор //.

  • __div__(self, other) Деление, оператор /.

  • __truediv__(self, other) Правильное деление. Заметьте, что это работает только когда используется from __future__ import division.

  • __mod__(self, other) Остаток от деления, оператор %.

  • __divmod__(self, other) Определяет поведение для встроенной функции divmod().

  • __pow__ Возведение в степень, оператор **.

  • __lshift__(self, other) Двоичный сдвиг влево, оператор <<.

  • __rshift__(self, other) Двоичный сдвиг вправо, оператор >>.

  • __and__(self, other) Двоичное И, оператор &.

  • __or__(self, other) Двоичное ИЛИ, оператор |.

  • __xor__(self, other) Двоичный xor, оператор ^.

Магические методы преобразования типов

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

  • __int__(self) Преобразование типа в int.

  • __long__(self) Преобразование типа в long.

  • __float__(self) Преобразование типа в float.

  • __complex__(self) Преобразование типа в комплексное число.

  • __oct__(self) Преобразование типа в восьмеричное число.

  • __hex__(self) Преобразование типа в шестнадцатеричное число.

  • __index__(self) Преобразование типа к int, когда объект используется в срезах (выражения вида [start:stop:step]). Если вы определяете свой числовой тип, который может использоваться как индекс списка, вы должны определить __index__.

  • __trunc__(self) Вызывается при math.trunc(self). Должен вернуть своё значение, обрезанное до целочисленного типа (обычно long).

  • __coerce__(self, other) Метод для реализации арифметики с операндами разных типов. __coerce__ должен вернуть None если преобразование типов невозможно. Если преобразование возможно, он должен вернуть пару (кортеж из 2-х элементов) из self и other, преобразованные к одному типу.

Представление своих классов

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

  • __str__(self) Определяет поведение функции str(), вызванной для экземпляра вашего класса.

  • __repr__(self) Определяет поведение функции repr(), вызванной для экземпляра вашего класса. Главное отличие от str() в целевой аудитории. repr() больше предназначен для машинно-ориентированного вывода (более того, это часто должен быть валидный код на Питоне), а str() предназначен для чтения людьми.

  • __unicode__(self) Определяет поведение функции unicode(), вызванной для экземпляра вашего класса. unicode() похож на str(), но возвращает строку в юникоде. Если клиент вызывает str() на экземпляре вашего класса, а вы определили только __unicode__(), то это не будет работать. Постарайтесь всегда определять __str__() для случая, когда кто-то не имеет такой роскоши как юникод.

  • __format__(self, formatstr) Определяет поведение, когда экземпляр вашего класса используется в форматировании строк нового стиля. Например, "Hello, {0:abc}!".format(a) приведёт к вызову a.__format__("abc"). Это может быть полезно для определения ваших собственных числовых или строковых типов, которым вы можете захотеть предоставить какие-нибудь специальные опции форматирования.

  • __hash__(self) Определяет поведение функции hash(), вызванной для экземпляра вашего класса. Метод должен возвращать целочисленное значение, которое будет использоваться для быстрого сравнения ключей в словарях. Заметьте, что в таком случае обычно нужно определять и __eq__ тоже. Руководствуйтесь следующим правилом: a == b подразумевает hash(a) == hash(b).

  • __nonzero__(self) Определяет поведение функции bool(), вызванной для экземпляра вашего класса. Должна вернуть True или False, в зависимости от того, когда вы считаете экземпляр соответствующим True или False.

  • __dir__(self) Определяет поведение функции dir(), вызванной на экземпляре вашего класса. Этот метод должен возвращать пользователю список атрибутов. Обычно, определение __dir__ не требуется, но может быть жизненно важно для интерактивного использования вашего класса, если вы переопределили __getattr__ или __getattribute__.

  • __sizeof__(self) Определяет поведение функции sys.getsizeof(), вызванной на экземпляре вашего класса. Метод должен вернуть размер вашего объекта в байтах.

Магия контейнеров

Магические методы, используемые контейнерами.

  • __len__(self) Возвращает количество элементов в контейнере. Часть протоколов для изменяемого и неизменяемого контейнеров.

  • __getitem__(self, key) Определяет поведение при доступе к элементу, используя синтаксис self[key]. Тоже относится и к протоколу изменяемых и к протоколу неизменяемых контейнеров. Должен выбрасывать соответствующие исключения: TypeError если неправильный тип ключа и KeyError если ключу не соответствует никакого значения.

  • __setitem__(self, key, value) Определяет поведение при присваивании значения элементу, используя синтаксис self[nkey] = value. Часть протокола изменяемого контейнера. Опять же, вы должны выбрасывать KeyError и TypeError в соответствующих случаях.

  • __delitem__(self, key) Определяет поведение при удалении элемента (то есть del self[key]). Это часть только протокола для изменяемого контейнера. Вы должны выбрасывать соответствующее исключение, если ключ некорректен.

  • __iter__(self) Должен вернуть итератор для контейнера. Итераторы возвращаются в множестве ситуаций, главным образом для встроенной функции iter() и в случае перебора элементов контейнера выражением for x in container:. Итераторы сами по себе объекты и они тоже должны определять метод __iter__, который возвращает self.

  • __reversed__(self) Вызывается чтобы определить поведения для встроенной функции reversed(). Должен вернуть обратную версию последовательности. Реализуйте метод только если класс упорядоченный, как список или кортеж.

  • __contains__(self, item) __contains__ предназначен для проверки принадлежности элемента с помощью in и not in. Вы спросите, почему же это не часть протокола последовательности? Потому что когда __contains__ не определён, Питон просто перебирает всю последовательность элемент за элементом и возвращает True если находит нужный.

  • __missing__(self, key) __missing__ используется при наследовании от dict. Определяет поведение для для каждого случая, когда пытаются получить элемент по несуществующему ключу (так, например, если у меня есть словарь d и я пишу d["george"] когда "george" не является ключом в словаре, вызывается d.__missing__("george")).

Как вызывать магические методы

Магический метод Когда он вызывается (пример) Объяснение
__new__(cls [,...]) instance = MyClass(arg1, arg2) __new__ вызывается при создании экземпляра
__init__(self [,...]) instance = MyClass(arg1, arg2) __init__ вызывается при создании экземпляра
__cmp__(self, other) self == other, self > other, etc. Вызывается для любого сравнения
__pos__(self) +self Унарный знак плюса
__neg__(self) -self Унарный знак минуса
__invert__(self) ~self Побитовая инверсия
__index__(self) x[self] Преобразование, когда объект используется как индекс
__nonzero__(self) bool(self), if self: Булевое значение объекта
__getattr__(self, name) self.name # name не определено Пытаются получить несуществующий атрибут
__setattr__(self, name, val) self.name = val Присвоение любому атрибуту
__delattr__(self, name) del self.name Удаление атрибута
__getattribute__(self, name) self.name Получить любой атрибут
__getitem__(self, key) self[key] Получение элемента через индекс
__setitem__(self, key, val) self[key] = val Присвоение элементу через индекс
__delitem__(self, key) del self[key] Удаление элемента через индекс
__iter__(self) for x in self Итерация
__contains__(self, value) value in self, value not in self Проверка принадлежности с помощью in
__call__(self [,...]) self(args) «Вызов» экземпляра
__enter__(self) with self as x: with оператор менеджеров контекста
__exit__(self, exc, val, trace) with self as x: with оператор менеджеров контекста
__getstate__(self) pickle.dump(pkl_file, self) Сериализация
__setstate__(self) data = pickle.load(pkl_file) Сериализация

Что такое миксин?

(наверх)

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

Что такое наследование классов и как его использовать?

(наверх)

Наследование классов в объектно-ориентированном программировании позволяет создавать новые классы на основе уже существующих классов. Это позволяет наследующему классу наследовать атрибуты (переменные и методы) от родительского класса, что способствует повторному использованию кода и созданию иерархии классов.

Для создания наследования классов в Python используется следующий синтаксис:

class РодительскийКласс:
    # Атрибуты и методы родительского класса

class НаследующийКласс(РодительскийКласс):
    # Атрибуты и методы наследующего класса

В примере выше класс НаследующийКласс наследует атрибуты и методы от класса РодительскийКласс. Это означает, что НаследующийКласс может использовать все публичные атрибуты и методы, определенные в РодительскомКлассе, а также добавлять свои собственные атрибуты и методы.

Пример кода:

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

    def sound(self):
        pass  # Абстрактный метод, будет переопределен в дочерних классах

class Dog(Animal):
    def sound(self):
        return "Woof!"

class Cat(Animal):
    def sound(self):
        return "Meow!"

dog = Dog("Buddy")
print(dog.name)  # Выводит "Buddy"
print(dog.sound())  # Выводит "Woof!"

cat = Cat("Kitty")
print(cat.name)  # Выводит "Kitty"
print(cat.sound())  # Выводит "Meow!"

В этом примере Animal является родительским классом, а Dog и Cat являются наследующими классами. У них есть общий атрибут name, наследуемый от Animal, и переопределенный метод sound(). Созданные объекты классов Dog и Cat могут использовать как унаследованный атрибут name, так и метод sound(), соответствующий каждому классу.

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

Что такое полиморфизм и как его реализовать в python?

(наверх)

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

В Python полиморфизм достигается с помощью наследования и переопределения методов, а также с помощью использования механизма динамической типизации языка.

Пример полиморфизма в Python:

class Animal:
    def sound(self):
        pass

class Dog(Animal):
    def sound(self):
        return "Woof!"

class Cat(Animal):
    def sound(self):
        return "Meow!"

class Cow(Animal):
    def sound(self):
        return "Moo!"

def make_sound(animal):
    print(animal.sound())

dog = Dog()
cat = Cat()
cow = Cow()

make_sound(dog)  # Выводит "Woof!"
make_sound(cat)  # Выводит "Meow!"
make_sound(cow)  # Выводит "Moo!"

Как работает инкапсуляция в python?

(наверх)

Инкапсуляция в объектно-ориентированном программировании (ООП) - это механизм, который позволяет объединить данные (переменные) и методы, работающие с этими данными, в единый объект, скрывая внутреннюю реализацию от внешнего кода. Один из основных принципов ООП - скрытие деталей реализации и предоставление только интерфейса для взаимодействия с объектом.

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

Примеры инкапсуляции в Python:

  1. Использование одиночного подчеркивания _ перед именем переменной или метода. Это соглашение указывает на то, что переменная или метод предназначены для внутреннего использования и не должны быть использованы вне класса или его наследников:
class MyClass:
   def __init__(self):
       self._internal_var = 42

   def _internal_method(self):
       return "This is an internal method."

obj = MyClass()
print(obj._internal_var)  # Выводит 42
print(obj._internal_method())  # Выводит "This is an internal method."

В данном примере переменная _internal_var и метод _internal_method() считаются внутренними для класса MyClass, и хотя они доступны извне, они не рекомендуется использовать в других частях программы.

  1. Использование двойного подчеркивания __ перед именем переменной или метода. Это создает механизм именования, называемый "name mangling", который делает переменные и методы недоступными для прямого доступа извне класса:
class MyClass:
   def __init__(self):
       self.__private_var = 42

   def __private_method(self):
       return "This is a private method."

obj = MyClass()
print(obj.__private_var)  # Вызывает ошибку AttributeError
print(obj.__private_method())  # Вызывает ошибку AttributeError

В данном примере переменная __private_var и метод __private_method() являются приватными для класса MyClass и недоступны для прямого доступа извне класса.

Таким образом, инкапсуляция в Python достигается с помощью использования соглашений об именовании и специальных атрибутов. Однако, следует отметить, что в Python инкапсуляция является соглашением и не является полностью принудительной. Разработчики могут обойти эти соглашения и получить доступ к "скрытым" переменным и методам, но это считается плохой практикой, так как нарушает принципы ООП и может нарушить целостность класса.

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

(наверх)

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

Пример реализации инкапсуляции с использованием только публичных методов:

class MyClass:
    def __init__(self):
        self._private_var = 42

    def get_private_var(self):
        return self._private_var

    def set_private_var(self, value):
        # Дополнительные проверки или логика можно добавить здесь
        self._private_var = value

obj = MyClass()
print(obj.get_private_var())  # Выводит 42

obj.set_private_var(10)
print(obj.get_private_var())  # Выводит 10

В этом примере переменная _private_var считается внутренней для класса MyClass и не доступна непосредственно извне. Вместо этого, класс предоставляет публичные методы get_private_var() и set_private_var(), которые позволяют получать значение и устанавливать новое значение внутренней переменной _private_var.

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

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

Что такое дескрипторы и как они используются в Python?

(наверх)

Дескрипторы в Python - это механизм, который позволяет контролировать доступ к атрибутам класса. Они определяются с помощью специальных методов, таких как __get__(), __set__(), и __delete__(), которые определяют поведение при доступе, установке или удалении значения атрибута.

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

Примеры использования дескрипторов в Python:

  1. Дескриптор для доступа к атрибуту:
class Descriptor:
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        instance._value = value

class MyClass:
    attribute = Descriptor()

obj = MyClass()
obj.attribute = 42
print(obj.attribute)  # Выводит 42

В этом примере Descriptor является дескриптором, определяющим доступ к атрибуту attribute класса MyClass. При установке значения атрибута (obj.attribute = 42) вызывается метод __set__() дескриптора, который устанавливает значение _value в экземпляре класса. При чтении значения атрибута (print(obj.attribute)) вызывается метод __get__(), который возвращает значение _value из экземпляра класса.

  1. Дескриптор для выполнения дополнительной логики при доступе к атрибуту:
class Descriptor:
    def __get__(self, instance, owner):
        print("Getting attribute")
        return instance._value

    def __set__(self, instance, value):
        print("Setting attribute")
        instance._value = value

    def __delete__(self, instance):
        print("Deleting attribute")
        del instance._value

class MyClass:
    attribute = Descriptor()

obj = MyClass()
obj.attribute = 42  # Выводит "Setting attribute"
print(obj.attribute)  # Выводит "Getting attribute" и 42
del obj.attribute  # Выводит "Deleting attribute"

В этом примере дескриптор Descriptor выполняет дополнительные операции при доступе, установке или удалении значения атрибута. Когда значение устанавливается (obj.attribute = 42), вызывается метод __set__(), который выводит сообщение "Setting attribute". При чтении значения (print(obj.attribute)) вызывается метод __get__(), который выводит сообщение "Getting attribute" и возвращает значение _value. При удалении атрибута (del obj.attribute) вызывается метод __delete__(), который выводит сообщение "Deleting attribute" и удаляет _value.

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

Что такое абстрактные классы и зачем они нужны в Python?

(наверх)

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

Абстрактные классы в Python реализуются с использованием модуля abc (Abstract Base Classes). Модуль abc предоставляет базовый класс ABC и декораторы для создания абстрактных методов и свойств.

Примеры использования абстрактных классов в Python:

  1. Создание абстрактного класса с абстрактным методом:
from abc import ABC, abstractmethod

class AbstractClass(ABC):
    @abstractmethod
    def abstract_method(self):
        pass

class ConcreteClass(AbstractClass):
    def abstract_method(self):
        print("Implementation of abstract_method")

# obj = AbstractClass()  # Вызывает ошибку TypeError, так как абстрактный класс не может быть инстанциирован

obj = ConcreteClass()
obj.abstract_method()  # Выводит "Implementation of abstract_method"

В этом примере AbstractClass является абстрактным классом, определяющим абстрактный метод abstract_method(). Класс ConcreteClass наследуется от AbstractClass и обязан предоставить реализацию для абстрактного метода. Объект класса ConcreteClass может быть создан и вызван абстрактный метод.

  1. Создание абстрактного класса с абстрактным свойством:
from abc import ABC, abstractproperty

class AbstractClass(ABC):
    @abstractproperty
    def abstract_property(self):
        pass

class ConcreteClass(AbstractClass):
    @property
    def abstract_property(self):
        return "Value of abstract_property"

# obj = AbstractClass()  # Вызывает ошибку TypeError, так как абстрактный класс не может быть инстанциирован

obj = ConcreteClass()
print(obj.abstract_property)  # Выводит "Value of abstract_property"

В этом примере AbstractClass является абстрактным классом, определяющим абстрактное свойство abstract_property. Класс ConcreteClass наследуется от AbstractClass и предоставляет реализацию для абстрактного свойства с помощью декоратора @property. Объект класса ConcreteClass может быть создан и доступен к абстрактному свойству.

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

Что такое множественное наследование и какие проблемы могут возникнуть при его использовании в Python?

(наверх)

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

Пример использования множественного наследования:

class BaseClass1:
    def method1(self):
        print("Method 1 from BaseClass1")

class BaseClass2:
    def method2(self):
        print("Method 2 from BaseClass2")

class DerivedClass(BaseClass1, BaseClass2):
    pass

obj = DerivedClass()
obj.method1()  # Выводит "Method 1 from BaseClass1"
obj.method2()  # Выводит "Method 2 from BaseClass2"

В этом примере классы BaseClass1 и BaseClass2 являются родительскими классами, а класс DerivedClass наследует от обоих родительских классов. Это позволяет объекту класса DerivedClass иметь доступ как к методу method1() из BaseClass1, так и к методу method2() из BaseClass2.

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

  1. Проблемы с именованием: Если несколько родительских классов имеют методы с одинаковыми именами, возникает неоднозначность при вызове метода из класса-наследника. Это называется "ромбовидным наследованием" или "проблемой алмаза". В таких случаях необходимо явно указывать, какой метод нужно вызывать.
class BaseClass:
    def method(self):
        print("Method from BaseClass")

class SubClass1(BaseClass):
    def method(self):
        print("Method from SubClass1")

class SubClass2(BaseClass):
    def method(self):
        print("Method from SubClass2")

class DerivedClass(SubClass1, SubClass2):
    pass

obj = DerivedClass()
obj.method()  # Выводит "Method from SubClass1"

В этом примере DerivedClass наследует от SubClass1 и SubClass2, которые оба переопределяют метод method(). При вызове метода method() у объекта DerivedClass будет выполнена реализация из SubClass1, так как SubClass1 указан первым в списке наследования.

  1. Проблемы с дублированием кода: Если несколько родительских классов реализуют одинаковую функциональность, то может возникнуть дублирование кода. Это может привести к сложностям в поддержке и изменении кода.
class BaseClass1:
    def method(self):
        print("Method from BaseClass1")

class BaseClass2:
    def method(self):
        print("Method from BaseClass2")

class DerivedClass(BaseClass1, BaseClass2):
    pass

obj = DerivedClass()
obj.method()  # Выводит "Method from BaseClass1"

В этом примере и BaseClass1, и BaseClass2 имеют метод method(), но при наследовании в DerivedClass используется реализация из BaseClass1. Это может быть неожиданным результатом и может потребовать явного решения о том, какой метод использовать.

  1. Сложность иерархии классов: Множественное наследование может привести к сложности в иерархии классов и усложнить понимание и структурирование кода. Если иерархия классов становится слишком сложной, может быть трудно отследить, откуда берется определенное свойство или метод.

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

Как можно реализовать агрегацию объектов в Python?

(наверх)

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

Пример реализации агрегации объектов в Python:

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

class Course:
    def __init__(self, name, students):
        self.name = name
        self.students = students

# Создание объектов Student
student1 = Student("Alice", 1)
student2 = Student("Bob", 2)
student3 = Student("Charlie", 3)

# Создание объекта Course с агрегацией объектов Student
course = Course("Math", [student1, student2, student3])

# Доступ к агрегированным объектам
for student in course.students:
    print(student.name, student.id)

В этом примере у нас есть классы Student и Course. Класс Student представляет студента с его именем и идентификатором. Класс Course представляет курс с именем и агрегацией объектов Student, которые являются студентами этого курса.

Мы создаем несколько объектов класса Student (student1, student2, student3) и затем создаем объект класса Course (course) с агрегацией этих объектов.

Затем мы можем получить доступ к агрегированным объектам Student через атрибут students объекта Course. В цикле мы выводим имена и идентификаторы каждого студента.

Агрегация объектов позволяет создавать более сложные иерархии и связи между объектами. Это основа для построения более гибких и расширяемых структур данных в объектно-ориентированном программировании.

Что такое примеси (mixins) и как они используются в Python?

(наверх)

Примеси (mixins) в Python - это специальный подход в объектно-ориентированном программировании, который позволяет добавлять дополнительное поведение и функциональность классам без использования наследования.

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

Пример использования примесей в Python:

class LoggableMixin:
    def log(self, message):
        print(f"Logging: {message}")

class Database:
    def save(self):
        print("Saving to the database")

class File:
    def write(self):
        print("Writing to the file")

class User(Database, LoggableMixin):
    def __init__(self, name):
        self.name = name

user = User("Alice")
user.save()  # Вызов метода из класса Database
user.log("User created")  # Вызов метода из примеси LoggableMixin

В этом примере у нас есть примесь LoggableMixin, которая содержит метод log(), предназначенный для логирования сообщений. Затем у нас есть классы Database и File, которые представляют различные объекты для сохранения данных.

Класс User наследует от класса Database и использует примесь LoggableMixin. Теперь объекты класса User могут использовать методы и свойства как из класса Database, так и из примеси LoggableMixin.

При создании объекта класса User (в данном случае user) мы можем вызывать метод save() из класса Database и метод log() из примеси LoggableMixin.

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

Что такое композиция объектов и как ее можно использовать в Python?

(наверх)

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

Пример использования композиции объектов в Python:

class Engine:
    def start(self):
        print("Engine started")

    def stop(self):
        print("Engine stopped")

class Car:
    def __init__(self):
        self.engine = Engine()  # Композиция объекта Engine

    def start(self):
        print("Car started")
        self.engine.start()

    def stop(self):
        print("Car stopped")
        self.engine.stop()

car = Car()
car.start()  # Запуск автомобиля и двигателя
car.stop()  # Остановка автомобиля и двигателя

В этом примере у нас есть класс Engine, представляющий двигатель, и класс Car, представляющий автомобиль. В классе Car мы создаем объект engine, который представляет двигатель и является компонентом автомобиля.

При вызове метода start() у объекта car, он вызывает метод start() у объекта engine, что приводит к запуску двигателя. Аналогично, при вызове метода stop() у объекта car, он вызывает метод stop() у объекта engine, что приводит к остановке двигателя.

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

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

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

Типы и структуры данных в python

(наверх)

Какие типы и структуры данных бывают в python?

Объект Тип
Строка str
Целое число int
Число с плавающей точкой float
Список list
Кортеж tuple
Словарь dict
Множество set
Логический bool
Функция function
Класс, определяемый пользователем type
Экземпляр класса class
Встроенная функция builtin_function_or_method
type type

Что такое мутабельные и иммутабельные типы данных?

(наверх)

Объекты в питоне бывают двух значительно отличающихся сортов: изменяемые (mutable) и неизменяемые (immutable). Неизменяемыми являются целые и действительные числа (int, float), строки (str), последовательности байтов (бинарные данные, bytes), а также кортежи, все элементы которых неизменяемы (tuple). Напротив, списки (list), словари (dict) и множества (set) являются изменяемыми.

Что может быть в качестве ключа словаря?

(наверх)

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

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

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

(наверх)

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

Одинаковые данные будут иметь одинаковое хеш-значение.

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

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

В чём особенность словаря в python?

(наверх)

Словари в Python - неупорядоченные коллекции произвольных объектов с доступом по ключу. Их иногда ещё называют ассоциативными массивами или хеш-таблицами.

Чтобы работать со словарём, его нужно создать. Сделать это можно несколькими способами:

  • Во-первых, с помощью литерала:
d = {}
print(d)
{}
d = {'dict': 1, 'dictionary': 2}
prtin(d)
{'dict': 1, 'dictionary': 2}
  • Во-вторых, с помощью функции dict:
d = dict(short='dict', long='dictionary')
prtin(d)
{'short': 'dict', 'long': 'dictionary'}
d = dict([(1, 1), (2, 4)])
prtin(d)
{1: 1, 2: 4}
  • В-третьих, с помощью метода fromkeys:
d = dict.fromkeys(['a', 'b'])
prtin(d)
{'a': None, 'b': None}
d = dict.fromkeys(['a', 'b'], 100)
prtin(d)
{'a': 100, 'b': 100}
  • В-четвертых, с помощью генераторов словарей, которые очень похожи на генераторы списков.
d = {a: a ** 2 for a in range(7)}
prtin(d)
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}

Методы словарей

  • dict.clear() - очищает словарь.
  • dict.copy() - возвращает копию словаря.
  • classmethod dict.fromkeys(seq[, value]) - создает словарь с ключами из seq и значением value (по умолчанию None).
  • dict.get(key[, default]) - возвращает значение ключа, но если его нет, не бросает исключение, а возвращает default (по умолчанию None).
  • dict.items() - возвращает пары (ключ, значение).
  • dict.keys() - возвращает ключи в словаре.
  • dict.pop(key[, default]) - удаляет ключ и возвращает значение. Если ключа нет, возвращает default (по умолчанию бросает исключение).
  • dict.popitem() - удаляет и возвращает пару (ключ, значение). Если словарь пуст, бросает исключение KeyError. Важно помнить, что словари неупорядочены.
  • dict.setdefault(key[, default]) - возвращает значение ключа, но если его нет, не бросает исключение, а создает ключ со значением default (по умолчанию None).
  • dict.update([other]) - обновляет словарь, добавляя пары (ключ, значение) из other. Существующие ключи перезаписываются. Возвращает None (не новый словарь!).
  • dict.values() - возвращает значения в словаре.

Списки, кортежи и множества в чём их отличие?

(наверх)

List (список)
Базовая структура данных в python. Элементы в списке хранятся последовательно, каждому из них присвоены индексы, начиная с нуля. В отличие от массива, список может хранить объекты любого типа.

Создание списка

my_list = [] # Создание пустого списка с помощью литерала списка
my_list = list() # Создание пустого списка с помощью встроенной функции

my_list = [1,2,['a','b'],4,5] # Инициализация списка

my_list = list('hello world') # Создание списка из итерируемого объекта
print(my_list)
['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']

my_list = [x for x in range(10)] # Генератор списков в действии
print(my_list)

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Доступные методы

  • my_list.append(x) - добавляет x в конец списка
  • my_list.clear() - очищает
  • my_list.copy() - возвращает копию списка my_list
  • my_list.count(x) - возвращает кол-во элементов со значением x
  • my_list.extend(x) - добавляет элементы списка x к концу списка my_list
  • my_list.index(x,start,end) - возвращает индекс первого найденного x, можно задать промежуток для поиска (опционально)
  • my_list.insert(index, x) - вставляет x на заданную позицию
  • my_list.pop(index) - возвращает элемент с указанным индексом и удаляет его, если индекс не указан - возвращается и удаляется последний элемент
  • my_list.remove(x) - удаляет первый элемент со значением x
  • my_list.reverse() - инвертирует порядок элементов в списке
  • my_list.sort(key=x) сортирует список на основе функции x

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

Tuple (кортёж)
Кортеж - это неизменяемый и более быстрый аналог списка. Он защищает хранимые данные от непреднамеренных изменений и может использоваться в качестве ключа в словарях (словарь - ассоциативный массив в python).

Создание кортежа.

my_tuple = () # Создание кортежа с помощью литерала
my_tuple = tuple() # Создание кортежа с помощью встроенной функции

my_tuple = (1,2,['a','b'],4,5) # Инициализация кортежа

my_tuple = tuple('hello world') # Создание кортежа из итерируемого объекта
print(my_tuple)
('h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd')

my_tuple = tuple(2**x for x in [0, 1, 2, 3]) # Генератор кортежей
print(my_tuple)
(1, 2, 4, 8)

Доступные методы

  • my_tuple.count(x) - возвращает кол-во элементов со значением x
  • my_tuple.index(x,start,end) - возвращает индекс первого найденного x, можно задать промежуток для поиска (опционально)

В каких случаях использовать? Для хранения данных вместо списка (если они не предполагают изменений).

Set (множество)
Множество - это набор уникальных элементов в случайном порядке (неупорядоченный список). Множества примечательны тем, что операция проверки "принадлежит ли объект множеству" происходит значительно быстрее аналогичных операций в других структурах данных.

Создание множества

my_something = {} # !!! Попытка создать множество при помощи литерала даст нам словарь
type(my_something)
<class 'dict'> 

my_set = set() # Создание при помощи встроенной функции

my_set = {1,2,3,4,5} # Инициализация множества

my_set = set('hello world') # Создания множества из итерируемого объекта
print(my_set)
{'r', 'o', 'e', 'h', 'd', 'w', 'l', ' '}

my_set = {x for x in range(10)} # Генератор множеств
print(my_set)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

Доступные методы

  • my_set.add(x) - добавляет x во множество
  • my_set.difference(x) - возвращает множество элементов my_set, которые не входят во множество x
  • my_set.difference_update(x) - удаляет из множества my_set все элементы, которые входят во множество x
  • my_set.discard(x) - удаляет элемент x из my_set
  • my_set.intersection(x) - возвращает элементы общие для множеств my_set и x
  • my_set.intersection_update(x) - удаляет из множества my_set элементы, которых нет во множестве x
  • my_set.isdisjoint(x) - возвращает true если my_set и x не содержат одинаковых значений
  • my_set.issubset(x) - возвращает true если все элементы my_set входят во множество x
  • my_set.issuperset(x) - возвращает true если все элементы x входят во множество my_set
  • my_set.pop() - возвращает и удаляет первый (на данный момент) элемент множества
  • my_set.remove(x) - удаляет x из множества
  • my_set.symmetric_difference(x) - возвращает все элементы из x и my_set, которые встречаются только в одном из множеств
  • my_set.symmetric_difference_update(x) - обновляет исходное множество таким образом, что оно будет состоять из всех элементов x и my_set, которые встречаются только в одном из множеств
  • my_set.union(x) - возвращает новое множество, состоящее из всех элементов x и my_set
  • my_set.update(x) - добавляет в my_set все элементы x

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

Как работает механизм генераторов списков в python?

(наверх)

Механизм генераторов списков в Python предоставляет удобный способ создания списков с помощью компактного и выразительного синтаксиса. Генераторы списков позволяют создавать списки на основе итерации или применения операций к элементам других последовательностей.

Основной синтаксис генераторов списков выглядит следующим образом:

[выражение for элемент in последовательность (если условие)]

Элементы в квадратных скобках [] образуют новый список, их значение определяется выражением, которое указывается в начале генератора списка. Затем мы указываем ключевое слово for, за которым следует переменная, которая будет хранить значения элементов из последовательности, и непосредственно сама последовательность.

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

Рассмотрим несколько примеров генераторов списков:

  1. Генератор списка с использованием цикла:
numbers = [i for i in range(10)]
print(numbers)  # Вывод: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  1. Генератор списка с условием:
even_numbers = [i for i in range(10) if i % 2 == 0]
print(even_numbers)  # Вывод: [0, 2, 4, 6, 8]
  1. Генератор списка для преобразования элементов:
names = ["Alice", "Bob", "Charlie"]
name_lengths = [len(name) for name in names]
print(name_lengths)  # Вывод: [5, 3, 7]
  1. Генератор списка для работы с объектами разных типов:
mixed_list = [x if isinstance(x, int) else 0 for x in [1, "two", 3, "four", 5]]
print(mixed_list)  # Вывод: [1, 0, 3, 0, 5]

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

Как работает механизм словарей в python?

(наверх)

Механизм словарей в Python представляет собой встроенную структуру данных, которая позволяет хранить и организовывать пары ключ-значение. Словари в Python являются изменяемыми (mutable) и неупорядоченными (unordered) коллекциями элементов. Они основаны на хэш-таблицах, что позволяет эффективно выполнять операции вставки, удаления и поиска.

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

Примеры кода:

  1. Создание словаря:
# Пустой словарь
my_dict = {}

# Словарь с начальными значениями
my_dict = {'apple': 2, 'banana': 3, 'orange': 5}
  1. Добавление элементов в словарь:
my_dict = {}
my_dict['apple'] = 2
my_dict['banana'] = 3
my_dict['orange'] = 5

# Или можно добавить несколько элементов сразу
my_dict.update({'grape': 4, 'mango': 6})
  1. Получение значения по ключу:
my_dict = {'apple': 2, 'banana': 3, 'orange': 5}
print(my_dict['apple'])  # Выводит: 2

# Можно использовать метод get() для получения значения
print(my_dict.get('banana'))  # Выводит: 3

# Если ключа нет в словаре, метод get() вернет None или значение по умолчанию
print(my_dict.get('grape'))  # Выводит: None
print(my_dict.get('grape', 0))  # Выводит: 0 (значение по умолчанию)
  1. Удаление элемента из словаря:
my_dict = {'apple': 2, 'banana': 3, 'orange': 5}
del my_dict['apple']
print(my_dict)  # Выводит: {'banana': 3, 'orange': 5}

# Можно использовать метод pop() для удаления и получения значения элемента
value = my_dict.pop('banana')
print(my_dict)  # Выводит: {'orange': 5}
print(value)  # Выводит: 3
  1. Проверка наличия ключа в словаре:
my_dict = {'apple': 2, 'banana': 3, 'orange': 5}
print('apple' in my_dict)  # Выводит: True
print('grape' in my_dict)  # Выводит: False
  1. Итерация по словарю:
my_dict = {'apple': 2, 'banana': 3, 'orange': 5}
for key in my_dict:
    print(key, my_dict[key])

# Вывод:
# apple 2
# banana 3
# orange 5

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

Что такое namedtuple в python и как его использовать?

(наверх)

namedtuple в Python - это класс из модуля collections, который предоставляет удобный способ создания и использования неизменяемых (immutable) объектов, похожих на кортежи, но с возможностью доступа к элементам через имена полей.

Определение namedtuple выполняется с использованием функции namedtuple() из модуля collections. Синтаксис следующий:

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age', 'gender'])

В этом примере мы создали именованный кортеж с именем "Person" и полями "name", "age" и "gender". Теперь мы можем создавать экземпляры этого именованного кортежа, указывая значения для каждого поля:

person1 = Person(name='Alice', age=25, gender='Female')
person2 = Person(name='Bob', age=30, gender='Male')

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

print(person1.name)  # Выводит 'Alice'
print(person2.age)   # Выводит 30

namedtuple также предоставляет другие полезные методы, например, ._asdict(), который возвращает именованный кортеж в виде словаря, и ._replace(), который позволяет создать новый именованный кортеж с заменой значений полей:

person_dict = person1._asdict()
print(person_dict)  # Выводит OrderedDict([('name', 'Alice'), ('age', 25), ('gender', 'Female')])

new_person = person2._replace(age=35)
print(new_person)   # Выводит Person(name='Bob', age=35, gender='Male')

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

Как работает механизм множеств в python?

(наверх)

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

Вот некоторые основные операции и методы, которые можно использовать с множествами в Python:

Создание множества:

my_set = {1, 2, 3}  # Инициализация множества с помощью фигурных скобок
my_set = set([1, 2, 3])  # Инициализация множества с помощью функции set()

Добавление элементов:

my_set.add(4)  # Добавление элемента в множество

Удаление элементов:

my_set.remove(2)  # Удаление элемента из множества
my_set.discard(3)  # Удаление элемента из множества (без генерации ошибки, если элемент отсутствует)
my_set.clear()  # Удаление всех элементов из множества

Проверка на наличие элемента:

if 1 in my_set:
    print("Элемент присутствует в множестве")

Операции над множествами:

set1 = {1, 2, 3}
set2 = {3, 4, 5}

union_set = set1.union(set2)  # Объединение множеств
intersection_set = set1.intersection(set2)  # Пересечение множеств
difference_set = set1.difference(set2)  # Разность множеств

Итерация по множеству:

for item in my_set:
    print(item)

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

Что такое графы и как их можно реализовать в Python?

(наверх)

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

В Python существует несколько способов реализации графов. Рассмотрим два наиболее распространенных: с использованием словаря и с использованием классов.

  1. Реализация графов с использованием словаря:
    В этом подходе мы используем словарь для представления графа, где ключи - это вершины, а значения - это список смежных вершин.
graph = {
    'A': ['B', 'C'],
    'B': ['C', 'D'],
    'C': ['D'],
    'D': ['C'],
    'E': ['F'],
    'F': ['C']
}

В этом примере граф содержит вершины от 'A' до 'F', и связи между ними определены в словаре. Например, вершина 'A' имеет смежные вершины 'B' и 'C', вершина 'B' имеет смежные вершины 'C' и 'D', и так далее.

  1. Реализация графов с использованием классов:
    В этом подходе мы определяем класс для вершины графа и класс для самого графа. Класс вершины содержит информацию о самой вершине и ее смежных вершинах. Класс графа содержит список вершин и методы для добавления новых вершин и ребер.

Пример кода:

class Vertex:
    def __init__(self, key):
        self.key = key
        self.connections = {}

    def add_neighbor(self, neighbor, weight=0):
        self.connections[neighbor] = weight

    def get_connections(self):
        return self.connections.keys()

class Graph:
    def __init__(self):
        self.vertices = {}

    def add_vertex(self, vertex):
        new_vertex = Vertex(vertex)
        self.vertices[vertex] = new_vertex

    def add_edge(self, start, end, weight=0):
        if start not in self.vertices:
            self.add_vertex(start)
        if end not in self.vertices:
            self.add_vertex(end)
        self.vertices[start].add_neighbor(end, weight)

    def get_vertices(self):
        return self.vertices.keys()

В этом примере класс Vertex представляет вершину графа, а класс Graph представляет сам граф. Метод add_vertex добавляет новую вершину в граф, метод add_edge добавляет ребро между двумя вершинами, а методы get_connections и get_vertices возвращают смежные вершины и список всех вершин соответственно.

Какие типы данных в Python поддерживаются многопоточностью, и почему?

(наверх)

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

  1. Список (List):
    Списки в Python являются изменяемыми и потокобезопасными структурами данных, благодаря чему могут быть использованы в многопоточных средах. Они могут использоваться для совместного доступа и модификации данных в разных потоках.
from threading import Thread

def append_to_list(my_list, item):
    my_list.append(item)

shared_list = []
thread1 = Thread(target=append_to_list, args=(shared_list, 1))
thread2 = Thread(target=append_to_list, args=(shared_list, 2))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(shared_list)  # Результат: [1, 2]
  1. Словарь (Dictionary):
    Словари в Python также являются изменяемыми и потокобезопасными структурами данных. Они могут быть использованы для общего доступа к данным в разных потоках.
from threading import Thread

def update_dictionary(my_dict, key, value):
    my_dict[key] = value

shared_dict = {}
thread1 = Thread(target=update_dictionary, args=(shared_dict, 'key1', 'value1'))
thread2 = Thread(target=update_dictionary, args=(shared_dict, 'key2', 'value2'))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(shared_dict)  # Результат: {'key1': 'value1', 'key2': 'value2'}
  1. Очередь (Queue):
    Модуль queue в Python предоставляет потокобезопасные очереди, которые могут использоваться для обмена данными между потоками. Очереди обеспечивают правильную синхронизацию и блокировки при обращении к элементам.
from queue import Queue
from threading import Thread

def process_queue(queue):
    while not queue.empty():
        item = queue.get()
        # Обработка элемента

shared_queue = Queue()
shared_queue.put(1)
shared_queue.put(2)

thread1 = Thread(target=process_queue, args=(shared_queue,))
thread2 = Thread(target=process_queue, args=(shared_queue,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
  1. Замок (Lock):
    Замки (Lock) представляют собой механизм синхронизации, который обеспечивает эксклюзивный доступ к разделяемым данным. Они используются для предотвращения одновременного доступа к данным из нескольких потоков.
from threading import Thread, Lock

def increment_counter(counter, lock):
    for _ in range(10000):
        lock.acquire()
        counter += 1
        lock.release()

shared_counter = 0
lock = Lock()

thread1 = Thread(target=increment_counter, args=(shared_counter, lock))
thread2 = Thread(target=increment_counter, args=(shared_counter, lock))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(shared_counter)  # Результат: 20000

Все эти типы данных обеспечивают безопасность потоков и правильную синхронизацию при работе с множественными потоками. Использование подобных структур данных позволяет избежать гонок данных (data races) и других проблем, связанных с многопоточностью.

Какие типы данных в Python поддерживаются многопроцессорностью, и почему?

(наверх)

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

  1. Разделяемая память (Shared Memory):
    Разделяемая память позволяет разным процессам обмениваться данными, используя общий сегмент памяти. В Python это достигается с использованием класса Value из модуля multiprocessing.
from multiprocessing import Process, Value

def increment_counter(counter):
    counter.value += 1

shared_counter = Value('i', 0)

processes = []
for _ in range(10):
    process = Process(target=increment_counter, args=(shared_counter,))
    processes.append(process)
    process.start()

for process in processes:
    process.join()

print(shared_counter.value)  # Результат: 10

В этом примере создается разделяемая переменная shared_counter с помощью класса Value, и каждый процесс инкрементирует ее значение. Разделяемая переменная позволяет безопасно обмениваться данными между процессами.

  1. Очередь (Queue):
    Очереди из модуля multiprocessing предоставляют механизм для передачи данных между процессами. Они являются потокобезопасными и обеспечивают правильную синхронизацию доступа к данным.
from multiprocessing import Process, Queue

def process_queue(queue):
    while not queue.empty():
        item = queue.get()
        # Обработка элемента

shared_queue = Queue()
shared_queue.put(1)
shared_queue.put(2)

processes = []
for _ in range(2):
    process = Process(target=process_queue, args=(shared_queue,))
    processes.append(process)
    process.start()

for process in processes:
    process.join()

В этом примере создается разделяемая очередь shared_queue, которая заполняется элементами. Затем создаются процессы, которые получают элементы из очереди и выполняют обработку.

  1. Пул процессов (Process Pool):
    Пул процессов из модуля multiprocessing предоставляет возможность создания пула фиксированного количества процессов, которые могут выполнять задачи параллельно. Он предоставляет удобный интерфейс для распределения работы между процессами.
from multiprocessing import Pool

def square(number):
    return number ** 2

numbers = [1, 2, 3, 4, 5]

with Pool(processes=2) as pool:
    result = pool.map(square, numbers)

print(result)  # Результат: [1, 4, 9, 16, 25]

В этом примере используется пул процессов для расчета квадратов чисел из списка numbers. map метод пула процессов позволяет распределить задачи между процессами и получить результаты.

  1. Блокировки (Lock): Блокировки из модуля multiprocessing используются для обеспечения безопасного доступа к общим ресурсам из разных процессов. Они позволяют синхронизировать доступ к данным и предотвратить гонки данных (data races).
from multiprocessing import Process, Lock

def increment_counter(counter, lock):
    lock.acquire()
    try:
        counter.value += 1
    finally:
        lock.release()

shared_counter = Value('i', 0)
lock = Lock()

processes = []
for _ in range(10):
    process = Process(target=increment_counter, args=(shared_counter, lock))
    processes.append(process)
    process.start()

for process in processes:
    process.join()

print(shared_counter.value)  # Результат: 10

В этом примере создается разделяемая переменная shared_counter с помощью класса Value и блокировка lock с помощью класса Lock. Каждый процесс захватывает блокировку перед доступом к общей переменной и освобождает ее после завершения операции.

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

Что такое рекурсия и как она работает в Python?

(наверх)

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

(наверх)

Какие структуры данных в Python можно использовать для реализации очередей и стеков, и как они работают?

(наверх)

Какие структуры данных в Python можно использовать для реализации графов, и как они работают?

(наверх)

Какие структуры данных в Python можно использовать для реализации деревьев, и как они работают?

(наверх)

Какие структуры данных в Python можно использовать для реализации куч (heap), и как они работают?

(наверх)

Что такое "Big O notation", и как она используется для анализа производительности алгоритмов и структур данных в Python?

(наверх)

GIL

(наверх)

Что такое GIL?

В Python используется глобальная блокировка интерпретатора (Global Interpreter Lock — GIL), накладывающая некоторые ограничения на потоки. А именно, нельзя использовать несколько процессоров одновременно. Блокировка, позволяет только одному потоку управлять интерпретатором Python. Это означает, что в любой момент времени будет выполняться только один конкретный поток. Из этого следует, что с потоками невозможно использовать несколько ядер процессора.

GIL был введен в Python потому, что управление памятью CPython не является потокобезопасным. Имея такую блокировку Python может быть уверен, что никогда не будет условий гонки.

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

image

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

Многопоточные и многопроцессорные программы в python

(наверх)

Параллелизм дает возможность работать над несколькими вычислениями одновременно в одной программе. Такого поведения в Python можно добиться несколькими способами:

  • Используя многопоточность threading, позволяя нескольким потокам работать по очереди.
  • Используя несколько ядер процессора multiprocessing. Делать сразу несколько вычислений, используя несколько ядер процессора. Это и называется параллелизмом.
  • Используя асинхронный ввод-вывод с модулем asyncio. Запуская какую то задачу, продолжать делать другие вычисления, вместо ожидания ответа от сетевого подключения или от операций чтения/записи.

Разница между потоками и процессами

(наверх)

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

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

Асинхронный ввод-вывод не является ни потоковым (threading), ни многопроцессорным (multiprocessing). По сути, это однопоточная, однопроцессная парадигма и не относится к параллельным вычислениям.

Что такое условия гонки и потокобезопасность?

(наверх)

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

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

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

Алгоритм планирования доступа потоков к общим данным

(наверх)

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

GC

(наверх)

Как в питоне обстоят дела с памятью (управлением памятью)

(наверх)

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

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

  • Ссылочный подсчет - это механизм, который считает количество ссылок на каждый объект в памяти. Когда количество ссылок на объект становится равным нулю, объект удаляется.

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

В Python есть также модуль gc, который предоставляет функции для контроля над сборщиком мусора. Например, можно задать максимальный размер кучи (heap), после которого сборщик мусора будет вызываться. Также можно использовать функцию gc.collect(), чтобы явно вызвать сборку мусора вручную.

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

Кроме того, в Python есть возможность использовать различные библиотеки для управления памятью, такие как memory_profiler и objgraph, которые позволяют анализировать использование памяти в программе и находить утечки памяти.

Сколько стоит проверка элемента в нотации?

(наверх)

Проверка элемента в Python зависит от типа объекта, в котором нужно произвести проверку, и используемого метода проверки.

В общем случае, проверка элемента в Python происходит достаточно быстро и может быть выполнена за константное время O(1), когда элемент находится в списке (list) или словаре (dictionary), и за линейное время O(n), когда элемент нужно найти в кортеже (tuple) или множестве (set).

Например, для проверки наличия элемента в списке используется оператор in или метод list.count(). Оператор in возвращает True, если элемент присутствует в списке, и False в противном случае. Метод list.count() возвращает количество вхождений элемента в список.

Для проверки наличия элемента в словаре используется метод dict.get() или оператор in. Оператор in проверяет наличие ключа в словаре и возвращает True, если ключ присутствует, и False в противном случае. Метод dict.get() возвращает значение, связанное с указанным ключом, если ключ присутствует в словаре, и None в противном случае.

Для проверки наличия элемента в кортеже используется оператор in. Оператор in проверяет наличие элемента в кортеже и возвращает True, если элемент присутствует, и False в противном случае. Поскольку кортежи являются неизменяемыми, поиск элемента может занять линейное время O(n), где n - размер кортежа.

Для проверки наличия элемента в множестве используется, угадайте какой оператор - in. Множества в Python реализованы в виде хэш-таблиц, поэтому проверка наличия элемента занимает константное время O(1), если элемент находится в множестве.

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

Если есть два объекта и они указывают друг на друга

(наверх)

Если в Python есть два объекта, которые указывают друг на друга, то такая ситуация называется циклической ссылкой или циклической зависимостью.

Циклическая ссылка возникает, когда объект A содержит ссылку на объект B, а объект B содержит ссылку на объект A. Например:

a = [1, 2]
b = [3, 4]
a.append(b)
b.append(a)

В этом случае объект a содержит ссылку на объект b через метод append(), а объект b содержит ссылку на объект a через тот же метод.

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

Для решения этой проблемы Python использует алгоритм под названием "сборка мусора с подсчетом ссылок". Сборщик мусора проверяет, сколько ссылок ведет к каждому объекту в памяти. Если на объект не указывает ни одна ссылка, то он автоматически удаляется из памяти.

Однако, если объекты создают циклическую ссылку, то каждый объект в цикле имеет хотя бы одну ссылку на себя, что препятствует сборке мусора. В этом случае для удаления объектов из памяти требуется использовать специальные методы, такие как gc.collect(), которые вызывают сборку мусора вручную и позволяют обойти проблему циклических ссылок.

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

Итераторы, декораторы и генераторы

(наверх)

(наверх)

Что такое итератор?

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

Итерируемый — объект, в котором есть метод __iter__. В свою очередь, итератор — объект, в котором есть два метода: __iter__ и __next__. Почти всегда iterator возвращает себя из метода __iter__, так как они выступают итераторами для самих себя, но есть исключения.

В целом стоит избегать прямого вызова __iter__ и __next__. При использовании for или генераторов списков Python вызывает эти методы сам. Если всё-таки необходимо вызвать методы напрямую, лучше использовать встроенные функции iter и next и в параметрах передаём итератор или контейнер. Например, если c — итерируемый, используемiter(c) вместо c.__iter__(). Если a — итератор, используем next(a), а не a.__next__(). Это похоже на использование len.

Раз уж речь зашла о len, то стоит упомянуть, что итераторы не должны иметь и часто не имеют определённой длины. Поэтому в них часто нет имплементации __len__. Чтобы подсчитать количество элементов в итераторе, приходится делать это вручную или использовать sum.

Некоторые итерируемые (iterable) не являются итераторами, но используют другие объекты как итераторы. Например, объект list относится к итерируемым, но не является итератором. В нём реализован метод __iter__, но отсутствует метод __next__. Итераторы объектов list относятся к типу listiterator. У объектов list есть определённая длина, а у listiterator нет.

>>> a = [1, 2]
>>> type(a)
<type 'list'>
>>> type(iter(a))
<type 'listiterator'>
>>> it = iter(a)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> len(a)
2
>>> len(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'listiterator' has no len()

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

Пример итератора - считает с нуля до бесконечности. Это упрощённая версия itertools.count.

class count_iterator:
    n = 0

    def __iter__(self):
        return self

    def __next__(self):
        y = self.n
        self.n += 1
        return y

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

>>> counter = count_iterator()
>>> next(counter)
0
>>> next(counter)
1
>>> next(counter)
2
>>> next(counter)
3
>>> list(counter)  # Бесконечный цикл

Если у объекта нет метода __iter__, его можно обойти, если определить метод __getitem__. В этом случае встроенная функция iter возвращает итератор с типом iterator, который использует __getitem__ для обхода элементов списка. Этот метод возвращает StopIteration или IndexError, когда обход завершается. Пример:

class SimpleList(object):
    def __init__(self, *items):
        self.items = items

    def __getitem__(self, i):
        return self.items[i]

И пример использования:

>>> a = SimpleList(1, 2, 3)
>>> it = iter(a)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Ещё один интересный пример: генерация последовательности Хофштадтера. В приведённом ниже коде итератор используется для генерации последовательности с помощью вложенных повторений.

Q(n)=Q(n−Q(n−1))+Q(n−Q(n−2))

Например, qsequence([1, 1]) генерирует точную последовательность Хофштадтера. Мы используем исключение StopIteration, чтобы показать, что последовательность не может продолжаться, так как для генерации следующего элемента должен использоваться несуществующий индекс. Если в параметрах укзать значения [1, 2], последовательность немедленно заканчивается.

class qsequence:
    def __init__(self, s):
        self.s = s[:]

    def __next__(self):
        try:
            q = self.s[-self.s[-1]] + self.s[-self.s[-2]]
            self.s.append(q)
            return q
        except IndexError:
            raise StopIteration()

    def __iter__(self):
        return self

    def current_state(self):
        return self.s

Пример использования:

>>> Q = qsequence([1, 1])
>>> next(Q)
2
>>> next(Q)
3
>>> [next(Q) for __ in range(10)]
[3, 4, 5, 5, 6, 6, 6, 8, 8, 8]

Что такое генератор?

(наверх)

Генераторами называют итераторы, определение которых выглядит как определение функций.
Ещё одно определение: генераторы — функции, которые внутри используют выражение yield. Генераторы не могут возвращать значения, вместо этого выдают элементы по готовности. Python автоматизирует запоминание контекста генератора, то есть текущий поток управления, значение локальных переменных и так далее. Каждый вызов метода __next__ у объекта генератора возвращает следующее значение. Метод __iter__ также реализуется автоматически. То есть генераторы можно использовать везде, где требуются итераторы.

def count_generator():
   n = 0
   while True:
     yield n
     n += 1

Как это применяется на практике.

>>> counter = count_generator()
>>> counter
<generator object count_generator at 0x106bf1aa0>
>>> next(counter)
0
>>> next(counter)
1
>>> iter(counter)
<generator object count_generator at 0x106bf1aa0>
>>> iter(counter) is counter
True
>>> type(counter)
<type 'generator'>

Теперь посмотрим на реализацию последовательности Q Хофштадтера с помощью генератора. Заметьте, эта реализация значительно проще использованного выше подхода. Однако здесь уже невозможно использовать методы типа current_state. Извне невозможно получить доступ к переменным, которые хранятся в контексте генератора.

Существует словарь gi_frame.f_locals, но он относится к CPython, но не входит в стандарт языка Python.

Одно из возможных решений — получение одновременно списка и результата.

def hofstadter_generator(s):
    a = s[:]
    while True:
        try:
            q = a[-a[-1]] + a[-a[-2]]
            a.append(q)
            yield q
        except IndexError:
            Return

Итерация в данном примере завершается простым return без параметров. Внутри происходит возбуждение исключения StopIteration. Следующий пример связан с распределением Бернулли, которое реализуется с помощью двух генераторов. Речь идёт о бесконечной последовательности случайных булевых значений. При этом вероятность True равна p, а вероятность False определяется формулой q=1-p. Затем применяется экстрактор фон Неймана, который принимает процесс Бернулли с 0 < p < 1 как источник энтропии и возвращает чистый процесс Бернулли с p = 0.5

import random

def bernoulli_process(p):
    if p > 1.0 or p < 0.0:
        raise ValueError("p should be between 0.0 and 1.0.")

    while True:
        yield random.random() < p

def von_neumann_extractor(process):
    while True:
        x, y = next(proccess), next(process)
        if x != y:
            yield x

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

>>> def tent_map(mu, x0):
...    x = x0
...    while True:
...        yield x
...        x = mu * min(x, 1.0 - x)
...
>>>
>>> t = tent_map(2.0, 0.1)
>>> for __ in range(30):
...    print(next(t))
...
0.1
0.2
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.799999999999
0.400000000001
0.800000000003
0.399999999994
0.799999999988
0.400000000023
0.800000000047
0.399999999907
0.799999999814
0.400000000373
0.800000000745
0.39999999851
0.79999999702

Ещё один пример касается последовательности Коллатца.

def collatz(n):
   yield n
   while n != 1:
     n = n / 2 if n % 2 == 0 else 3 * n + 1
     yield n

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

Пример использования генератора:

>>> # Если гипотеза Коллатца верна, list(collatz(n)) с любым n 
... # всегда завершается
>>> list(collatz(7))
[7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]
>>> list(collatz(13))
[13, 40, 20, 10, 5, 16, 8, 4, 2, 1]
>>> list(collatz(17))
[17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]
>>> list(collatz(19))
[19, 58, 29, 88, 44, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]

Что такое декоратор?

(наверх)

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

Декораторы в Python реализованы в виде обычных функций, которые обычно имеют имя, начинающееся с символа @, за которым следует имя декоратора. Например:

@decorator_function
def some_function():
    # ...

Здесь decorator_function - это имя декоратора, который будет применен к функции some_function(). Обратите внимание, что перед именем функции стоит символ @.

Примеры использования декораторов:

Декоратор, который измеряет время выполнения функции:

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Time taken: {end_time - start_time:.6f} seconds")
        return result
    return wrapper

@timer_decorator
def some_function():
    time.sleep(2)
    return "Done"

Здесь timer_decorator - это декоратор, который добавляет функциональность для измерения времени выполнения функции. Он принимает функцию в качестве аргумента, определяет внутри себя функцию-обертку wrapper, которая вызывает переданную функцию и измеряет время ее выполнения. Функция-обертка затем возвращает результат выполнения переданной функции.

Декоратор, который кэширует результаты функции:

def cache_decorator(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@cache_decorator
def fibonacci(n):
    if n in [0, 1]:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

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

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

Какие декораторы стандартной библиотеки вы знаете?

(наверх)

Стандартная библиотека Python содержит множество полезных декораторов. Ниже перечислены некоторые из них:

  • functools.lru_cache() - декоратор для кэширования результатов функции с использованием алгоритма LRU (Least Recently Used).

  • functools.singledispatch() - декоратор для создания полиморфной функции, которая вызывает различные реализации функции в зависимости от типа аргумента.

  • contextlib.contextmanager() - декоратор для создания менеджера контекста, который позволяет использовать блок with для выполнения операций до и после выполнения блока кода.

  • unittest.skipIf() и unittest.skipUnless() - декораторы для пропуска тестов, если заданное условие истинно или ложно соответственно.

  • asyncio.coroutine() и asyncio.ensure_future() - декораторы для определения и запуска асинхронных функций в asyncio.

  • property - декоратор для определения атрибутов класса как свойств, которые вычисляются при доступе к ним.

  • staticmethod - декоратор для определения статических методов класса, которые не требуют доступа к экземпляру класса.

  • classmethod - декоратор для определения методов класса, которые принимают класс в качестве первого аргумента.

  • functools.partial() - функция-фабрика для создания новой функции, которая является частичным применением другой функции с заданными аргументами.

  • wrapt.decorator() - библиотека, содержащая декораторы для создания оберток вокруг функций с поддержкой множества возможностей, таких как поддержка декораторов, передача аргументов и т.д.

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

Что такое list comprehension, какой синтаксис создания генераторов?

(наверх)

List comprehension - это способ создания нового списка на основе существующего списка (или другой итерируемой последовательности), используя синтаксис, который позволяет применять операции и фильтры к элементам списка. Это удобный способ создания списков с минимальным объемом кода.

Пример создания списка с помощью list comprehension:

numbers = [1, 2, 3, 4, 5]
squares = [x**2 for x in numbers]
print(squares)  # Output: [1, 4, 9, 16, 25]

Это эквивалентно следующему коду с использованием цикла for:

numbers = [1, 2, 3, 4, 5]
squares = []
for x in numbers:
    squares.append(x**2)
print(squares)  # Output: [1, 4, 9, 16, 25]

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

Пример создания генератора с помощью генераторного выражения:

numbers = [1, 2, 3, 4, 5]
squares = (x**2 for x in numbers)
for num in squares:
    print(num)  # Output: 1, 4, 9, 16, 25

Это эквивалентно следующему коду с использованием цикла for и функции-генератора:

def squares(nums):
    for x in nums:
        yield x**2

numbers = [1, 2, 3, 4, 5]
squares_gen = squares(numbers)
for num in squares_gen:
    print(num)  # Output: 1, 4, 9, 16, 25

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