Автоматизирай Това Начало на книгата

Списъци

Още една тема, която е нужна за писането на по-ефективни програми, е типът данни списък (list) и сходния тип комплект (кортеж, tuple). Списъците и комплектите могат да съдържат няколко стойности, което улеснява писането на програми, обработващи големи количества данни. Списъците могат да се използват за подреждане на данни в йерархия, защото един списък може да съдържа други списъци.

В тази глава се обсъждат основите на списъците. Ще се споменат и методите, които са функции на стойности от определени типове данни. Накратко се минава през последователните типове данни (списък, комплект и низ) и се сравняват един с друг. Следващата глава се занимава с типа данни речник.

Списъчен тип данни

Списък (list) е стойност, която съдържа няколко стойности в подредена последователност. С терминът списъчна стойност (list value) се обозначава самият списък (който може да бъде записан в променлива или предаден на функция, както всяка друга стойност), а не стойностите в него. Списъчна стойност изглежда по този начин : ['котка', 'прилеп', 'плъх', 'слон']. Подобно на низовете, които използват единична кавичка за начало и край на низа, списъка започва с отваряща средна (квадратна) скоба и завършва със затваряща средна скоба, []. Стойностите в списъка се наричат елементи (items). Елементите са разделени със запетаи (comma-delimited). Ето пример от интерактивната конзола:

>>> [1, 2, 3]
[1, 2, 3]
>>> ['котка', 'прилеп', 'плъх', 'слон']
['котка', 'прилеп', 'плъх', 'слон']
>>> ['здравей', 3.1415, True, None, 42]
['здравей', 3.1415, True, None, 42]
➊ >>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam
['котка', 'прилеп', 'плъх', 'слон']

На променливата spam ➊ се присвоява само една стойност - списъчната стойност. А тази списъчна стойност съдържа другите стойности. Стойността [] е празен списък без стойности, както '' е празен низ.

Използване на индекс за получаване на определена стойност от списък

Нека променливата spam съдържа списъка ['котка', 'прилеп', 'плъх', 'слон']. В Python код spam[0] ще се пресметне до 'котка', spam[1] ще има резултат 'прилеп' и т.н. Числото в квадратните скоби, които са след списъка, се нарича индекс (index). Първата стойност в списъка е на позиция с индекс 0, втората стойност е с индекс 1, третата е на позиция 3 и така нататък. Следващото изображение показва променливата spam, съдържаща списък, както и до кой елемент се премсмятат изрази с индекси. Първият индекс е 0, така че последният индекс е равен на размера на списъка минус едно. Така последният индекс за списък с четири елемента е равен на 3.

Съответствието между индексите и стойностите в списъка
Съответствието между индексите и стойностите в списъка.

Следва пример от интерактивната конзола.

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[0]
'котка'
>>> spam[1]
'прилеп'
>>> spam[2]
'плъх'
>>> spam[3]
'слон'
>>> ['котка', 'прилеп', 'плъх', 'слон'][3]
'слон'
➊ >>> 'Здравей, ' + spam[0]
➋ 'Здравей, котка'
>>> 'Един ' + spam[1] + ' изяде ' + spam[0] + '.'
'Един прилеп изяде котка.'

Изразът 'Здравей, ' + spam[0] се пресмята до 'Здравей, ' + 'котка', защото spam[0] се пресмята до 'котка'. От своя страна този израз се пресмята до низа 'Здравей, котка' ➋.

Python дава грешка IndexError при използване на индекс, който е по-голям от броя на стойностите в списъка.

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[10000]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        spam[10000]
IndexError: list index out of range

Индексите са само цели числа, не могат да се използват дроби. Следващият пример в интерактивната конзола причинява TypeError:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[1]
'прилеп'
>>> spam[1.0]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        spam[1.0]
TypeError: list indices must be integers or slices, not float >>> spam[int(1.0)]
'прилеп'

Списъците могат да съдържат други списъци. Стойностите в тези списъци могат да се достъпят с използване на няколко индекса:

>>> spam = [['котка', 'прилеп'], [10, 20, 30, 40, 50]]
>>> spam[0]
['котка', 'прилеп']
>>> spam[0][1]
'прилеп'
>>> spam[1][4]
50

Първият индекс показва коя списъчна стойност да се използва, а вторият указва стойността в този списък. Например spam[0][1] принтира 'прилеп', втората стойност в първия списък. Програмата принтира целият подсписък, ако се използва само един индек.

Отрицателни индекси

Индексите започват от 0 и нарастват, но могат да се използват и отрицателни числа. Стойност -1 сочи последния елемент в списъка, стойност -2 е предпоследния елемент и т.н. Демонстрация в интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[-1]
'слон'
>>> spam[-3]
'прилеп'
>>> 'Може ли ' + spam[-1] + ' да се страхува от ' + spam[-3] + '?'
'Може ли слон да се страхува от прилеп?'

Получаване на списък от друг списък с разрез

С индекс може да се вземе единична стойност от списък. Разрез (slice) взема няколко стойности от един списък, във формата на нов списък. Разрезът се пише между квадратни скоби, като индекс, и се състои от две числа, разделени с двуеточие. Разликата между индекси и разрези:

  • spam[2] е списък с индекс (едно число)
  • spam[1:4] е списък с разрез (две числа)

Първото число от разреза е началния индекс. Второто число е крайният индекс. Разрезът е от първия индекс до втория индекс, без да го включва. Резултатът от разреза е нов списък. Пример в интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[0:4]
['котка', 'прилеп', 'плъх', 'слон']
>>> spam[1:3]
['прилеп', 'плъх']
>>> spam[0:-1]
['котка', 'прилеп', 'плъх']

За краткост може да се пропусне една или двете стойности в разреза. Пропускането на първия индекс е същото като използване на 0, началото на списъка. Пропускането на втория индекс е същото като използване на размера на списъка, което ще вземе списъка до края. Примери от интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[:2]
['котка', 'прилеп']
>>> spam[1:]
['прилеп', 'плъх', 'слон']
>>> spam[:]
['котка', 'прилеп', 'плъх', 'слон']

Вземане на размера на списъка с функцията len()

При подаване на списък на функцията len(), тя връща броят на стойности в списъка (точно както връща броя символи в низ, ако се подаде такава стойност). Пример от интерактивната конзола:

>>> spam = ['котка', 'куче', 'мишка']
>>> len(spam)
3

Промяна на стойност в списък

Обикновено от лявата страна на оператора за присвояване има име на променлива, като spam = 42. Но там може да има индекс от списък, ковто позволява промяна на стойността за този индекс. Например spam[1] = 'зебра' означава "На стойността за индекс 1 в списъка spam се присвоява стойност 'зебра'." примери в интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam[1] = 'зебра'
>>> spam
['котка', 'зебра', 'плъх', 'слон']
>>> spam[2] = spam[1]
>>> spam
['котка', 'зебра', 'зебра', 'слон']
>>> spam[-1] = 12345
>>> spam
['котка', 'зебра', 'зебра', 12345]

Слепване и размножаване на списъци

Списъците могат да се слепват (concatenation) и размножат (replication), подобно на низовете. Операторът + комбинира два списъка и създава нов списък. Операторът * може да се използва със списък и цяло число за размножаване на списъка. Примери от интерактивната конзола:

>>> [1, 2, 3] + ['A', 'B', 'C']
[1, 2, 3, 'A', 'B', 'C']
>>> ['X', 'Y', 'Z'] * 3
['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
>>> spam = [1, 2, 3]
>>> spam = spam + ['A', 'B', 'C']
>>> spam
[1, 2, 3, 'A', 'B', 'C']

Премахване на стойности от списък с del

Конструкцията del изтрива стойност от списък по индекс. Изтриването премества напред стойностите след изтритата. Пример от интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> del spam[2]
>>> spam
['котка', 'прилеп', 'слон']
>>> del spam[2]
>>> spam
['котка', 'прилеп']

del може да се използва и с прости променливи, които се изтриват, като операция, която е обратната на присвояването. При опит за използване на изтрита променлива се получава грешка NameError, защото променливата вече не съществува. На практика почти никога не се налага изтриване на проста променлива. Конструкцията del се използва основно за изтриване на стойности от списък.

Работа със списъци

Първоначално при писане на програми е изкушаващо да се използват много отделни променливи за съхраненеие на група от подобни стойности. Ако искам да запиша имената на домашните си любимци мога да напиша код като:

petName1 = 'Кари'
petName2 = 'Рафи'
petName3 = 'Чарли'
petName4 = 'Шери'
petName5 = 'Баба Костенурка'
petName6 = 'Чарлз'

Това е лош начин за писане на код (също така нямам толкова домашни любимци :) ). На първо място - програмата не може да съхранява повече домашни любимци, отколкото са променливите (в случая - 6). Също така този тип програми предполагат много копиран или почти еднакъв код. Ето колко еднакъв код има в следната програма, записана като allMyPets1.py:

print('Въведи името на любимец 1:')
petName1 = input()
print('Въведи името на любимец 2:')
petName2 = input()
print('Въведи името на любимец 3:')
petName3 = input()
print('Въведи името на любимец 4:')
petName4 = input()
print('Въведи името на любимец 5:')
petName5 = input()
print('Въведи името на любимец 6:')
petName6 = input()
print('Имената на любимците са :')
print(petName1 + ' ' + petName2 + ' ' + petName3 + ' ' + petName4 + ' ' + petName5 + ' ' + petName6 )

Използването на една променлива, съдържаща списък, е много по-добре от множество повторени променливи. За пример следва нова и подобрена версия на предната програма. Новата версия използва един списък, който може да съдържа произволно количетво домашни любимци. Ето и програмата, записана като allMyPets2.py:

petNames = []
while True:
    print('Въведи име на любимец ' + str(len(petNames) + 1) + ' (Или натисни ENTER за край):')
    name = input()
    if name == '':
        break
    petNames = petNames + [name] # слепване на списъци
print('Имената на любимците са:')
for name in petNames:
    print(' ' + name)

При изпълнение на програмата се поучава подобен резултат:

Въведи име на любимец 1 (Или натисни ENTER за край):
Кари
Въведи име на любимец 2 (Или натисни ENTER за край):
Рафи
Въведи име на любимец 3 (Или натисни ENTER за край):
Чарли
Въведи име на любимец 4 (Или натисни ENTER за край):
Шери
Въведи име на любимец 5 (Или натисни ENTER за край):
Баба Костенурка
Въведи име на любимец 6 (Или натисни ENTER за край):
Чарлз
Въведи име на любимец 7 (Или натисни ENTER за край):

Имената на любимците са:
Кари
Рафи
Чарли
Шери
Баба Костенурка
Чарлз

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

В Глава 2 се разгледа как се използва цикъл for за изпълнение на код определен брой пъти. Техически погледнато, цикълът for повтаря тялото си веднъж за всеки елемент от списък. Ако се изпълни следния код:

for i in range(4):
    print(i)

ще има следния резултат:

0
1
2
3

Извикването range(4) всъщност връща поредица, която за Python е същата като [0, 1, 2, 3] (поредците са описани по-надолу). Следващата програма има същия резултат като по-горната:

for i in [0, 1, 2, 3]
    print(i)

Този for цикъл реално изпълнява тялото си с стойност на i равна на поредната стойност от списъка [0, 1, 2, 3].

Често срещана техника в Python е използването на range(len(someList)) във for цикъл за завъртане по индексите в списък. Пример от интерактивната конзола:

>>> supplies = ['моливи', 'хартия', 'принтери', 'мишки']
>>> for i in range(len(supplies)):
... print('Индекс ' + str(i) + ' в supplies е: ' + supplies[i])

Индекс 0 в supplies е: моливи
Индекс 1 в supplies е: хартия
Индекс 2 в supplies е: принтери
Индекс 3 в supplies е: мишки

Използването на range(len(supplies)) в цикъла for е удобно, защото така в тялото на цикъла може лесно да се използва индекса (съхранен в променливата i) и стойността на този индекс (supplies[i]). Допълнителен бонус от използването на range(len(supplies)) е автоматичното извъртане на всички индекси в supplies, без значение колко са стойностите в списъка.

Операторите in и not in

Операторите in и not in се използват за определяне дали в списък има или не дадена стойност. Както останалите опратори, in и not in се използват в изрази. Необходими са две стойности : стойност, която да се търси и списък, в който да се търси. Тези изрази се пресмятат до булева стойност. Примери от интерактивната конзола:

>>> 'Как си' in ['Здравей', 'Здрасти', 'Как си', 'Ехо']
True
>>> spam = ['Здравей', 'Здрасти', 'Как си', 'Ехо']
>>> 'котка' in spam
False
>>> 'Как си' not in spam
False
>>> 'котка' not in spam
True

Пример за използването на операторите е следващата програма. Тя позволява на потребителя да напише име на домашен любимец и проверява дали името вече го има. Следва програмата, записана като myPets.py:

myPets = ['Кари', 'Рафи', 'Чарли']
print('Въведи име на любимец:')
name = input()
if name not in myPets:
    print('Нямам домашен любимец с име ' + name)
else:
    print(name + ' е от моите домашни любимци')

Резултатът може да изглежда така :

Въведи име на любимец:
Бруно
Нямам домашен любимец с име Бруно

Множествено присвояване

Множественото присвояване (multiple assignment), с по-техническо име разопаковане на комплект (tuple upacking), е съкратен начин за присвояване на няколко променливи едновременно. Техните стойности са елементите от даден списък. Вместо да се пише:

>>> cat = ['дебел', 'сив', 'шумен']
>>> size = cat[0]
>>> color = cat [1]
>>> disposition = cat[2]

Може да се напише този ред:

>>> cat = ['дебел', 'сив', 'шумен']
>>> size, color, disposition = cat

Python предизвиква ValueError, ако броят на променливите не съвпада с броя на елементите в списъка.

>>> cat = ['дебел', 'сив', 'шумен']
>>> size, color, disposition, name = cat
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        size, color, disposition, name = cat
ValueError: not enough values to unpack (expected 4, got 3)

Използване на функцията enumerate() за списъци

функцията enumerate() може да се използва вместо техниката с range(len(someList)) при цикъл for. enumerate() връща две стойности - индекс на елемент и самият елемент, при всяко завъртане на цикъла. Следва примерна програма, която има същият резултат като тази от "Използване на for цикли със списъци" по-нагоре:

>>> supplies = ['моливи', 'хартия', 'принтери', 'мишки']
>>> for index, item in enumerate(supplies):
... print('Индекс ' + str(index) + ' в supplies е: ' + item)

Индекс 0 в supplies е: моливи
Индекс 1 в supplies е: хартия
Индекс 2 в supplies е: принтери
Индекс 3 в supplies е: мишки

Функцията enumerate() е полезна, когато в тялото на цикъла трябва да се използват и елемента, и неговия индекс.

Използване на функциите random.choice() и random.shuffle() за списъци

Модулът random има двойка функции, приемащи списък както параметър. Функцията random.choice() връща случайно избран елемент от списъка. Пример от интерактивната конзола:

>>> import random
>>> pets = ['Куче', 'Котка', 'Лос']
>>> random.choice(pets)
'Лос'
>>> random.choice(pets)
'Лос'
>>> random.choice(pets)
'Куче'

Извикването random.choice(someList) може да се приема като кратка форма на someList[random.randint(0, len(someList) - 1)]

Функцията random.shuffle() размества елементите на списъка. Тази функция нр връща нов списък, а променя подадения списък. Пример от интерактивната конзола:

>>> import random
>>> people = ['Ангел', 'Борко', 'Васко', 'Георги']
>>> random.shuffle(people)
>>> people
['Васко', 'Георги', 'Борко', 'Ангел']
>>> random.shuffle(people)
>>> people
['Георги', 'Борко', 'Васко', 'Ангел']

Сложни (комбинирани) оператори за присвояване

Често при присвояване на стойност на променлива се използва текущата стойност на променливата. Ако например променливата spam има стойност 42, за увеличаване с единица ще се използва подобен код:

>>> spam = 42
>>> spam = spam + 1
>>> spam
43

За краткост може да се изполва комбинираният оператор за присвояване +=:

>>> spam = 42
>>> spam += 1
>>> spam
43

Има комбинирани оператори за присвояване за следните оператори, описани в следващата таблица.

Комбинирани оператори за присвояване
Комбиниран оператор Еквивалентен стандартен оператор
spam += 1 spam = spam + 1
spam -= 1 spam = spam - 1
spam *= 1 spam = spam * 1
spam /= 1 spam = spam / 1
spam %= 1 spam = spam % 1

Операторът += може да се използва за слепване на низове и списъци, а операторът *= - за размножаване на низове и списъци. Пример от интерактивната конзола:

>>> spam = 'Здравей, '
>>> spam += ' свят!'
>>> spam
'Здравей, свят!'
>>> bacon = ['телевизор']
>>> bacon *= 3
>>> bacon
['телевизор', 'телевизор', 'телевизор']

Методи

Метод (method) е функция, която се "извиква на" стойност. Ако в spam има запазен списък, неговият списъчен метод index() се извиква така : spam.index('Здравей'). Името на метода е след стйноста, разделен с точка.

Всеки тип данни има собствен набор методи. Типът данни списък има няколко полезни метода, служещи за намиране, добавяне, премахване и други манипулации на стойностите в списъка.

Намиране на стойност в спиък с метода index()

Списъчните стойности имат метод index(), на който се подава стойност и, ако списъкът съдържа тази стойност, връща индекса на стойността. Python дава ValueError, ако в списъка няма такава стойност. Пример от интерактивната конзола:

>>> spam = ['Здравей', 'Здрасти', 'Как си', 'Ехо']
>>> spam.index('Здравей')
0
>>> spam.index('Ехо')
3
>>> spam.index('Как си Как си Как си')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        spam.index('Как си Как си Как си')
ValueError: 'Как си Как си Как си' is not in list

В списъка може да има повтарящи се стойности. В този случай функцията връща индекса на първото срещане на стойността. Ето пример от интерактивната конзола, в който да се забележи, че index() връща 1, а не 3:

>>> spam = ['Зоя', 'Пенка', 'Фидана', 'Пенка']
>>> spam.index('Пенка')
1

Добавяне на стойности в списък с методите append() и insert()

Методите append() и insert() се използват за добавяне на стойности към списък. Пример за добавяне на стойност към списък с append() от интерактивната конзола :

>>> spam = ['котка', 'куче', 'прилеп']
>>> spam.append('лос')
>>> spam
['котка', 'куче', 'прилеп', 'лос']

Извикването на метода append() добавя аргумента му в края на списъка. Методът insert() може да добави вмъкне стойност на всяки индекс от списъка. Първият аргумент на insert() е индексът на новата стойност, а вторият е новата стойност, която ще бъде вмъкната. Пример от интерактивната конзола:

>>> spam = ['котка', 'куче', 'прилеп']
>>> spam.insert(1, 'пиле')
>>> spam
['котка', 'пиле', 'куче', 'прилеп']

Трябва да се отбележи, че кодът е spam.append('лос') и spam.insert(1, 'пиле'), а не spam = spam.append('лос') и spam = spam.insert(1, 'пиле'). Нито един от двата метода append() и insert() не връща като резултат новата стойност на spam. (Всъщност връщат None, така че със сигурност не трябва тази стойност да се присвоява на spam). Вместо това списъкът се променя на място. По-късно промяната на място се разглежда в по-големи подробности.

Методите са за определен тип данни. Методите append() и insert() са списъчни методи и могат да се викат само на списъчни стойности, но не и на други стойности като низ или целочислен. Следва демонстрация на съобщенията за грешки AttributeError, които могат да се получат:

>>> eggs = 'Здравей'
>>> eggs.append('свят')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        eggs.append('свят')
AttributeError: 'str' object has no attribute 'append'
>>> bacon = 42
>>> bacon.insert(1, 'свят')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        bacon.insert(1, 'свят')
AttributeError: 'int' object has no attribute 'insert'

Премахване на стойности от списък с метода remove()

Методът remove() премахва от списъка стойността, която е подадена на метода. Пример от интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam.remove('прилеп')
>>> spam
['котка', 'плъх', 'слон']

При опит за изтриване на стойност, която не съществува в списъка, се получава грешка ValueError. Пример от интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'слон']
>>> spam.remove('пиле')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        spam.remove('пиле')
ValueError: list.remove(x): x not in list

Стойността ще бъде премахната само веднъж, ако я има няколко пъти. Пример от интерактивната конзола:

>>> spam = ['котка', 'прилеп', 'плъх', 'котка', 'котка', 'котка']
>>> spam.remove('котка')
>>> spam
['прилеп', 'плъх', 'котка', 'котка', 'котка']

Конструкцията del се използва, когато се знае индекса на стойността, която да се премахне. Методът remove() се използва, ако се знае стойността, която да се премахне.

Сортиране на стойностите в списък с метода sort()

Списъците с числа или низове могат да се сортират с помощта на метода sort(). Пример от интерактивната конзола:

>>> spam = [2, 5, 3.14, 1, -7]
>>> spam.sort()
>>> spam
[-7, 1, 2, 3.14, 5]
>>> spam = ['мравки', 'котки', 'кучета', 'язовци', 'зебри']
>>> spam.sort()
>>> spam
['зебри', 'котки', 'кучета', 'мравки', 'язовци']

Сортирането може да се извърши в обратен ред с помощта на именувания аргумент reverse. Пример от интерактивната конзола:

>>> spam.sort(reverse=True)
>>> spam
['язовци', 'мравки', 'кучета', 'котки', 'зебри']

Трябва да се отбележат три неща за метода sort(). Първо, методът sort() сортира списъка на място. Не трябва да се използва върнатата стойност с код като spam = spam.sort().

На второ място, не могат да се сортират списъци, в които има едновременно числа и низове. Python не знае как да сравни две такива стойности. Следващият пример от интерактивната конзола води до TypeError:

>>> spam = [1, 2, 3, 4, 'Ангел', 'Боби']
>>> spam.sort()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        spam.sort()
TypeError: '<' not supported between instances of 'str' and 'int'

Трето - sort() използва ASCII за определяне на реда на низовете. Това означава, че всички главни букви са преди малките букви. По този начин малката буква а се нарежда след главната буква Я. Пример от интерактивната конзола:

>>> spam = ['Ангел', 'антилопа', 'Боби', 'бивол', 'Васил', 'вълк']
>>> spam.sort()
>>> spam
['Ангел', 'Боби', 'Васил', 'антилопа', 'бивол', 'вълк']

При необходимост от сортиране в нормален азбучен ред може да се използва именувания аргумент key, на който да се подаде str.lower.

>>> spam = ['а', 'я', 'А', 'Я']
>>> spam.sort(key=str.lower)
>>> spam
['а', 'А', 'я', 'Я']

По този начин sort() ще сравнява всички елементи все едно са с малки букви, без реално да променя стойностите.

Обръщане на реда на стойностите в списък с метода reverse()

Методът reverse() служи за бързо обръщане на реда на елементите в списък. Пример от интерактивната конзола:

>>> spam = ['котка', 'куче', 'лос']
>>> spam.reverse()
>>> spam
['лос', 'куче', 'котка']

Подобно на sort(), методът reverse() не връща нов списък. По тази причина се пише spam.reverse(), а не spam = spam.reverse().

Използване на списък за подобряване на програма

В предишната глава имаше програма, която връща различен низ, спрямо стойността на случайно число. С помощта на списък може да се напише много по-елегантна версия на тази програма. Един списък може да замести много редове от идентични elif конструкции. Следва кода на програмата, записана като magic8Ball2.py:

import random

messages = ['Със сигурност',
    'Така е',
    'Да',
    'Неясно, опитай отново'
    'Питай по-късно',
    'Концентрирай се и питай пак',
    'Отговорът е - не',
    'Перспективата не е добра',
    'Много съмнително']

print(messages[random.randint(0, len(messages) - 1)])

Тази програма работи по съвсем същия начин като предишната magic8Vall.py

Добре е да се обърне внимание на изразът, който се изполва за индекс в messages : random.randint(0, len(messages) - 1). Той дава случайно число, което да се използва за индекс, без значение от размера на messages. Тоест, ще се получи случайно число между 0 и стойността len(messages) - 1. Предимството на този подход е в отпадналата нужда от промяна на код, ако се добавят или премахват низове от списъка messages. Така има по-малко промени в кода при необходимост, което води до по-малка вероятност за нови бъгове.

Последователни типове данни

Списъците не са единствения тип данни, които представляват наредени поредици от стойности. Например, низовете и списъците са всъщност подобни, ако низът се разгледа като "списък" от текстови символи. В Python последователните типове данни включват списъци, низове, обектите поредици върнати от range() и комплектите (описани по-долу). Много от приложимите за списък действия могат да се изпълняват и с останалите последователни типове данни : индексиране, разрязване, използване във for цикъл, използване с оператори in и not in. Ето и потвърждение от интерактивната конзола :

>>> name = 'Станимир'
>>> name[0]
'С'
>>> name[-2]
'и'
>>> name[0:4]
'Стан'
>>> 'Ст' in name
True
>>> 'с' in name
False
>>> 'н' not in name
False
>>> for i in name:
... print('* * * ' + i + ' * * *')

* * * С * * *
* * * т * * *
* * * а * * *
* * * н * * *
* * * и * * *
* * * м * * *
* * * и * * *
* * * р * * *

Изменими и неизменими типове данни

Все пак има една съществена разлика между списъците и низовете. Списъчната стойност е изменим (mutable) тип данни - могат да се добавят, премахват и променят стойностите в него. От друга страна низът е неизменим (immutable) - той не може да бъде променен. При опит за промяна на някой от символите в низа се получава TypeError. Пример от интерактивната конзола:

>>> name = 'Коткатя Шери'
>>> name[6] = 'а'
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        name[6] = 'а'
TypeError: 'str' object does not support item assignment

"Промяната" на низ може да стане с използване на разрез и слепване за получаване на нов низ, който използва копирани части от оригиналния низ. Пример от интерактивната конзола :

>>> name = 'Коткатя Шери'
>>> newName = name[0:6] + 'а' + name[7:12]
>>> name
'Коткатя Шери'
>>> newName
'Котката Шери'

Използва се [0:6] и [7:12] за копиране на символите, които не трябва да се променят. За отбелязване е, че оригиналният низ 'Коткатя Шери' не се е променил, защото низовете са неизменими.

Стойност от тип списък е изменима, но в следващия код втория ред не променя списъка eggs :

>>> eggs = [1, 2, 3]
>>> eggs = [4, 5, 6]
>>> eggs
[4, 5, 6]

Списъчната стойност в променливата eggs не е променена. Вместо това напълно нова списъчна стойности ([4, 5, 6]) замества старата стойност ([1, 2, 3]). Следващата картинка изобразява това действие :

Изпълнение на eggs = [4, 5, 6]
При изпълнението на eggs = [4, 5, 6] съдържанието на eggs се замества с нова списъчна стойност.

За реална промяна на оригиналния списък в eggs трябва да се използва код, подобен на :

>>> eggs = [1, 2, 3]
>>> del eggs[2]
>>> del eggs[1]
>>> del eggs[0]
>>> eggs.append(4)
>>> eggs.append(5)
>>> eggs.append(6)
>>> eggs
[4, 5, 6]

В този случай списъчната стойност в eggs си остава същата, само съдържанието на списъка е променено. Следващата картинка изобразява седемте промени направени от седемте реда на примера:

Изпълнение на del и append()
Конструкцията del и методът append() променят съдържанието на списъка.

Промяната на стойност на изменим тип данни (както с del и append() в предния пример) променя самата стойност, тъй като стойността в променливата не е заменена с нова списъчна стойност.

Разделението между изменимите и неизменими типове изглежда ненужно, но "Предаване по референция" по-долу обяснява различното поведение при извикване на функции с изменими спрямо неизменими аргументи. Преди това се демонстрира типът комплект, който е неизменим вариант на списъчния тип данни.

Тип данни комплект

Типът данни комплект (tuple) е почти същият като типът списък, с две разлики. На първо време - комплектите се пишат със скоби, ( и ), вместо квадратни скоби [ и ]. Пример от интерактивната конзола :

>>> eggs = ('здравей', 42, 0.5)
>>> eggs[0]
'здравей'
>>> eggs[1:3]
(42, 0.5)
>>> len(eggs)
3

Но основната разлика между списък и комплект е в изменимостта. Комплектите са неизменими, подобно на низовете. Стойностите в комплект не могат да се променят, добавят или премахват. Следва пример от интерактивната конзола, в който се получава грешка TypeError :

>>> eggs = ('здравей', 42, 0.5)
>>> eggs[1] = 99
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        eggs[1] = 99
TypeError: 'tuple' object does not support item assignment

Комплект с една стойност се пише със запетая след стойността. Иначе Python ще помисли стойността за написана в нормални скоби. По запетаята Python разбира, че става въпрос за комплект. За разлика от други езици за програмиране, в Python може да има запетая след последната стойност в списък или комплект. С използване на функцията type() в интерактивната конзола може да се види разликата :

>>> type(('Здравей'))
<class 'str'>
>>> type(('Здравей',))
<class 'tuple'>

Комплектите могат да се използват като подсказка при четене на кода, че поредицата от стойностите не е предвидена да се променя. Ако дадена поредица от стойности не трябва никога да се променя - използва се комплект. Друга полза от използването на комплект вместо списък е оптимизирането, което Python може да извърши. Така кодът с неизменими типове се изпълнява малко по-бързо.

Преобразуване на типове с функциите list() и tuple()

Извикването str(42) връща '42', представянето на числото 42 като низ. По същия начин функциите list() и tuple() връщат списък или комплект, отговарящ на подадената стойност. В примерите от интерактивната конзола може да се забележи, как типът на стойността се променя :

>>> tuple(['котка', 'куче', 5])
('котка', 'куче', 5)
>>> list(('котка', 'куче', 5))
['котка', 'куче', 5]
>>> list('Здравей')
['З', 'д', 'р', 'а', 'в', 'е', 'й']

Преобразуването на комплект в списък е подходящо при необходимост от изменима версия на комплект.

Референции

В началото изглежда, че променливите "съхраняват" низове или числени стойности. Всъщност това е опростено обяснение на реалността. Технически погледнато, променливите съхраняват референции(препратки) към локации в компютърната памет, където са разположени стойностите. Пример от интерактивната конзола:

>>> spam = 42
>>> cheese = spam
>>> spam = 100
>>> spam
100
>>> cheese
42

При присвояването на стойност 42 на променливата spam, всъщност в паметта на компютъра се създава стойност 42, а в променливата spam се записва референция (препратка, reference) към тази стойност. При копирането на стойността на spam и присвояването ѝ на променливата cheese, всъюност се копира референцията. И двете променливи spam и cheese сочат към стойността 42 в компютърната памет. При последващото променяне на стойността в spam на 100 се създава нова стойност 100. В spam се запазва референция до тази нова стойност. Това не афектира стойността на cheese. Числата са неизменим (immutable) тип, който не се променя. След присвояване на нова стойност на променливата spam, тази променлива вече сочи към напълно различна стойност в паметта.

Но списъците не работят посъщия начин, защото списъчната стойност можже да се променя. Списъците са изменими (mutable). Следва код в интерактивната конзола, който улеснява разбирането на тази разлика :

➊ >>> spam = [0, 1, 2, 3, 4, 5]
➋ >>> cheese = spam # Копира се референцията, а не списъка
➌ >>> cheese[1] = 'Здравей!' # Това променя списъка
>>> spam
[0, 'Здравей!', 2, 3, 4, 5]
>>> cheese # Променливата cheese реферира същия списък
[0, 'Здравей!', 2, 3, 4, 5]

Това може да изглежда странно. Кодът променя само списъка cheese, но изглежда, че и списъкът spam е променен.

При създаването на списък ➊, в променливата spam се присвоява референция към списъка. На следващия ред ➋ се копира само референцията към списъка от spam в cheese, но не и самата списъчна стойност. Това означава, че променливите spam и cheese сочат към един и същи списък. В паметта имасамо един списък, защото списъкът не е бил копиран. Така при променянето на елемент от cheese ➌ се променя същия списък, който се сочи и от spam.

Трябва да се помни, че променливите са като кутии, които съдържат стойности. Предните изображения в тази глава, които показват списъци в кутии, не са много точни, защото променливите не съдържат списъци, а референции към списъци. (Тези референции имат уникален ID номер, който се използва вътрешно от Python, но може да се игнорира). Следващото изображение показва резултата от присвояване на списък на променливата spam, следвайки метафората за променливите като кутии

Записване на референция към списък
spam = [0, 1, 2, 3, 4, 5] записва в променливата референция към списък, не самият списък.

На следващото изображение е показано как референцията от spam се копира в cheese. Създава се само нова референция, която се запазва в cheese, а не нов списък. Двете референции се отнасят за един и същи списък.

Копиране на референция
cheese = spam копира референцията, не самият списък.

При промяна на списъка, който се сочи от cheese, се променя и списъка, който се сочи от spam, защото cheese и spam сочат един и същи списък.

Технически погледнато, променливите в Python съдържат референции към стойности, но хората често казват, че дадена променлива съдържа стойността.

Уникален номер и функцията id()

Човек може да се чуди защо странното поведение на изменимите списъци от предната секция не се получава с неизменими стойности, като числа или низове. За обяснение може да се използва вградената функция id(). Всички стойности в Python имат уникален номер, който може да се види с помощта на функцията id(). Пример от интерактивната конзола:

>>> id('Здрасти') # върнатото число ще е различно на всеки компютър
4496527520

При изпълнението на id('Здрасти') Python създава низ 'Здрасти' в компютърната памет. След това функцията id() връща числото, отговарящо на адреса в паметта, където е съхранен низа. Python избира този адрес спрямо свободната памет в момента, така че то ще е различно при всяко изпълнение на този ред.

Всички низове, включително и 'Здрасти', са неизменими и не могат да се променят. При "промяна" на низ в променлива, на практика се създава нов низ на различно място в паметта, а променливата започва да сочи този нов низ. Пример от интерактивната конзола, в който уникалния номер на низа, сочен от bacon се променя :

>>> bacon = 'Здравей'
>>> id(bacon)
4496527520
>>> bacon += ' свят!' # Създава се нов низ от 'Здравей' и ' свят!'
>>> id(bacon) # bacon сега сочи напълно различен низ
4496527408

От друга страна, списъците са изменими обекти, които могат да се променят. Методът append() не създава нов списък. Той променя съществуващия списък. Това е нарича "промяна на обекта на място (in place)".

>>> eggs = ['котка', 'куче'] # Този ред създава нов списък
>>> id(eggs)
4495876544
>>> eggs.append('лос') # append() променя списъка "на място"
>>> id(eggs) # eggs все още сочи към същия списък както в началото
4495876544
>>> eggs = ['прилеп', 'мишка', 'крава'] # Този ред създава нов списък, който има нов уникален номер
>>> id(eggs) # eggs вече сочи към напълно различен списък
4496818624

Когато две променливи сочат към един и същи списък (както spam и cheese в предната секция), промяната на списъчната стойност се отразява и на двете променливи, защото и двете сочат към същия списък. Списъчните методи като append(), extend(), remove(), sort() и reverse() променят списъка на място.

Python има автоматична система за почистване на паметта (automatic garbage collection), която изтрива всяка стойност, която вече не се сочи от нито една променлива. Това се прази, за да може да се освободи паметта. За радост не се налага да се знае как точно работи почистването на паметта. В други езици за програмиране се използва ръчно управление на паметта, което често води до проблеми и бъгове.

Предаване на референция

Референциите са особено важни за разбирането на механизма на предаване на аргументи. При извикване на функция в променливите параметри се копират стойностите на аргументите. При списъците (и речниците, които са описани в следващата глава) това означава копиране на референцията. Последствията от това могат да се видят в примерната програма, записана като passingReference.py:

def eggs(someParameter):
    someParameter.append('Здравей')
spam = [1, 2, 3]
eggs(spam)
print(spam)

Функцията eggs() не връща нов списък, а директно променя подаденият ѝ списък. При изпълнение на програмата се получава следния резултат :

[1, 2, 3, 'Здравей']

spam и someParameter съдържат различни референции, но и двете сочат към един и същи списък. По тази причина извикването на append('Здравей') променя списъка, дори и след връщане от функцията.

Това поведение трябва да се помни. Неотчитането на начина, по който Python третира списъците и речниците, може да доведе до объркване и проблеми.

Функциите copy() и deepcopy() от модула copy.

Често предаването на референции е най-удобният начин за работа със списъци. Това не е така, ако функцията променя списъка, а промените в оригиналния списък не са желани. За тази цел Python има модул copy, в който има функции copy() и deepcopy(). Първата функция - copy.copy(), се използва за копиране на изменима стойност, а не за копиране на референция. Пример от интерактивната конзола:

>>> import copy
>>> spam = ['А', 'Б', 'В', 'Г']
>>> id(spam)
4491184576
>>> cheese = copy.copy(spam)
>>> id(cheese) # cheese е различен списък с различен номер
4492330816
>>> cheese[1] = 42
>>> spam
['А', 'Б', 'В', 'Г']
>>> cheese
['А', 42, 'В', 'Г']

Сега променливите spam и cheese сочат различни списъци. По тази причина само списъкът в cheese е променен от израза cheese[1] = 42.

Функцията copy.deepcopy() се използва, когато в списъка има други списъци. Тази функция ще копира и всички вътрешни списъци.

Кратка програма : играта "Живот" на Конуей

Играта "Живот" на Конуей (Conway`s Game of Life) е пример за клетъчен автомат (cellular automata) : набор от правила, които направляват поведението на определен брой клетки. На практика се получават красиви анимации. Всяка стъпка може да се нарисува с помощта на хартия на квадратчета, като всяко квадратче е една клетка. Запълнен квадрат е "жив", а празен квадрат е "мъртъв". Ако жив квадрат има два или три живи съседни квадрата - той остава жив на следващата стъпка. Ако мъртъв квадрат има точно три живи съседни квадрата - той ще нъде жив на следващата стъпка. Всички останали квадрати ще са мъртви на следващата стъпка. На следващата изображение се вижда пример за няколко поредни стъпки.

Четири поредни съупки в симулация на играта 'Живот'на Конуей
Четири поредни съупки в симулация на играта 'Живот'на Конуей

Правилата са прости, но водят до възникване на изненадващи резултати. В тази игра има конфигурации, които могат да се движат, възпроизвеждат, дори да наподобява компютърен процесор. Но в основата на това сложно поведение е сравнително проста програма.

За представяне на двуизмерната решетка могат да се използва списък със списъци. Всеки вътрешен списък представя една колона с квадрати, като жив квадрат се представя с # символ, а мъртвите квадрати се представят с ' ' низ, съдържащ един интервал. Ето и кодът на програмата conway.py. В началото може да не се разбира добре как целият код работи. Трябва да се следват коментарите и обяснението след програмата:

# Играта "Живот" на Конуей
import random, time, copy
WIDTH = 60
HEIGHT = 20

# Създава се списък със списъци за клетките
nextCells = []
for x in range(WIDTH):
    column = [] # Създава нова колона
    for y in range(HEIGHT):
        if random.randint(0, 1) == 0:
            column.append('#') # Добавя жива клетка
        else:
            column.append(' ') # Добавя мъртва клетка
    nextCells.append(column) # Добавя колоната към списъка с колони

while True: # Основният цикъл на програмата
    print('\n\n\n\n\n') # Всяка стъпка се отделя с нови редове
    currentCells = copy.deepcopy(nextCells)

    # Принтиране на currentCells на екрана
    for y in range(HEIGHT):
        for x in range(WIDTH):
            print(currentCells[x][y], end='') # Принтира # или интервал
        print() # Принтира нов ред в края на всеки ред

    # Пресмята клетките на следващата стъпка спрямо клетките на сегашната
    for x in range(WIDTH):
        for y in range(HEIGHT):
            # Получаване на координатите на съседните клетки:
            # '% WIDTH' гарантира, че leftCoord ще е винаги между 0 и WIDTH - 1
            leftCoord = (x - 1) % WIDTH
            rigthCoord = (x + 1) % WIDTH
            aboveCoord = (y - 1) % HEIGHT
            belowCoord = (y + 1) % HEIGHT

            # Преброяване на живите съседни клетки:
            numNeighbors = 0
            if currentCells[leftCoord][aboveCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал горе-ляво е жива
            if currentCells[x][aboveCoord] == '#':
                numNeighbors += 1 # Клетката отгоре е жива
            if currentCells[rigthCoord][aboveCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал горе-дясно е жива/span>
            if currentCells[leftCoord][y] == '#':
                numNeighbors += 1 # Клетката отляво е жива
            if currentCells[rigthCoord][y] == '#':
                numNeighbors += 1 # Клетката отдясно е жива
            if currentCells[leftCoord][belowCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал долу-ляво е жива
            if currentCells[x][belowCoord] == '#':
                numNeighbors += 1 # Клетката отдолу е жива
            if currentCells[rigthCoord][belowCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал долу-дясно е жива

            # Проверка на клетката според правилата на играта:
            if currentCells[x][y] == '#' and (numNeighbors == 2 or numNeighbors == 3):
                # Живи клетки с 2 или 3 съседни остават живи:
                nextCells[x][y] = '#'
            elif currentCells[x][y] == ' ' and numNeighbors == 3:
                # Мъртви клетки с 3 съседни оживяват:
                nextCells[x][y] = '#'
            else:
                # Всички останали умират или остават мътрви
                nextCells[x][y] = ' '

    time.sleep(1) # Добавена 1 секунда пауза за по-добра анимация

Следва анализ на кода ред по ред.

# Играта "Живот" на Конуей
import random, time, copy
WIDTH = 60
HEIGHT = 20

Първо се импортират модулите, от които ще се използват функции. В случая са random.randint(), time.sleep() и copy.deepcopy().

# Създава се списък със списъци за клетките
nextCells = []
for x in range(WIDTH):
    column = [] # Създава нова колона
    for y in range(HEIGHT):
        if random.randint(0, 1) == 0:
            column.append('#') # Добавя жива клетка
        else:
            column.append(' ') # Добавя мъртва клетка
    nextCells.append(column) # Добавя колоната към списъка с колони

Началното състояние на клетъчния автомат ще е напълно случайно. Трябва да се създаде структура от списък със списъци, които ще съхраняват низовете '#' и ' ', които представят живите и мъртвите клетки. Позицията им в списъка със списъци отговаря на тяхното положение на екрана. Всеки вътрешен списък представя една колона от клетки. Извикването на random.randint(0, 1) е с 50 на 50 шанс за получаване на жива или мъртва клетка.

Този списък със списъци се съхранява в променлива с име nextCells, като първата стъпка в оснояния цикъл е копирането на nextCell в currentCells. В тази структура списък от списъци координатата Х започва от 0 отляво и се увеличава надясно, докато координатата У започва от 0 отгоре и се увеличава надолу. Така nextCells[0][0] отговаря на клетката в горния ляв ъгъл на екрана, nextCells[1][0] отговаря на клетката отдясно на първата клетка, а nextCells[0][1] отговаря на клетката под нея.

while True: # Основният цикъл на програмата
    print('\n\n\n\n\n') # Всяка стъпка се отделя с нови редове
    currentCells = copy.deepcopy(nextCells)

Всяко завъртане на основния цикъл отговаря на една стъпка на клетъчния автомат. На всяка стъпка се копира nextCells в currentCells, currentCells се принтират на екрана и след това клетките в currentCells се използват за пресмятане на клетките в nextCells.

    # Принтиране на currentCells на екрана
    for y in range(HEIGHT):
        for x in range(WIDTH):
            print(currentCells[x][y], end='') # Принтира # или интервал
        print() # Принтира нов ред в края на всеки ред

Тези цикли се използват за принтиране на цял ред клетки на екрана, последвани от нов ред. Това действие се повтаря за всеки ред в nextCells.

    # Пресмята клетките на следващата стъпка спрямо клетките на сегашната
    for x in range(WIDTH):
        for y in range(HEIGHT):
            # Получаване на координатите на съседните клетки:
            # '% WIDTH' гарантира, че leftCoord ще е винаги между 0 и WIDTH - 1
            leftCoord = (x - 1) % WIDTH
            rigthCoord = (x + 1) % WIDTH
            aboveCoord = (y - 1) % HEIGHT
            belowCoord = (y + 1) % HEIGHT

След това се изполват двойка for цикли за пресмятане на всяка клетка в следващото състояние. Дали клетката е жива или мъртва зависи от съседните, така че се пресмятат индексите на клетките отляво, отдясно, отгоре и отдолу спрямо текущите координати.

Операторът % (остатък от деление) се използва за "превъртане". Левият съсед на клетката в най-лявата колона 0 ще е 0 - 1 или -1. За превъртане до най-дясната колона с индекс 59 се изполва (0 - 1) % WIDTH. WIDTH е 60, така че този израз се пресмята до 59. Тази техника за превъртане с остатъка от делението се използва и за десния, горния и долния съсед.

            # Преброяване на живите съседни клетки:
            numNeighbors = 0
            if currentCells[leftCoord][aboveCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал горе-ляво е жива
            if currentCells[x][aboveCoord] == '#':
                numNeighbors += 1 # Клетката отгоре е жива
            if currentCells[rigthCoord][aboveCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал горе-дясно е жива/span>
            if currentCells[leftCoord][y] == '#':
                numNeighbors += 1 # Клетката отляво е жива
            if currentCells[rigthCoord][y] == '#':
                numNeighbors += 1 # Клетката отдясно е жива
            if currentCells[leftCoord][belowCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал долу-ляво е жива
            if currentCells[x][belowCoord] == '#':
                numNeighbors += 1 # Клетката отдолу е жива
            if currentCells[rigthCoord][belowCoord] == '#':
                numNeighbors += 1 # Клетката по диагонал долу-дясно е жива

За всяка клетка nextCells[x][y] се преброяват живите съседи, за да се определи дали клетката трябва да е жива или мъртва. Поредицата if изрази проверяват всеки от осемте съседа на клетката, като към numNeighbors се добавя единица за всеки жив съсед.

            # Проверка на клетката според правилата на играта:
            if currentCells[x][y] == '#' and (numNeighbors == 2 or numNeighbors == 3):
                # Живи клетки с 2 или 3 съседни остават живи:
                nextCells[x][y] = '#'
            elif currentCells[x][y] == ' ' and numNeighbors == 3:
                # Мъртви клетки с 3 съседни оживяват:
                nextCells[x][y] = '#'
            else:
                # Всички останали умират или остават мътрви
                nextCells[x][y] = ' '

    time.sleep(1) # Добавена 1 секунда пауза за по-добра анимация

Вече се знае броят на живи съседи на клетката currentCells[x][y] и nextCells[x][y] може да се определи като '#' или ' '. Програмата преминава през всички възможни координати и след това добавя едносекундна пауза с извикване на time.sleep(). След това изпълнението на програмата се връща в началото на основния цикъл за началото на следващата стъпка.

Известни са няколко конфигурации с имена като "глайдер", "осцилатор" или "космически кораб". Конфигурацията "глайдер", която е изобразена по-горе, представлява шаблон, който се "мести" по диагонал на всеки четири стъпки. В програмата conway.py може да се създаде глайдер, ако редът:

if random.randint(0, 1) == 0:

if (x, y) in ((1, 0), (2, 1), (0, 2), (1, 2), (2, 2)):

В интернет могат да се намерят всякакви интересни конструкции, направени с помощта на играта на живот на Конуей

Обобщение

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

Списъците са последователен тип данни, който е изменим. Това означава, че съдържанието на списъка може да се променя. Други последователни типове данни, като комплект и низ, са неизменими. Променлива, която съдържа комплект или низ, може да получи нова стойност, но това не е същото като промяна на съществуващата стойност - както например спицъчните методи append() или remove().

Променливите не съхраняват списъчни стойности - те съхраняват референции към списъците. Това е важна разлика, проявяваща се при копиране на променливи или предаване на списъци като аргументи при извикване на функции. Тъй като се копира референция към списъка, всяка промяна на списъка може да се отрази на друга променлива в програмата. Функциите copy() и deepcopy() се използват за създаване на списък, който не променя оригиналния списък.

Упражнения

  1. Какво е []
  2. Как се присвоява стойност 'здравей' като трета стойност в списък, който е променливата spam ? (в момента spam съдържа [2, 4, 6, 8, 10])

  3. За следващите три въпроса се използва spam, която съдържа ['а', 'б', 'в', 'г'].

  4. До какво се пресмята spam[int(int('3' * 2)//11)] ?
  5. До както се пресмята spam[-1] ?
  6. До какво се пресмята spam[:2] ?

  7. За следващите три въпроса се използва bacon, която съдържа [3.14, 'котка', 11, 'котка', True].

  8. До какво се пресмята bacon.index('котка') ?
  9. Как изглежда списъка след изпълнение на bacon.append(99) ?
  10. Как изглежда списъка след изпълнение на bacon.remove('котка') ?
  11. Кой е операторът за слепване на списъци ? А за размножаване ?
  12. Каква е разликата между списъчните методи append() и insert() ?
  13. Кои са двата начина за премахване на стойности от списък ?
  14. По какво си приличат списъяните и низовите стойности ?
  15. Кои са разликите между списък и комплект ?
  16. Как се изписва комплект, който има само целочислена стойност 42 ?
  17. Как списъчна стойност се преобразува до комплект ? Как от комплект може да се получи списък ?
  18. Променливите, които "съдържат" списъци, всъщност не съхраняват списък. Какво точно съдържат ?
  19. Каква е разликата между copy.copy() и copy.deepcopy() ?

Практически упражнения

Да се напишат програми, които да вършат следните задачи.

Разделяне със запетаи

Даден е списък, подобен на :

spam = ['ябълки', 'банани', 'сирене', 'котки']

Да се напише функция, която приема като аргумент списъчна стойност и връща низ, който се състои от всички стойности, разделени със запетая и интервал, а преди последната стойност има добавено и. Например при подаване на списъка spam, функцията трябва да върне 'ябълки, банани, сирене и котки'. Функцията трябва да работи с подаден произволен списък. Трябва да се провери и случая на подаване на празен списък [].

Поредици при хвърляне на монета

В това упражнение ще се направи експеримент. Нека резултатът от хвърляне на монета се записва като "Е" за ези и "Т" за тура. След 100 опита ще има списък, който ще изглежда подобно на "Т Т Т Т Е Е Е Е Т Т". Ако човек трябва мислено да хвърли монета 100 пъти, вероятно ще получи поредица като "Е Т Е Т Е Е Т Е Т". Поредицата изглежда случайна (за хората), но не е математически случайна. Човек никога не би написал шест поредни ези или тура, въпреки че това е доста вероятно да се случи при наистина случайни хвърляния на монета. Хората са предвидимо объркани от случайността.

Да се напише програма, която намира колко пъти се среща поредица от шес ези или шест тура в списък със случайно разпределени ези и тура. Програмата разбива експеримента на две части: първата част генерира списък със случайно избрани стойности 'ези' и 'тура'; втората част проверява дали има търсената поредица в списъка. Кодът да се постави в цикъл, който повтаря експеримента 10 000 пъти. Така може да се определи какъв е процентът при 100 хвърляния на монета да се получи поредица от шест ези или щест тура. Като помощ - извикването на функцията random.randint(0, 1) ще върне 0 в 50% от случаите и 1 в останлите 50%.

За начало може да се използва следния код:

import random
numberOfStreaks = 0
for experimentNumber in range(10000):
    # Код, който създава списък със 100 стойност 'ези' или 'тура'
    # Код, който проверява дали в списъка има поредица от 6 ези или тура
print('Шанс за 6 поредни: %s%%'%(numberOfStreaks / 100))

Резултатът ще е само приблизителен, но 10 000 е задоволителен като размер на теста. С познания по математика може да се определи точния отговор, без да има нужда да се пише програма. Но все пак програмистите предпочитат компютрите да смятат вместо тях.

Изображение от символи

Нека да има списък от списъци, като стойностите във вътрешните списъци са едносимволен низ, подобно на:

grid = [['.', '.', '.', '.', '.', '.'],
          ['.', 'O', 'O', '.', '.', '.'],
          ['O', 'O', 'O', 'O', '.', '.'],
          ['O', 'O', 'O', 'O', 'O', '.'],
          ['.', 'O', 'O', 'O', 'O', 'O'],
          ['O', 'O', 'O', 'O', 'O', '.'],
          ['O', 'O', 'O', 'O', '.', '.'],
          ['.', 'O', 'O', '.', '.', '.'],
          ['.', '.', '.', '.', '.', '.']]

За grid[x][y] може да се мисли като символът на координати x, y от "изображението", съставено от символите. Началото (0, 0) е в горния ляв ъгъл, като x нараства надясно, а y нараства надолу.

Да се напише код, който от показаната стойност на grid да принтира следното изображение:

..OO.OO..
.OOOOOOO.
.OOOOOOO.
..OOOOO..
...OOO...
....O....

Подсказка: Трябва да се използва цикъл в цикъл, за да се принтира grid[0][0], после grid[1][0], после grid[2][0] и така нататък до grid[8][0]. Това ще приключи с първия ред, така че трябва да последва добавяне на нов ред. След това програмата трябва да принтира grid[0][1], после grid[1][1], после grid[2][1] и така нататък. Последния принтиран символ трябва да е grid[8][5].

Също така е полезно да се помни, че с изполване на именувания аргумент end при извикване на print() може да се предотврати автоматичното добавяне на нов ред след всеки print().

Подкрепете автора чрез покупка на оригиналната книга от No Starch Press или Amazon.