Продвинутые советы по Python, которые вы должны использовать, но, вероятно, не используете.
Темы:
- Основы программирования: ведение журнала, подсказка типов, генераторы, анализ аргументов и декораторы.
- Поведение класса: наследование, Dunders, перегрузка операторов, инкапсуляция и абстрактные классы.
- Введение в шаблоны проектирования: Singleton, Factory, Proxy и Composit.
Основы программирования
Ведение журнала
При мониторинге и отладке большинство программистов-самоучек начинают либо с написания собственного модуля логгера, либо (не дай бог) просто печатают все, что только можно вообразить.
Использование встроенных logging
и (pip
устанавливаемых) coloredlogs
может выполняться с минимальными усилиями.
Напомним, что существует 5 уровней безопасности при ведении журнала:
DEBUG: - for devs, troubleshoot and testing. INFO: - informational about current state. WARNING - system doesn't crash but could cause a problem. Example: RAM limit approaching. ERROR - couldn't do some operation, but the system can still runs. CRITICAL - the entire system is impaired.
Изменение уровня безопасности будет определять, сколько скрипт будет регистрировать в консоли/записывать в обработчик. Реализация регистратора состоит из handler
(который записывает файл журнала); formatter
(необязательно, для форматирования вывода); и logger instance
.
import logging, coloredlogs # configuration formatter = logging.Formatter("[%(asctime)s] [%(levelname)8s] --- %(message)s (%(filename)s:%(lineno)s)", "%Y-%m-%d %H:%M:%S") logger = logging.getLogger('My logger.') handler = logging.FileHandler('mylogs.log') handler.setLevel(logging.DEBUG) handler.setFormatter(formatter) logger.addHandler(handler) coloredlogs.install(level='DEBUG', logger=logger) # examples logger.debug("this is a DEBUG message") logger.info("this is an INFO message") logger.warning("this is a WARNING message") logger.error("this is an ERROR message") logger.critical("this is a CRITCAL message")
Подсказка типа
Python имеет динамическую типизацию. Однако часто полезно следить за типом переменных. Type hinting
используется регулярно, но не вызывает проблем с компиляцией, поэтому его можно игнорировать. Условные операторы можно использовать для записи исключений или обработки нежелательных types
или returns
во время выполнения, но это может излишне раздувать код.
mypy
— это пакет, предназначенный для решения этой проблемы во время разработки. Аргументы функции и возвращаемое значение types
можно добавить стандартным образом. Затем мы запускаем скрипт из терминала, используя mypy script.py
(в отличие от python script.py
) — автоматически проверяя, соответствуют ли types
их ожиданиям.
Например, приведенный ниже код компилируется без ошибок, но функция ошибочно передает string
вместо ожидаемого int
.
def func(arg_1:int, arg_2:list[int]) -> tuple: return arg_1, arg_2 func('12', [1,2,3])
Во время разработки можно было запустить скрипт с mypy script.py
, поймав ошибку:
Генераторы
Генераторы используют «ленивое выполнение», чтобы избежать выполнения больших итераций до тех пор, пока они не потребуются. Слишком знакомая функция range()
является примером генератора. То есть он не вычисляет полный диапазон элементов и не сохраняет их в памяти, а сохраняет саму функцию (с «состоянием»), а затем может генерировать следующий шаг в последовательности по мере необходимости.
Это можно увидеть, просмотрев размер (в байтах) двух экземпляров range()
: первый содержит один элемент, второй — 1 миллион элементов. Сравните размер генерации r
с размером соответствующего списка l
.
import sys # range items (generators) r1 = range(1) r2 = range(1000000) # convert to list l1 = list(r1) l2 = list(r2) def ptr(obj, name): print(f'object:{name} is {sys.getsizeof(obj):>7} bytes.') ptr(r1, 'r1') ptr(r2, 'r2') ptr(l1, 'l1') ptr(l2, 'l2')
Во многих случаях может оказаться выгодным написать собственный генератор. Этого можно добиться с помощью оператора yeild
в python. Генераторы работают бесконечно, делая возможными бесконечные последовательности. Например, вот генератор, который возвращает силы x
навечно.
def power_x(x): y = 0 while True: yield x**y y += 1 value = power_x(4) print(next(value)) print(next(value)) print(next(value)) print(next(value)) print(next(value))
Разбор аргумента
Нам часто нужно выполнить сценарий из командной строки, и мы можем захотеть включить аргументы. sys.argv
возвращает список аргументов, переданных сценарию (где sys.argv[0]
— имя файла).
Предположим, мы хотим использовать необязательные аргументы, а не простой список позиционных аргументов. Встроенный пакет getopt
можно использовать для включения необязательных аргументов. Например:
import getopt import sys opts, args = getopt.getopt( sys.argv[1:], 'f:m:', ['filename','message'] ) print('opt: ', opts) print('args: ', args)
Декораторы
Как следует из названия, декораторы используются для decorate
функций (добавления дополнительных функций). Они принимают форму:
def decorator(function): def wrapper(*args, **kwargs): # add decorations rt = function(*args, **kwargs) return rt return wrapper @decorator def function(x): # some function pass
Где некоторая дополнительная логика может содержаться в функции decorator.wrapper()
. Строка @decorator
над функцией эквивалентна вызову:
decorator(function)(*args, **kwargs)
Например, мы можем создать декоратор, который измеряет время функции:
import time def timer_decorator(function): def wrapper(*args, **kwargs): start = time.time() rt = function(*args, **kwargs) end = time.time() print(f'function: {function.__name__} was executed in {end-start} seconds.') return rt return wrapper @timer_decorator def funct(x): a = 1 for i in range(1,x): a *= i return a funct(10000)
Поведение класса
Наследование
Наследование (включено для полноты) позволяет дочернему классу наследовать все свойства родительского класса. Например, класс Ferrari
может иметь все свойства класса car
, но также содержать отдельные свойства.
# Inheretence class Car: def __init__(self, type, age) -> None: self.type = type self.age = age class Ferrari(Car): def __init__(self, type, age, speed) -> None: super(Ferrari, self).__init__(type, age) self.speed = speed
Переменные класса
Цель ООП-программирования — объединить данные и операции в объекты (классы). Конкретный экземпляр класса содержит определенные данные, которые определяют экземпляр. Также возможно связать данные с объектом уровня class
. Мы можем захотеть отслеживать определенные данные по всем экземплярам типа класса.
class Base: count = 0 def __init__(self) -> None: Base.count += 1 [Base() for _ in range(5)] print('Instances: ', Base.count)
Статические методы.тот же принцип можно применить к методам, определив статические методы. Это методы, которые могут быть классифицированы в классе напрямую без создания экземпляра.
class Base: @staticmethod def mymethod(): # some function pass Base.mymethod()
Дандерс
Методы Dunder (двойное подчеркивание) определяют встроенное поведение. Стандартное поведение можно переопределить, включив методы dunder в определение класса. Конструктор (__init__
), деструктор (__del__
) и приведение строкового типа (__str__
) являются типичными примерами этого.
Важно отметить, что если вы хотите получить доступ к поведению dunder по умолчанию, можно использовать super().__dunder__()
. Другим недоиспользуемым дандером является метод __call__
, который делает объект вызываемым. Более конкретно,
class Example: def __str__(self) -> str: txt = super().__str__() txt += ' - added to text...' return txt def __call__(self, *args, **kwds): print(f'\n{self.__class__.__name__} was called!') ex = Example() print(ex) ex()
Перегрузка оператора
Особый случай dunders, Перегрузка оператора позволяет нам определить, как объекты взаимодействуют с операторами (+,-,*,÷)
. Многие формы программирования (включая мою родную науку о данных) по своей природе являются математическими. Переопределение поведения оператора по умолчанию можно использовать, чтобы указать, как должны взаимодействовать объекты класса. Рассмотрим оператор +
(модифицированный с помощью __add__
dunder):
class Vector: def __init__(self,x,y) -> None: self.x, self.y = x, y def __add__(self, other): return Vector(self.x+other.x, self.y+other.y) def __str__(self) -> str: return ' x:{}, y:{}'.format(self.x, self.y) v1 = Vector(1,2) v2 = Vector(5,4) v3 = v1 + v2 print(v1) print(v2) print(v3)
Инкапсуляция
Иногда мы хотим скрыть или ограничить некоторую информацию о классе. Предположим, у нас есть класс Person
со свойством age
. Соглашение об именовании с двойным хранилищем делает переменную частной:
class Person: def __init__(self, age) -> None: self.__age = age
В результате мы не можем изменить переменную age
на более позднем этапе:
p1 = Person() p1.__age
Мы можем добавить property
к классу, который позволит нам взаимодействовать с этой переменной.
@property def Age(self): return self.__age
Затем мы можем добавить метод Property.setter
(используя то же имя метода) для обновления свойства. Если сложить все вместе, получится:
class Person: def __init__(self, age) -> None: self.__age = age @property def Age(self): return self.__age @Age.setter def Age(self, value): if value <= 0: return else: self.__age = value p1 = Person(20) print(p1.Age) p1.Age = -2342 print(p1.Age) p1.Age = 2342 print(p1.Age)
Обратите внимание, что мы запретили возрасту устанавливать отрицательное значение. Таким образом мы можем контролировать и ограничивать свойства класса.
Абстрактные классы
Абстрактные классы, определенные как классы, которые не могут быть созданы, определяют схемы для создания согласованных классов. Предположим, мы хотим определить набор классов, реализующих один и тот же набор методов. Все подклассы могут наследовать от абстрактного класса. Таким образом, если какой-либо подкласс не сможет реализовать требуемый метод, будет возбуждено исключение.
from abc import ABCMeta, abstractstaticmethod class IAbstractClass(metaclass=ABCMeta): @abstractstaticmethod def method_01(): pass class child_class(IAbstractClass): @staticmethod def method_01(): print('required method implemented!')
Если бы method_01()
не был реализован в дочернем_классе, возникло бы исключение компиляции.
Для повышения надежности системы при определении шаблонов проектирования часто используются абстрактные классы.
Введение в шаблоны проектирования
Хорошая разработка программного обеспечения заключается в разработке надежных систем во время выполнения. Это требует продуманных дизайнерских решений как на уровне архитектуры, так и на уровне кода.
Хотя это ни в коем случае не является исчерпывающим, вот несколько (Gang of 4) шаблонов проектирования программного обеспечения для улучшения вашего кода. Подробнее о шаблонах проектирования читайте на Refactoring.Guru.
Синглтон
Как следует из названия, шаблон проектирования Singleton добавляет к объекту класса ограничение, позволяющее создать его экземпляр только один раз. Замечательно сформулировано Refactoring.Guru, это может быть необходимо для обеспечения того, чтобы переменная не была перезаписана или чтобы ограничить доступ к общим ресурсам.
Ключом является переменная класса Singleton
: __instance
. При первом создании экземпляра одноэлементного класса для него устанавливается значение self
. Таким образом, Singleton инициализируется во второй раз, будет возбуждено исключение.
from abc import ABCMeta, abstractstaticmethod class IAbstractClass(metaclass=ABCMeta): @abstractstaticmethod def print_data(): pass class Singleton(IAbstractClass): __instance = None def __init__(self) -> None: if Singleton.__instance is not None: raise Exception('Singleton cannot be instantiated more than once.') super().__init__() Singleton.__instance = self @staticmethod def get_instance(): if Singleton.__instance is not None: return Singleton.__instance return 'No instance of Singleton.' @staticmethod def print_data(): print(f'{Singleton.__instance}')
Фабрика
Предположим, мы хотим отложить категоризацию класса до тех пор, пока не будет получена дополнительная информация (некоторые расчеты, данные или пользовательский ввод). Фабричный метод задерживает указание определения класса, используя класс Factory
для создания подклассов.
При реализации фабричного шаблона проектирования мы реализуем класс Factory
, который строит желаемый подкласс с учетом некоторых данных. Следуя методологии абстрактного класса, шаблон проектирования factory
может быть реализован следующим образом:
from abc import ABCMeta, abstractstaticmethod class IAbstractClass(metaclass=ABCMeta): @abstractstaticmethod def print_class_type(): """Interface Method""" pass class subclass_01(IAbstractClass): def __init__(self) -> None: super().__init__() def print_class_type(self): print('Class 01') class subclass_02(IAbstractClass): def __init__(self) -> None: super().__init__() def print_class_type(self): print('Class 02') class Factory: @staticmethod def build_subclass(class_type): if class_type == 'a': return subclass_01() return subclass_02() if __name__ == '__main__': choice = input('Type of class?\n') person = Factory.build_subclass(choice) person.print_class_type()
Прокси
Как и в случае с декораторами, шаблон проектирования прокси оборачивает дополнительный уровень функциональности вокруг класса. Вероятно, увеличить модульность или отложить выполнение основного класса до тех пор, пока не будет доступно больше ресурсов. Прокси — это посредник, который занимается реализацией основного класса.
from abc import ABCMeta, abstractstaticmethod class IAbstractClass(metaclass=ABCMeta): @abstractstaticmethod def class_method(): """Implement in class.""" class CoreClass(IAbstractClass): def class_method(self): print('Core class executed...') class ProxyClass(IAbstractClass): def __init__(self) -> None: super().__init__() self.core = CoreClass() def class_method(self): print('Proxy class executed...') self.core.class_method()
Композит
Интуитивно понятно, что составной класс — это просто набор подклассов, образующих более крупный класс. Естественно вписываясь в большую часть того, как организован корпоративный мир, он интуитивно понятен и прост в реализации.
from abc import ABCMeta, abstractmethod, abstractstaticmethod class IAbstractModule(metaclass=ABCMeta): @abstractmethod def __init__(self, nodes) -> None: """Implement in child class""" @abstractstaticmethod def print_data(): """Implement in child class""" class child_01(IAbstractModule): def __init__(self, nodes) -> None: self.nodes = nodes def print_data(self): print('child 01 has {} nodes.'.format(self.nodes)) class child_02(IAbstractModule): def __init__(self, nodes) -> None: self.nodes = nodes def print_data(self): print('child 02 has {} nodes.'.format(self.nodes)) class ParentClass(IAbstractModule): def __init__(self, nodes) -> None: # super().__init__(nodes) self.nodes = nodes self.base_nodes = nodes self.children = [] def add_child(self, child): self.children.append(child) self.nodes += child.nodes def print_data(self): s = '\n'+'...'*10; print(s) print('Core nodes: {}'.format(self.base_nodes)) for chd in self.children: chd.print_data() print('Total nodes: {}'.format(self.nodes)) s = '...'*10+'\n'; print(s) # implement c1 = child_01(21) c2 = child_02(43) p1 = ParentClass(6) p1.add_child(c1) p1.add_child(c2) p1.print_data()
Фин 👋.