LINUX.ORG.RU

Правильная архитектура программы в Python

 


2

3

Написал достаточно большую программу на Python 3 (на данный момент все оформлено в виде одного большого main.py). В ней почти все состоит из функций, подгружается достаточно много внешних модулей. Программа нелинейная, и, в зависимости от задачи, в разном порядке может производить разные действия.

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

from main import some_function
но есть одна заковырка. Мне необходимо увеличить скорость импорта из главного модуля. При этом возникают следующие вопросы:

1) Что делать с внешними модулями (os, sys, difflib, pyperclip, zipfile, rarfile, pyunpack, codecs, re, time, shutil, webbrowser...)? Если подгружать все сразу, то даже импорт какого-нибудь примитива из главного модуля займет несколько секунд из-за подгрузки всех внешних модулей. Если же подгружать модули в случае необходимости, то, во-первых, как проверить их существование на самом старте и сообщить об этом, а не ловить ошибки посреди работы, а во-вторых, придется во все функции вставлять import, что не очень хорошо хотя бы потому, что одни и те же модули могут загружаться несколько раз (хотя я и не знаю, как это реализовано, может, python умный и не делает импорт из тех же самых модулей по нескольку раз).

2) Программа использует кучу глобальных переменных, которые подгружаются из внешнего файла посредством parser.get (.getint, .getfloat и т.д.). Возникает такой же вопрос, как и с модулями. Кроме того, как мне объявлять эти переменные? Просто в теле главного модуля? Тогда каждый мало-мальский импорт из главного модуля потребует наличия конфигурационного файла. А если в виде функции, то как сделать загружаемые из конфига переменные глобальными? Большинство переменных нужны на чтение, но некоторые нужны и на запись. Бывалые питонисты не рекомендуют использовать global. Кроме того, рекомендуется объявлять переменные явно, а не через exec и eval, потому что это угроза безопасности. Но при этом опять оказывается необходимым тащить с собой конфиг.

3) В каждой функции, чтобы иметь возможность контролировать ее результат, я использую логирование. Логирование также может использовать внешние модули (например, os), а также некоторые глобальные переменные (на чтение). Что с этим делать? Поставить заглушку, если условие if __name__ == '__main__' не выполняется?

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

Deleted

Мне необходимо увеличить скорость импорта из главного модуля.

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

risenshnobel ★★★
()

Что делать с внешними модулями (os, sys, difflib, pyperclip, zipfile, rarfile, pyunpack, codecs, re, time, shutil, webbrowser...)? Если подгружать все сразу, то даже импорт какого-нибудь примитива из главного модуля займет несколько секунд

Можно сделать demandimport в стиле Mercurial, но, ИМХО, тебе рано об этом думать.

Программа использует кучу глобальных переменных, которые подгружаются из внешнего файла посредством parser.get (.getint, .getfloat и т.д.). Возникает такой же вопрос, как и с модулями

В каждой функции, чтобы иметь возможность контролировать ее результат, я использую логирование. Логирование также может использовать внешние модули (например, os), а также некоторые глобальные переменные (на чтение). Что с этим делать?

Переписывать.

Но для начала - попытаться спроектировать программу заново.

tailgunner ★★★★★
()
Ответ на: комментарий от Deleted

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

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

По остальному:

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

Да, питон умный, и кэширует модули, но такой подход считается плохой практикой (хотя есть и сторонники). В том же PEP8 рекомендуют всё импортировать в начале файла.

3) В каждой функции, чтобы иметь возможность контролировать ее результат, я использую логирование. Логирование также может использовать внешние модули (например, os), а также некоторые глобальные переменные (на чтение). Что с этим делать? Поставить заглушку, если условие if __name__ == '__main__' не выполняется?

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

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

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

Но, повторюсь, всё зависит от твоего конкретного кода. Возможно, если всё выбросить и грамотно переписать, все проблемы исчезнут.

risenshnobel ★★★
()

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

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

П.С. не программист, но мнение имею.

yvv ★★☆
()

1. Разбивать свою программу на модули по принципу «где что импортируется одинаково - то в один модуль». Это к примеру, а вообще - смотря какая цель у такого разбиения. Общие функции, которые предполагается переиспользовать в других программах собери в пакет. А основной модуль оставь отдельно.

2. Использовать ООП и функциональщину, строить контексты, хранить общие изменяемые объекты в них. Не забывать про os.environ.

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

4. ООП часто может быть полезно. Но если не понимаешь зачем оно тебе нужно в каждой конкретной ситуации - то лучше не используй. А без ООП dict вполне может заменить object.

ei-grad ★★★★★
()
Ответ на: комментарий от yvv

П.С. не программист, но мнение имею.

история всей ветки Development в одной фразе

Virtuos86 ★★★★★
()
Ответ на: комментарий от ei-grad

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

def a_plus_b(a):
    global b
    return a + b

b = 2

print(a_plus_b(2))
ei-grad ★★★★★
()
Последнее исправление: ei-grad (всего исправлений: 1)
Ответ на: комментарий от risenshnobel

В том же PEP8 рекомендуют всё импортировать в начале файла.

Ясно, спасибо.

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

logging, как выяснилось, по умолчанию пишет в системной кодировке, даже если файл изначально создавался в UTF-8. Возможно, это как-то настраивается, но мне в любом случае надо было понавешать сверху много условий, поэтому я ограничился write и print. Впрочем, это наименьшая из проблем. Мне достаточно указать в программе UseLog=False, и функция у меня задействована не будет.

Deleted
()
Ответ на: комментарий от yvv

Я тоже не программист :) На самом деле, главное, чтобы совет был дельный.

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

Я вот что подумал. Можно ли компилировать проекты в *.pyc и/или в бинарники (например, в *.exe через cx_freeze) так, чтобы в компилируемом коде незадействованные модули и функции были выброшены?

Deleted
()

придется во все функции вставлять import

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

ei-grad ★★★★★
()
Ответ на: комментарий от Deleted

компилировать проекты в *.pyc и/или в бинарники (например, в *.exe через cx_freeze) так, чтобы в компилируемом коде незадействованные модули и функции были выброшены?

нет, так не получится.

все-таки питон чересчур дружелюбный ЯП :-/

Virtuos86 ★★★★★
()
Ответ на: комментарий от ei-grad

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

Тут возникает дилемма. Есть куча (>100) переменных, которые необходимо использовать в качестве глобальных хотя бы на чтение. Такие переменные либо загоняются в самое начало исходника, либо читаются из конфига. Так вот: если мне для повторного задействования кода нужно загрузить из главного модуля любой примитив типа load_file(), то с программой придется таскать с собой и конфиг.

Deleted
()
Ответ на: комментарий от Deleted

Есть куча (>100) переменных

и нет никакой связи между этими переменными? они не делятся на изолированные группы?

anonymous
()
Ответ на: комментарий от Deleted

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

ei-grad ★★★★★
()
Ответ на: комментарий от Deleted

Если прямо все 100 вместе используются в каждой функции - засунь их в любой контейнер (dict, list, object, модуль, ...), и явно передавай в функции ссылку на этот контейнер. У object такие функции будут методами класса. Но слишком много переменных внутри класса это тоже антипаттерн. При использовании классов и объектов лучше не пренебрегать принципами ООП.

Если сильно хочется какого-нибудь syntax sugar, чтобы выглядело как будто ты используешь глобальные переменные - посмотри как во flask стек контекстов сделан. Но тогда придется этот syntax sugar тащить везде где захочешь эти функции использовать.

ei-grad ★★★★★
()
Ответ на: комментарий от anonymous

В конфиге у меня такие разделы: Linux settings, Windows settings, Mac settings, Variables, Integer Values, Floating Values, Boolean. Переменные разные - некоторые задают шрифт, другие - расположение каталогов, третьи (boolean) указывают, какие функции будут применены, а какие нет и т.п.

Deleted
()
Ответ на: комментарий от ei-grad

На самом деле, некоторые переменные, например, default_encoding, действительно лучше было бы сделать константами (поскольку мне удалось корректно читать/записывать файлы в UTF-8 как на линуксе, так и на винде).

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

Deleted
()
Последнее исправление: Deleted (всего исправлений: 1)
Ответ на: комментарий от ei-grad

Видимо, для небольших модулей лучше делать from main import <...> вместо from main import * и дальше писать десятки используемых функций, а также отдельно решать, что делать с используемыми параметрами.

Deleted
()

Первое: Настройки нужно хранить в .json файле.
Второе: json нужно парсить встроенным модулем json, преобразовывая в dict.
Третье: повторный импорт модуля моментален, python - это виртуальная машина. Он не интерпретатор. Сначала он весь код компилирует в спец байт код, потом только выполняет.
Четвёртое: то, с чем ты столкнулся, это проблема Функционального Программирования. Заметь, большинство модулей в питоне 2 и 3 написаны в Объектном стиле, несмотря на приспособленность к ФП. Они написаны так не просто так, а для понятного/удобного синтаксиса и ради хорошей архитектуры кода. Т.ч. бери пример с программистов самого питона и пиши как они - на классах, но без особого фанатизма как тут некоторые будут советовать, типа наследоваться от object :)

menangen ★★★★★
()
Последнее исправление: menangen (всего исправлений: 1)
Ответ на: комментарий от Deleted

Спасибо! Пойду читать про json.

Тут читать нечего. Это словарь преобразованный в строку.

yvv ★★☆
()
Ответ на: комментарий от menangen

Хахаха. Если конфиг из 10 значений, то обязательно юзать жесон? Который во многих реализациях позволяет делать такую вещь, как переполнение буфера.

gh0stwizard ★★★★★
()

Ничего не имею против питона, но, как много в этих строках:

set_include_path(join(PATH_SEPARATOR, array('foo/', 'bar/', 'baz/')));

function mainAutoload($componentName) {
    require_once $componentName;
}
spl_autoload_register('mainAutoload');

// и погнали
blah::staticMethod();
$a = new component();

Даже в говнопыхе есть: установка путей где искать компоненты (модули), кастомный автолоад (да, только для классов, но это и есть представление где класс === некий модуль, он же... о боже - файл), где require_once не будет повторно грузить файл с компонентом, а пых тупо возьмет отпарсенный контекст из кеша, да и название require_ONCE говорит само за себя. Все модули-классы-компоненты (назови как хош) грузятся автоматом с твоими кастомными пожеланиями и плюшками и только тогда, когда понадобятся где-то посреди скрипта, никак не раньше. Нахрена подрубать все сразу?

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

Во, кстати имитация импорта:

// file: bar/module1
<?php

abctract class module1 { public static function import() {} }

function test1() {}
function test2() {}
function test3() {}
function test4() {}

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

module1::import(); // ну или init или как назовешь
test1(); // доступна и выполняется, остальные тоже

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

Вобщем после «импорта» в глобальном скопе будут доступны и ф-ции и еще другие классы содержащиеся в этом файле.

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

deep-purple ★★★★★
()
Ответ на: комментарий от menangen

Настройки нужно хранить в .json файле.
json нужно парсить встроенным модулем json, преобразовывая в dict.

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

типа наследоваться от object

Хм, а от чего тогда надо наследоваться при написании класса в python 2, если не от object?

risenshnobel ★★★
()
Ответ на: комментарий от risenshnobel

Хм, а от чего тогда надо наследоваться при написании класса в python 2, если не от object?

Можно ни от чего не наследоваться (old-style classes). Вообще не заморачивайся, он семь строк держался достойно, но в конце его что-то понесло.

t184256 ★★★★★
()
Ответ на: комментарий от t184256

В конце на него напала бешеная собака, она то его и понесла.

menangen ★★★★★
()
Ответ на: комментарий от menangen

Парсинг строк key=value на перле занимает от 3 до 100 строк кода. Уверен на питоне также. А вот тащить зависимости почем лень на каждый чих, имхо, бэд практис.

gh0stwizard ★★★★★
()

то даже импорт какого-нибудь примитива из главного модуля займет несколько секунд

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

1) Ты это проверял? Может там один какой-то стремный модуль закрался?

2) Это точно сторонние модули, а не у тебя везде какая-то тормозная инициализация?

vertexua ★★★★★
()
Последнее исправление: vertexua (всего исправлений: 1)
Ответ на: комментарий от gh0stwizard

Кто-то писал о том, что нужно тащить зависимости (импорт модулей) на каждый чих? Пишешь 6 модулей. В седьмом импортириуешь каждый модуль один раз. Сводишь в единую точку. Из неё запускаешь программу. Всё.

menangen ★★★★★
()
Ответ на: комментарий от Deleted

какие функции будут применены, а какие нет

я надеюсь ты функцию саму переопределяешь, а не проверяешь каждый раз внутри (или вне) функции нужно ли её исполнять

anonymous
()
Ответ на: комментарий от tailgunner

Можно сделать demandimport в стиле Mercurial, но, ИМХО, тебе рано об этом думать.

Главное что тебе уже можно об этом думать.

anonymous
()
Ответ на: комментарий от vertexua

Вот кусок кода, где загружаются модули.

import os, sys, difflib, pyperclip, zipfile, rarfile, pyunpack, codecs, re, time, shutil, webbrowser, subprocess, enchant, logging, ntpath, posixpath
from charade.universaldetector import UniversalDetector
from configparser import SafeConfigParser
import tkinter as tk
from operator import itemgetter
import tkinter.messagebox as tkmes
import easygui as eg
import urllib.request, lxml
from lxml.html.clean import Cleaner, clean_html
import pymorphy2

sys_type=detect_os()
if sys_type=='win':
	#http://mail.python.org/pipermail/python-win32/2012-July/012493.html
	_tz = os.getenv('TZ')
	if _tz is not None and '/' in _tz:
		os.unsetenv('TZ')
	# Импортируем win-only модули 
	import pythoncom
	from win32com.shell import shell, shellcon
	import win32clipboard, win32com.client, win32con, win32api
# Загружается последним ввиду проблем с TZ (см. выше)
import datetime
Загружается это все на слабом нетбуке секунды 4. В принципе, не критично, но неплохо было бы и побыстрее.

Deleted
()
Ответ на: комментарий от anonymous

Главное что тебе уже можно об этом думать.

Странно, что это главное для тебя, но как скажешь.

tailgunner ★★★★★
()
Ответ на: комментарий от anonymous

Я делаю примерно так:

#DeleteImages=False
DeleteImages=load_option_bool(SectionBooleans,'DeleteImages')
#..............................
if DeleteImages:
	delete_files_by_mask(c_folder,['.gif','.png','.ppm','.jpg'])

Deleted
()
Ответ на: комментарий от Deleted

Может быть, вам нужен профайлер? Или банально time.time проверить время импорта каждого модуля и найти паршивую овцу?

menangen ★★★★★
()

на данный момент все оформлено в виде одного большого main.py

кучу глобальных переменных

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

  • убиться об стену;
  • если всё сделано правильно - этот пункт не понадобится.

без обид. ты и сам назвал свои главные проблемы. смысла тут спрашивать?

anonymous
()
Ответ на: комментарий от menangen

Я не думаю, что тут где-то есть «паршивая овца». Скорее всего, просто слишком много модулей.

Deleted
()

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

n04h
()
Ответ на: комментарий от Deleted

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

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.