Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
458 Глава 8. Усовершенствованные приемы программирования Функцию filter() всегда можно заменить выражениемгенератором или генератором списков. Упрощение предполагает совместное использование функции и итери руемого объекта и получение в качестве результата отдельного значе ния. При этом используется следующий порядок работы: сначала функции передаются значения первого и второго элементов итерируе мого значения, затем вычисленный результат и значение третьего эле мента, затем вычисленный результат и значение четвертого элемента и т. д., пока не будут использованы все элементы. Это понятие поддер живается функцией functools.reduce() из модуля functools. Ниже при водятся две строки программного кода, выполняющие одни и те же вычисления: functools.reduce(lambda x, y: x * y, [1, 2, 3, 4]) # вернет: 24 functools.reduce(operator.mul, [1, 2, 3, 4]) # вернет: 24 В модуле operator имеются функции, реализующие действия всех опе раторов языка Python, призванные упростить программирование в функциональном стиле. Здесь во второй строке была задействована функция operator.mul(), чтобы избежать необходимости создавать лямбдафункцию, выполняющую умножение, как это сделано в пер вой строке. В языке Python имеется еще несколько встроенных функций, выпол няющих упрощение: all(), принимающая итерируемый объект и воз вращающая True, если для каждого элемента итерируемого объекта встроенная функция bool() возвращает значение True; any(), возвра щающая True, если хотя бы для одного элемента итерируемого объекта будет получено значение True; max(), возвращающая элемент итерируе мого объекта с наибольшим значением; min(), возвращающая элемент итерируемого объекта с наименьшим значением; sum(), возвращаю щая сумму значений элементов итерируемого объекта. Теперь, когда мы познакомились с ключевыми понятиями, рассмот рим несколько примеров. Начнем с двух способов получить суммар ный размер всех файлов в списке files: functools.reduce(operator.add, (os.path.getsize(x) for x in files)) functools.reduce(operator.add, map(os.path.getsize, files)) Использование функции map() часто дает более компактный программ ный код, чем эквивалентный генератор списков или выражениегене ратор, за исключением случаев, когда используется условное выраже ние. Здесь вместо выражения lambda x, y: x + y мы использовали функцию сложения operator.add(). Если бы нам потребовалось определить суммарный размер только фай лов с расширением .py, можно было бы отфильтровать все файлы, не являющиеся файлами с программным кодом на языке Python. Ниже приводятся три варианта реализации этого действия: Функциональное программирование 459 functools.reduce(operator.add, map(os.path.getsize, filter(lambda x: x.endswith(".py"), files))) functools.reduce(operator.add, map(os.path.getsize, (x for x in files if x.endswith(".py")))) functools.reduce(operator.add, (os.path.getsize(x) for x in files if x.endswith(".py"))) Вероятно, второй и третий вариант выглядят более предпочтительны ми, потому что они не требуют создавать лямбдафункцию, но выбор между использованием выражениягенератора (или генератораспи сков) и функциями map() и filter() – зачастую лишь вопрос личных предпочтений. Использование функций map(), filter() и functools.reduce() часто по зволяет устранить циклы, как было продемонстрировано в примерах выше. Эти функции особенно удобны, когда необходимо адаптировать программный код, написанный на функциональном языке програм мирования, при этом в языке Python обычно имеется возможность за менить функцию map() генератором списков, функцию filter() – гене ратором списков с условием и во многих случаях функцию func tools.reduce() можно заменить такими встроенными функциями, как all() , any(), max(), min() и sum(). Например: sum(os.path.getsize(x) for x in files if x.endswith(".py")) При этом получается тот же результат, что и в трех предыдущих при мерах, но программный код получился более компактным. В дополнение к функциям, реализующим действия опе раторов языка Python, модуль operator также предостав ляет функции operator.attrgetter() и operator.itemget ter() , первую из которых мы коротко рассматривали вы ше в этой главе. Обе они возвращают функции, которые затем могут вызываться для извлечения определенных атрибутов или элементов. Операция получения среза может использоваться для извлечения по следовательности, составляющей часть списка, а операция получения среза с заданным шагом может использоваться для извлечения после довательности частей списка (например, каждого третьего элемента, с помощью инструкции L[::3]); точно так же функция operator.item getter() может использоваться для извлечения последовательности произвольных частей, например, operator.itemgetter(4, 5, 6, 11, 18)(L). Функция, возвращаемая функцией operator.itemgetter(), не обяза тельно должна вызываться непосредственно, как показано в этом при мере, – ее можно сохранить и передавать в виде аргумента функции map() , filter() или functools.reduce(), а также использовать в слова рях, списках или генераторах множеств. Когда необходимо выполнить сортировку, можно определить ключе вую функцию. Эта функция может быть любой функцией, например, Функция operator. attrget ter(), стр. 428 460 Глава 8. Усовершенствованные приемы программирования лямбдафункцией, встроенной функцией или методом (таким как str.lower() ), а также функцией, возвращаемой функцией operator.at trgetter() . Например, предположим, что список L хранит объекты с атрибутом priority, тогда отсортировать список в порядке приорите тов можно следующим способом: L.sort(key=operator.attrgetter("prio rity")) В дополнение к модулям functools и operator, упоминавшимся выше, для обеспечения поддержки функционального стиля программирова ния может использоваться модуль itertools. Например, хотя можно выполнить итерации по двум или более спискам, применив к ним опе рацию конкатенации, но также можно реализовать альтернативный вариант с помощью функции itertools.chain(), как показано ниже: for value in itertools.chain(data_list1, data_list2, data_list3): total += value Функция itertools.chain() возвращает итератор, который сначала да ет последовательность значений из первой последовательности, затем последовательность значений из второй последовательности и т. д., пока не будут использованы все значения из всех последовательно стей. В модуле itertools имеется множество других функций, а в опи саниях к ним приводится множество маленьких, но полезных приме ров, с которыми стоит ознакомиться. Частично подготовленные функции Частичная подготовка функций – это создание функции из сущест вующей функции и некоторых аргументов, в результате чего получа ется новая функция, которая выполняет те же действия, что и ориги нальная функция, но некоторые ее аргументы оказываются фиксиро ванными и не могут передаваться вызывающим программным кодом. Ниже приводится очень простой пример: enumerate1 = functools.partial(enumerate, start=1) for lino, line in enumerate1(lines): process_line(i, line) В первой строке создается новая функция, enumerate1(). Она служит оберткой вокруг существующей функции (enumerate()) с именованным аргументом (start=1), поэтому при обращении к функции enumerate1() будет вызвана оригинальная функция с фиксированным аргументом и со всеми остальными аргументами, заданными во время вызова, в данном случае – с аргументом lines. Здесь функция enumerate1() была использована для обеспечения нумерации строк, начиная с 1. Использование частично подготовленных функций может упростить программный код, особенно, когда приходится вызывать одну и ту же функцию с одними и теми же аргументами снова и снова. Например, вместо того чтобы при обработке текстовых файлов в кодировке UTF8 Пример: valid.py 461 в каждом вызове функции open() указывать режим и кодировку, мож но просто создать пару функций с фиксированными аргументами: reader = functools.partial(open, mode="rt", encoding="utf8") writer = functools.partial(open, mode="wt", encoding="utf8") Теперь текстовые файлы можно открывать для чтения вызовом read er(filename) и для записи – вызовом writer(filename). Одной из наиболее типичных областей применения частично подго товленных функций является программирование графического интер фейса (о котором рассказывается в главе 13), где часто бывает удобно вызывать одну определенную функцию при нажатии на любую из мно жества кнопок. Например: loadButton = tkinter.Button(frame, text="Load", command=functools.partial(doAction, "load")) saveButton = tkinter.Button(frame, text="Save", command=functools.partial(doAction, "save")) В данном примере используется библиотека графического интерфейса tkinter , которая поставляется как стандартная часть Python. Класс tkinter.Button используется для создания кнопок – в этом примере соз даются две такие кнопки, обе они находятся в пределах одного и того же фрейма и на каждой отображается текст, указывающий их назна чение. Во время создания каждой кнопки в аргументе command указыва ется функция, которая должна вызываться библиотекой tkinter при нажатии кнопки, в данном случае это функция doAction(). Здесь была использована частично подготовленная функция, чтобы гарантиро вать, что первым аргументом в вызове функции soAction() будет стро ка, определяющая, какая кнопка была нажата, благодаря чему doAc tion() сможет определить, какое действие следует выполнять. Пример: valid.py В этом разделе мы объединим дескрипторы с декорато рами классов, чтобы реализовать мощный механизм соз дания атрибутов с проверкой. До сих пор при необходимости обеспечить проверку кор ректности значения, записываемого в атрибут, мы опи рались на свойства (то есть создавали методы чтения и записи). Недостаток такого подхода заключается в том, что программный код, реализующий проверку, необхо димо добавлять в каждый класс для каждого атрибута, где такая проверка необходима. Было бы намного проще и удобнее, если бы имелась возможность добавлять в клас сы атрибуты со встроенной проверкой корректности. Ниже приводятся несколько примеров синтаксиса, ко торый было бы желательно иметь: Дескрип торы, стр. 432 Декораторы классов, стр. 438 462 Глава 8. Усовершенствованные приемы программирования @valid_string("name", empty_allowed=False) @valid_string("productid", empty_allowed=False, regex=re.compile(r"[AZ]{3}\d{4}")) @valid_string("category", empty_allowed=False, acceptable= frozenset(["Consumables", "Hardware", "Software", "Media"])) @valid_number("price", minimum=0, maximum=1e6) @valid_number("quantity", minimum=1, maximum=1000) class StockItem: def __init__(self, name, productid, category, price, quantity): self.name = name self.productid = productid self.category = category self.price = price self.quantity = quantity Все атрибуты класса StockItem требуют проверки. Напри мер, атрибут productid может содержать только непус тую строку, которая начинается с трех алфавитных сим волов верхнего регистра и заканчивается четырьмя циф рами. Атрибут category может содержать только непус тую строку, которая должна иметь одно из указанных значений. И атрибут quantity может быть только числом в диапазоне от 1 до 1000 включительно. Если попытать ся записать недопустимое значение, будет возбуждено исключение. Проверка реализуется посредством объединения декора торов классов и дескрипторов. Как отмечалось выше, де кораторы классов могут принимать только один аргу мент – декорируемый класс. Поэтому здесь используется прием, продемонстрированный при первом обсуждении декораторов классов, в результате применения которого были созданы функции valid_string() и valid_number(), принимающие любые желаемые аргументы и возвра щающие декоратор, который в свою очередь принимает класс и возвращает модифицированную версию класса. Рассмотрим функцию valid_string(): def valid_string(attr_name, empty_allowed=True, regex=None, acceptable=None): def decorator(cls): name = "__" + attr_name def getter(self): return getattr(self, name) def setter(self, value): assert isinstance(value, str), (attr_name + " must be a string") if not empty_allowed and not value: raise ValueError("{0} may not be empty".format( Регулярные выражения, стр. 524 Декораторы классов, стр. 438 Пример: valid.py 463 attr_name)) if ((acceptable is not None and value not in acceptable) or (regex is not None and not regex.match(value))): raise ValueError("{0} cannot be set to {1}".format( attr_name, value)) setattr(self, name, value) setattr(cls, attr_name, GenericDescriptor(getter, setter)) return cls return decorator Функция начинается с того, что создает функциюдекоратор, которая принимает класс в виде единственного аргумента. Декоратор добавля ет в декорируемый класс два атрибута: частный атрибут данных и де скриптор. Например, когда функция valid_string() вызывается с име нем атрибута «productid», класс StockItem получает атрибут __produc tid , который будет хранить строку идентификатора продукта, и деск риптор productid атрибута, который будет использоваться для доступа к значению. Например, если создать экземпляр класса инструкцией item = StockItem("TV", "TVA4312", "Electrical", 500, 1) , мы сможем по лучать значение идентификатора продукта как item.productid и изме нять его инструкцией, например, item.productid = "TVB2100". Функция чтения, создаваемая декоратором, просто использует гло бальную функцию getattr(), возвращающую значение частного атри бута данных. Функция записи реализует проверку и в конце, для за писи нового (и корректного) значения в атрибут данных, использует функцию setattr(). В действительности частный атрибут данных соз дается при первой попытке присвоить ему значение. После создания функций чтения и записи снова вызывается функция setattr() – на этот раз, чтобы создать новый атрибут класса с задан ным именем (например, productid) и с дескриптором типа GenericDe scriptor в виде значения. В конце функциядекоратор возвращает мо дифицированный класс, а функция valid_string() возвращает функ циюдекоратор. Функция valid_number() по своей структуре идентична функции va lid_string() , она отличается только принимаемыми аргументами и реализацией проверки в функции записи, поэтому мы не будем пока зывать ее здесь. (Полный программный код примера вы найдете в фай ле Valid.py.) Последнее, что нам осталось описать, – это дескриптор GenericDescrip tor ; и это, как оказывается, самая простая часть примера: class GenericDescriptor: def __init__(self, getter, setter): self.getter = getter self.setter = setter def __get__(self, instance, owner=None): 464 Глава 8. Усовершенствованные приемы программирования if instance is None: return self return self.getter(instance) def __set__(self, instance, value): return self.setter(instance, value) Дескриптор используется для хранения функций чтения и записи для каждого атрибута и просто передает всю работу по чтению и записи этим функциям. В заключение В этой главе мы получили массу дополнительных сведений о поддержке процедурного и объектноориентированного программирования в язы ке Python и приобрели некоторое представление о поддержке функ ционального программирования. В первом разделе мы узнали, как создавать выражениягенераторы, и познакомились с функциямигенераторами поближе. Мы также уз нали, как динамически импортировать модули и как получать доступ к функциональным возможностям таких модулей, а также динамиче ски выполнять программный код. В этом разделе мы увидели приме ры создания и использования рекурсивных функций и нелокальных переменных. Мы также узнали, как создавать собственные декорато ры функций и методов и как определять и и использовать аннотации функций. Во втором разделе главы мы исследовали различные дополнительные аспекты объектноориентированного программирования. Сначала мы больше узнали о доступе к атрибутам, например, с помощью специаль ного метода __getattr__(). Затем мы познакомились с функторами и увидели, как они могут использоваться для получения функций с со стоянием, что также может быть достигнуто посредством добавления свойств к функции или за счет использования замыканий – оба прие ма также рассматриваются в этой главе. Мы узнали, как использовать инструкцию with вместе с менеджерами контекста и как создавать соб ственные менеджеры контекста. Поскольку объекты файлов в языке Python, помимо всего прочего, являются еще и менеджерами контек ста, мы можем выполнять операции с файлами с использованием структур try with ... except, чтобы гарантировать закрытие открытых файлов без необходимости реализовать блоки finally. Затем во втором разделе мы перешли к описанию дополнительных осо бенностей объектноориентированного программирования, начав с опи сания дескрипторов. Дескрипторы могут использоваться самыми раз ными способами, и эта технология лежит в основе многих стандарт ных декораторов Python, таких как @property и @classmethod. Мы узна ли, как создавать собственные дескрипторы, и увидели три различных примера их использования. Затем мы исследовали декораторы клас Упражнения 465 сов и увидели, что с их помощью можно модифицировать классы поч ти так же, как с помощью декораторов функций можно модифициро вать функции. В последних трех подразделах второго раздела мы познакомились с поддержкой в языке Python абстрактных базовых классов, множест венным наследованием и метаклассами. Мы узнали, как создавать собственные классы, использующие стандартные абстрактные базо вые классы, и как создавать собственные абстрактные классы. Мы также увидели, как использовать множественное наследование для объединения в одном классе возможностей нескольких классов. А из описания метаклассов мы узнали, как оказывать влияние на процесс создания и инициализации классов (в противоположность экземпля рам классов). В предпоследнем разделе были представлены некоторые функции и мо дули, которые в языке Python обеспечивают поддержку функциональ ного программирования. Мы узнали, как использовать распростра ненные идиомы функционального программирования, такие как ото бражение, фильтрация и упрощение. Мы также увидели, как созда вать частично подготовленные функции. В последнем разделе было показано, как, объединив декораторы клас сов и дескрипторы, можно реализовать мощный и гибкий механизм создания атрибутов со встроенной проверкой значений. Эта глава завершает описание самого языка программирования Py thon. Не все особенности языка были рассмотрены в этой и в предыду щих главах, но особенности, котрые не были охвачены нами, исполь зуются очень редко. Ни в одной из последующих глав не будет пред ставлено новых особенностей языка, однако во всех этих главах будут использоваться модули из стандартной библиотеки, которые не были описаны прежде, и в некоторых из них будут использоваться приемы, продемонстрированные в этой и в предыдущих главах. Кроме того, в программах, которые будут демонстрироваться в следующих главах, отсутствуют ограничения, применявшиеся ранее (то есть ограничения на использование только тех аспектов языка, которые были представ лены к текущему моменту), поэтому они являются наиболее характер ными примерами в этой книге. |