Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
168 Глава 3. Типы коллекций Благодаря тому, что существует возможность распаковывать итери руемые объекты с помощью оператора *, мы можем распаковать ите ратор, возвращаемый функцией range(). Например, если представить, что у нас имеется функция calculate(), которая принимает четыре ар гумента, ниже приводятся несколько способов вызова этой функции с аргументами 1, 2, 3 и 4: calculate(1, 2, 3, 4) t = (1, 2, 3, 4) calculate(*t) calculate(*range(1, 5)) Во всех трех случаях функции передается четыре аргумента. Во вто ром случае распаковывается кортеж из четырех элементов, а в треть ем – распаковывается итератор, возвращаемый функцией range(). Теперь рассмотрим небольшую, но законченную программу, которая соединяет в себе все, что мы узнали к настоящему моменту, и впервые явно использует функцию записи в файл. Программа generate_test_ names1.py читает данные из файла с фамилиями и из файла именами, создает два списка, а затем записывает 100 случайно образованных имен в файл testnames1.txt. В программе используется функция random.choice(), которая извлека ет случайный элемент из последовательности, поэтому вполне воз можно, что в окончательном списке одно и то же имя может появиться несколько раз. Для начала рассмотрим функцию, возвращающую спи сок имен, а затем перейдем к остальной части программы: def get_forenames_and_surnames(): forenames = [] surnames = [] for names, filename in ((forenames, "data/forenames.txt"), (surnames, "data/surnames.txt")): for name in open(filename, encoding="utf8"): names.append(name.rstrip()) return forenames, surnames Во внешнем цикле for ... in выполняется обход двух элементных кортежей, каждый из которых распаковы вается в две переменные. Хотя списки могут быть чрез вычайно длинными, возврат их из функции выполняет ся очень эффективно, так как в языке Python использу ются ссылки на объекты, поэтому фактически функция возвращает всего лишь две ссылки на объекты. Внутри программ на языке Python всегда следует использовать запись путей к файлам в стиле операционной системы UNIX, поскольку в этом случае можно не применять экранирование служебных симво лов и такой прием одинаково хорошо работает на всех поддерживае мых платформах (включая и Windows). Если у нас имеется строка пути, например в переменной path, и нам необходимо вывести ее перед Распаковыва ние кортежей, стр. 133 Обход в цикле и копирование коллекций 169 пользователем, мы всегда можем импортировать модуль os и вызвать метод path.replace("\", os.sep) для замены прямых слешей на символ разделитель каталогов, используемый в текущей платформе. forenames, surnames = get_forenames_and_surnames() fh = open("testnames1.txt", "w", encoding="utf8") for i in range(100): line = "{0} {1}\n".format(random.choice(forenames), random.choice(surnames)) fh.write(line) Получив два списка, программа открывает выходной файл для записи и сохраняет объект файла в переменной fh («file handle» – дескриптор файла). После этого вы полняется 100 циклов и на каждой итерации создается строка, в конец которой добавляется символ перевода строки, и эта строка записывается в файл. Мы не исполь зуем переменную цикла i – она нужна исключительно для того, чтобы удовлетворить требования синтаксиса цикла for ... in. Предыдущий фрагмент программного кода, функция get_forenames_and_surnames() и инструк ция import образуют полную программу. В программе generate_test_names1.py мы объединяли элементы из двух отдельных списков в единую строку. Другой способ объединения элементов двух или более списков (или других итерируемых объектов) заключается в использовании функции zip(). Функция zip() принима ет один или более итерируемых объектов и возвращает итератор, кото рый в свою очередь возвращает кортежи. Первый кортеж включает в себя первые элементы всех итерируемых объектов, второй кортеж – вторые элементы и т. д., итерации прекращаются, как только содер жимое любого из итерируемых объектов будет исчерпано. Например: >>> for t in zip(range(4), range(0, 10, 2), range(1, 10, 2)): ... print(t) (0, 0, 1) (1, 2, 3) (2, 4, 5) (3, 6, 7) Несмотря на то, что итераторы, возвращаемые вторым и третьим вызо вами функции range(), могут вернуть по пять элементов, тем не менее первый итератор может воспроизвести всего четыре элемента, тем са мым ограничивая количество элементов, которые может вернуть функ ция zip(). Ниже приводится модифицированная версия программы, генерирую щей имена, но на этот раз под имя отводится 25 символов, а вслед за каждым именем выводится случайный год. Программа называется generate_test_names2.py и выводит результаты в файл testnames2.txt. Мы не приводим здесь программный код функции get_forenames_and_ Врезка «Чте ние и запись текстовых файлов», стр. 157 170 Глава 3. Типы коллекций surnames() и вызов функции open(), так как они не изменились, за ис ключением имени выходного файла. limit = 100 years = list(range(1970, 2013)) * 3 for year, forename, surname in zip( random.sample(years, limit), random.sample(forenames, limit), random.sample(surnames, limit)): name = "{0} {1}".format(forename, surname) fh.write("{0:.<25}.{1}\n".format(name, year)) Программа начинается с определения значения максимального числа имен, которые могут быть сгенерированы. Затем создается список лет – путем создания списка со значениями в диапазоне от 1970 до 2012 включительно, после чего этот список дублируется трижды, поэтому в окончательном списке каждый год встречается три раза. Это необхо димо потому, что функция random.sample() (используемая вместо ran dom.choice() ) принимает итерируемый объект и число элементов, кото рые требуется воспроизвести – это число не может быть меньше, чем число элементов, которые может вернуть итерируемый объект. Функ ция random.sample() возвращает итератор, который воспроизводит ука занное число элементов без повторений. Поэтому данная версия про граммы всегда будет воспроизводить уникальные имена. В цикле for ... in распаковывается каждый кортеж, возвращаемый функцией zip(). Нам требуется ограни чить длину каждого имени 25 символами, а для этого сначала нужно создать строку с полным именем, а затем вторым вызовом метода str.format() ограничить ее дли ну. Каждое имя выравнивается по левому краю, а для имен короче 25 символов производится дополнение стро ки точками. Дополнительная точка гарантирует, что имена, полностью занимающие поле вывода, все же бу дут отделяться от года хотя бы одной точкой. В завершение этого подраздела мы упомянем еще две функции, имею щие отношение к итерируемым объектам – sorted() и reversed(). Функция sorted() возвращает отсортированный список элементов, а функция reversed() просто возвращает итератор, который позволяет выполнить обход элементов заданного итератора в обратном порядке. Ниже приводится пример использования функции reversed(): >>> list(range(6)) [0, 1, 2, 3, 4, 5] >>> list(reversed(range(6))) [5, 4, 3, 2, 1, 0] Функция sorted() – более сложная, как показано в примере ниже: >>> x = [] >>> for t in zip(range(10, 0, 1), range(0, 10, 2), range(1, 10, 2)): Распаковыва ние кортежей, стр. 133 Метод str. format() , стр. 100 Обход в цикле и копирование коллекций 171 ... x += t >>> x [10, 0, 1, 9, 2, 3, 8, 4, 5, 7, 6, 7, 6, 8, 9] >>> sorted(x) [10, 9, 8, 7, 6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> sorted(x, reverse=True) [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 6, 7, 8, 9, 10] >>> sorted(x, key=abs) [0, 1, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10] В предыдущем фрагменте функция zip() возвращает кортежи, состоя щие из трех элементов: ( – 10, 0, 1) , ( – 9, 2, 3) и т. д. Оператор += допол няет список, то есть добавляет каждый элемент заданной последова тельности в конец списка. Первый вызов функции sorted() возвращает копию списка, отсортиро ванную в привычном порядке. Второй вызов возвращает копию спи ска, отсортированную в обратном порядке. В последнем вызове функ ции sorted() определена функция «key», к которой мы вернемся через несколько мгновений. Обратите внимание, что в языке Python функции являются самыми обычными объектами, поэтому они могут передаваться другим функ циям в виде аргументов и сохраняться в коллекциях без излишних формальностей. Не забывайте, что имя функции – это ссылка на объ ект функции, а круглые скобки, следующие за именем, сообщают ин терпретатору Python о необходимости вызова этой функции. Когда функции sorted() передается функция key (в данном случае – функция abs()), она будет вызываться для каждого элемента списка (каждый элемент будет передаваться функции в виде единственного аргумента), чтобы создать «декорированный» список. Затем выполня ется сортировка декорированного списка, после чего в качестве ре зультата возвращается недекорированный список. Мы легко можем использовать собственные функции в качестве аргумента key, в чем вы вскоре убедитесь. Например, мы можем выполнить сортировку без учета регистра симво лов, передав в аргументе key метод str.lower(). Если представить, что у нас имеется список ["Sloop", "Yawl", "Cutter", "schooner", "ketch"], мы можем отсортировать его без учета регистра символов, используя DSUсортировку (Decorate, Sort, Undecorate – декорирование, сорти ровка, обратное декорирование), всего одной строкой программного кода, передав нужную функцию в аргументе key или выполнив сорти ровку явно, как показано в следующих двух эквивалентных фрагмен тах программного кода: temp = [] for item in x: temp.append((item.lower(), item)) 172 Глава 3. Типы коллекций x = [] for key, value in sorted(temp): x = sorted(x, key=str.lower) x.append(value) Оба фрагмента воспроизводят новый список: ["Cutter", "ketch", "schoo ner", "Sloop", "Yawl"], хотя действия, которые они выполняют, не идентичны, потому что во фрагменте справа создается промежуточ ный список temp. В языке Python реализован адаптивный алгоритм устойчивой сорти ровки со слиянием, который отличается высокой скоростью и интел лектуальностью и особенно хорошо подходит для сортировки частич но отсортированных списков, что встречается достаточно часто. 1 Сло во «адаптивный» в названии алгоритма означает, что алгоритм сорти ровки адаптируется под определенные условия, например, учитывает наличие частичной сортировки данных. Слово «устойчивый» означа ет, что одинаковые элементы не перемещаются относительно друг дру га (в конце концов, в этом нет никакой необходимости), и слова «сор тировка со слиянием» – это общее название используемых алгоритмов сортировки. Когда выполняется сортировка списка целых чисел, строк или данных других простых типов, используется оператор «меньше чем» (<). Интерпретатор Python может сортировать коллек ции, содержащие другие коллекции, выполняя рекурсивный спуск на произвольную глубину. Например: >>> x = list(zip((1, 3, 1, 3), ("pram", "dorie", "kayak", "canoe"))) >>> x [(1, 'pram'), (3, 'dorie'), (1, 'kayak'), (3, 'canoe')] >>> sorted(x) [(1, 'kayak'), (1, 'pram'), (3, 'canoe'), (3, 'dorie')] Python отсортировал список кортежей, сравнив сначала первые эле менты каждого кортежа, а если они были равны – вторые элементы. В результате элементы были отсортированы на основе целых чисел, с использованием строк для дополнительной сортировки. Мы можем принудительно сначала отсортировать список по строкам, а дополни тельную сортировку выполнить по целым числам, определив простую функцию, которая будет использоваться в качестве аргумента key: def swap(t): return t[1], t[0] Функция swap() принимает кортеж из двух элементов и возвращает но вый кортеж из двух элементов, в котором элементы переставлены мес тами. Представим, что функцию swap() мы ввели в среде IDLE, тогда можно выполнить следующее: 1 Алгоритм был создан Тимом Петерсом (Tim Peters). Интересное описание и обсуждение алгоритма можно найти в файле listsort.txt, который постав ляется в составе исходных программных кодов Python. Обход в цикле и копирование коллекций 173 >>> sorted(x, key=swap) [(3, 'canoe'), (3, 'dorie'), (1, 'kayak'), (1, 'pram')] Кроме того, списки могут быть отсортированы непосредственно, без создания копии, с помощью метода list.sort(), который принимает те же необязательные аргументы, что и функция sorted(). Сортировка может применяться только к коллекциям, все элементы которых могут сравниваться друг с другом: sorted([3, 8, 7.5, 0, 1.3]) # вернет: [7.5, 0, 1.3, 3, 8] sorted([3, "spanner", 7.5, 0, 1.3]) # возбудит исключение TypeError Хотя первый список содержит числа разных типов (int и float), тем не менее эти типы могут сравниваться друг с другом, поэтому сортировка к такому списку вполне применима. Но во втором списке содержится строка, которую не имеет смысла сравнивать с числами, поэтому воз буждается исключение TypeError. Если необходимо отсортировать спи сок, содержащий целые числа, числа с плавающей точкой и строки, представляющие числа, можно попробовать передать в аргументе key функцию float(): sorted(["1.3", 7.5, "5", 4, "2.4", 1], key=float) Это выражение вернет список [ – 7.3, ' – 2.4', 1, '1.3', 4, '5'] . Обрати те внимание, что значения в списке не изменились, то есть строки так и остались строками. Если какаялибо строка не сможет быть преобра зована в число (например, "spanner"), будет возбуждено исключение TypeError Копирование коллекций Поскольку в языке Python повсюду используются ссыл ки на объекты, когда выполняется оператор присваива ния (=), никакого копирования данных на самом деле не происходит. Если справа от оператора находится лите рал, например, строка или число, в операнд слева запи сывается ссылка, которая указывает на объект в памя ти, хранящий значение литерала. Если справа находит ся ссылка на объект, в левый операнд записывается ссылка, указывающая на тот же самый объект, на кото рый ссылается правый операнд. Вследствие этого опера ция присваивания обладает чрезвычайно высокой ско ростью выполнения. Когда выполняется присваивание крупной коллекции, такой как длинный список, экономия времени становится более чем очевидной. Например: >>> songs = ["Because", "Boys", "Carol"] >>> beatles = songs Ссылки на объекты, стр. 29 174 Глава 3. Типы коллекций >>> beatles, songs (['Because', 'Boys', 'Carol'], ['Because', 'Boys', 'Carol']) Здесь была создана новая ссылка на объект (beatles), и обе ссылки ука зывают на один и тот же список – никакого копирования данных не производилось. Поскольку списки относятся к категории изменяемых объектов, мы можем вносить в них изменения. Например: >>> beatles[2] = "Cayenne" >>> beatles, songs (['Because', 'Boys', 'Cayenne'], ['Because', 'Boys', 'Cayenne']) Изменения были внесены с использованием переменной beatles, но это всего лишь ссылка, указывающая на тот же самый объект, что и ссыл ка songs. Поэтому любые изменения, произведенные с использованием одной ссылки, можно наблюдать с использованием другой ссылки. Часто это именно то, что нам требуется, поскольку копирование круп ных коллекций может оказаться дорогостоящей операцией. Кроме то го, это также означает, что имеется возможность передавать списки или другие изменяемые коллекции в виде аргументов функций, изме нять эти коллекции в функциях и пребывать в уверенности, что изме нения будут доступны после того, как функция вернет управление вы зывающей программе. Однако в некоторых ситуациях действительно бывает необходимо соз дать отдельную копию коллекции (то есть создать другой изменяемый объект). В случае последовательностей, когда выполняется оператор извлечения среза, например, songs[:2], полученный срез – это всегда независимая копия элементов. Поэтому скопировать последователь ность целиком можно следующим способом: >>> songs = ["Because", "Boys", "Carol"] >>> beatles = songs[:] >>> beatles[2] = "Cayenne" >>> beatles, songs (['Because', 'Boys', 'Cayenne'], ['Because', 'Boys', 'Carol']) В случае словарей и множеств копирование можно выполнить с помо щью методов dict.copy() и set.copy(). Кроме того, в модуле copy имеет ся функция copy.copy(), которая возвращает копию заданного объек та. Другой способ копирования встроенных типов коллекций заклю чается в использовании имени типа как функции, которой в качестве аргумента передается копируемая коллекция. Например: copy_of_dict_d = dict(d) copy_of_list_L = list(L) copy_of_set_s = set(s) Обратите внимание, что все эти приемы копирования создают поверх ностные копии, то есть копируются только ссылки на объекты, но не Примеры 175 сами объекты. Для неизменяемых типов данных, таких как числа и строки, это равносильно копированию (за исключением более высо кой эффективности), но для изменяемых типов данных, таких как вложенные коллекции, это означает, что ссылки в оригинальной кол лекции и в копии будут указывать на одни и те же объекты. Эту осо бенность иллюстрирует следующий пример: >>> x = [53, 68, ["A", "B", "C"]] >>> y = x[:] # поверхностное копирование >>> x, y ([53, 68, ['A', 'B', 'C']], [53, 68, ['A', 'B', 'C']]) >>> y[1] = 40 >>> x[2][0] = 'Q' >>> x, y ([53, 68, ['Q', 'B', 'C']], [53, 40, ['Q', 'B', 'C']]) Когда выполняется поверхностное копирование списка x, копируется ссылка на вложенный список ["A", "B", "C"]. Это означает, что третий элемент в обоих списках, x и y, ссылается на один и тот же список, по этому любые изменения, произведенные во вложенном списке, можно наблюдать с помощью любой из ссылок, x или y. Если действительно необходимо создать абсолютно независимую копию коллекции с про извольной глубиной вложенности, необходимо выполнить глубокое копирование: >>> import copy >>> x = [53, 68, ["A", "B", "C"]] >>> y = copy.deepcopy(x) >>> y[1] = 40 >>> x[2][0] = 'Q' >>> x, y ([53, 68, ['Q', 'B', 'C']], [53, 40, ['A', 'B', 'C']]) Здесь списки x и y, а также элементы, которые они содержат, полно стью независимы. Обратите внимание: с этого момента мы будем использовать термины ко пия и поверхностная копия как взаимозаменяемые, а когда будет под разумеваться глубокое копирование, об этом будет упоминаться явно. |