Kodomo

Пользователь

Первый приём на тему чистоты и порядка в файлах. Когда программа становится большой (в нашем примере этого не будет, а в жизни бывает постоянно), хочется выкинуть из головы и с глаз долой части, о которых вы сейчас не думаете. Сильно облегчает голову и помогает. Для этого часть кода, относящуюся к одной теме мы можем пересадить в отдельный питонский модуль. Импортировать модули и пользоваться ими мы умеем, а теперь будем создавать.

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

Сделаем файл web_stuff.py:

   1 +import re
   2 +from urllib.request import urlopen
   3 +
   4 +def get_title(url):
   5 +    with urlopen(url) as file:
   6 +        text = file.read().decode('utf-8')
   7 +        return re.findall('<title>(.*)</title>', text)[0]

И файл use_web.py:

   1 +import web_stuff
   2 +
   3 +print(web_stuff.get_title('http://www.stihi-rus.ru/World/Shekspir/2.htm'))

Для простоты пока что в нашем модуле web_stuff нет ничего, кроме определений функций, тогда понятно, что делает питон: даёт нам возможность пользоваться этим функциями.

   1 +>>> 
   2 +Traceback (most recent call last):
   3 +  File "C:/Users/dendik/Desktop/learn-python/use_web.py", line 3, in <module>
   4 +    print(web_stuff.get_title('http://www.stihi-rus.ru/World/Shekspir/2.htm'))
   5 +  File "C:/Users/dendik/Desktop/learn-python\web_stuff.py", line 6, in get_title
   6 +    text = file.read().decode('utf-8')
   7 +UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcd in position 97: invalid continuation byte

Однако если мы его запустим, обнаружим проблему с кодировками.

Вспоминаем, что мы умеем писать try-except. Поправим web_stuff.py:

   1  def get_title(url):
   2      with urlopen(url) as file:
   3 -        text = file.read().decode('utf-8')
   4 +        text = file.read()
   5 +        try:
   6 +            text = text.decode('utf-8')
   7 +        except UnicodeDecodeError:
   8 +            text = text.decode('cp1251')
   9          return re.findall('<title>(.*)</title>', text)[0]

И снова запустим use_web.py.

+>>> 
+Но злой насмешкой будет твой ответ - Шекспир - сонет 2

Работает!

А ещё можем изменить способ, которым мы возвращаем заголовки (чтобы не падать, если заголовков нет или больше одного):

             text = text.decode('utf-8')
         except UnicodeDecodeError:
             text = text.decode('cp1251')
-        return re.findall('<title>(.*)</title>', text)[0]
+        return '; '.join(re.findall('<title>(.*)</title>', text))

Из прошлой жизни мы могли бы вспомнить, что когда мы видим незнакомый модуль, правильный инстинкт состоит в том, чтобы попросить от него хелпы:

   1 +>>> import web_stuff
   2 +>>> help(web_stuff)
   3 +Help on module web_stuff:
   4 +
   5 +NAME
   6 +    web_stuff
   7 +
   8 +FUNCTIONS
   9 +    get_title(url)
  10 +
  11 +FILE
  12 +    c:\users\dendik\desktop\learn-python\web_stuff.py

И на удивление, хелпы у нас почти есть. Только текста хелпов нет.

Правило: в питоне, если функция начинается со строки в кавычках, то эта строка и является хелпом от функции. Конструкция называется docstring.

Попробуем. Поправим web_stuff.py:

   1  from urllib.request import urlopen
   2  
   3  def get_title(url):
   4 +    '''Download webpage by URL, return it's title.'''
   5      with urlopen(url) as file:
   6          text = file.read()
   7          try:

Проверяем:

   1 +>>> import web_stuff
   2 +>>> help(web_stuff)
   3 +Help on module web_stuff:
   4 +
   5 +NAME
   6 +    web_stuff
   7 +
   8 +FUNCTIONS
   9 +    get_title(url)
  10 +        Download webpage by URL, return it's title.
  11 +
  12 +FILE
  13 +    c:\users\dendik\desktop\learn-python\web_stuff.py
  14 +>>> get_title('http://www.stihi-rus.ru/World/Shekspir/2.htm')
  15 +'Но злой насмешкой будет твой ответ - Шекспир - сонет 2'

Работает!

Названия функций и хелпы принято делать фразами в повелительном наклонении (мы приказываем компьютеру: "дай заголовок!"; и хелп: "Дай заголовок от страницы, если тебе дали её URL"). В английском языке это не очень ярко проявляется и часто трудно отличить от инфинитива, но намерение именно такое.

Надо заметить, что эта конструкция с точки зрения питона не является новым синтаксическим правилом. В питоне есть правило, что мы можем дать ему команду "исполни выражение" (и забудь, что оно вернуло). Мы этим типом команд очень много пользовались, например print("Hello") вычисляет значение None, которое питон потом с удовольствием игнорирует. Как побочный эффект на экране при этом что-то возникает – но какое питону дело-то до этих побочных эффектов! Точно так же мы можем написать строку: sin(42) – питон же не знает, что синус ничего не пишет на экран. Аналогично и просто: 42 или просто "hello" сами по себе на строке являются корректными команднами для питона. (И мы ими много пользовались на первом занятии, когда пользовались питоном чисто как калькулятором).

Однако, возмём последний пример использования нашей функции и добавим-ка его в нашу документацию в качестве примера:

   1  from urllib.request import urlopen
   2  
   3  def get_title(url):
   4 -    '''Download webpage by URL, return it's title.'''
   5 +    '''Download webpage by URL, return it's title.
   6 +
   7 +    Usage example:
   8 +    >>> get_title('http://www.stihi-rus.ru/World/Shekspir/2.htm')
   9 +    'Но злой насмешкой будет твой ответ - Шекспир - сонет 2'
  10 +    '''
  11      with urlopen(url) as file:
  12          text = file.read()
  13          try:

Вроде ничего нового... И тут сюрприз: мы можем попросить питон пройтись по всем таким примерам в модуле и проверить, что они действительно делают то, что в них сказано. Питон при этом ищет примеры по трём знакам больше и пробелу (как командная строка нам и выдаёт).

Делает это модуль doctest, из которого нас в основном интересует функция testmod:

   1 +>>> import web_stuff
   2 +>>> import doctest
   3 +>>> doctest.testmod(web_stuff)
   4 +TestResults(failed=0, attempted=1)

Из этого вырастает чудовищно полезный приём программирования. Называется test-driven development. Суть: сначала мы придумываем, на какие бы части нарезать нашу задачу. Затем мы для каждой части придумываем функцию. Затем мы для каждой функции сочиняем, как бы мы хотели, чтобы она работала, и пишем примеры использования. Затем мы пишем саму функцию и стараемся заставить наши примеры/тесты работать как нужно. (Надо заметить, что если наш тест не работает, то это звоночек о наличии ошибки: но ошибка при этом может быть как в функции, так и в тексте, так и в обоих. В этот момент нужно включать голову и действительно ДУМАТЬ).

А из этого, в свою очередь, вырастает ещё один очень полезный приём наведения порядка в голове: когда мы пишем какую-то часть программы, от всей остальной программы мы знаем только то, какие есть функции, какие у них аргументы, что про них написано в хелпе, и какие для них должны работать примеры. И ничего более! Мы доверяем себе в том, что рано или поздно мы заставим остальные части работать так, как описано. И пользуемся этим доверием.

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

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

Проверим это. Добавим принтов в наш web_stuff.py:

   1  import re
   2  from urllib.request import urlopen
   3  
   4 +best_url_ever = 'http://www.stihi-rus.ru/World/Shekspir/'
   5 +
   6 +print('Before get_title')
   7 +
   8  def get_title(url):
   9      '''Download webpage by URL, return it's title.
  10  
  11 @@ -14,3 +18,6 @@
  12          except UnicodeDecodeError:
  13              text = text.decode('cp1251')
  14          return '; '.join(re.findall('<title>(.*)</title>', text))
  15 +
  16 +print('After get_title')
  17 

Запустим use_web.py:

   1 +Before get_title
   2 +After get_title
   3 +Но злой насмешкой будет твой ответ - Шекспир - сонет 2

Всё логично. Сначала исполнился import, а следовательно, мы приказали питону:

Только после этого мы пошли исполнять вторую строку в use_web.py, и она напечатала нам ответ про то, какой заголовок есть у очередого сонета.

Если мы просто исполним web_stuff.py, то увидим:

   1 +>>> 
   2 +Before get_title
   3 +After get_title

Собственно, это единственные команды во всей прогармме, которые что-ото пишут на экран, логично ж!

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

Для этого есть конструкция if __name__ == "__main__" – тело такого if исполнится только в том случае, если программу не проимпортировали, а использовали как головную программу.

Несущественная техническая подробность: в переменной __name__ лежит либо название текущего модуля (в котором эта переменная упомянута в тот момент, когда она исполняется), либо слово "main", если это головная программа. Под видом переменных и функций, у которых в начале и в конце названия по два подчёркивания, питон позволяет нам немножко ковыряться в устройстве самого интерпретатора языка питона. (Такое свойство языка программирования называется "интроспекция").

   1  import re
   2  from urllib.request import urlopen
   3  
   4 +best_url_ever = 'http://www.stihi-rus.ru/World/Shekspir/'
   5 +
   6 +print('Before get_title')
   7 +
   8  def get_title(url):
   9      '''Download webpage by URL, return it's title.
  10  
  11 @@ -14,3 +18,6 @@
  12          except UnicodeDecodeError:
  13              text = text.decode('cp1251')
  14          return '; '.join(re.findall('<title>(.*)</title>', text))
  15 +
  16 +if __name__ == "__main__":
  17 +    print('After get_title')
  18 

Теперь если запустим web_stuff.py, увидим:

+>>> 
+Before get_title
+After get_title

А если запустим use_web.py, увидим:

+>>> 
+Before get_title
+Но злой насмешкой будет твой ответ - Шекспир - сонет 2

Чего мы, собственно, и добивались.

Первое, и самое типичное применение для такой конструкции – прятать в неё тесты. Чтобы не тратить время на тестирование, когда мы наш модуль импортируем, но иметь возможность протестировать его простым нажатием F5:

   1  import re
   2 +import doctest
   3  from urllib.request import urlopen
   4  
   5  best_url_ever = 'http://www.stihi-rus.ru/World/Shekspir/'
   6  
   7 -print('Before get_title')
   8 -
   9  def get_title(url):
  10      '''Download webpage by URL, return it's title.
  11  
  12 @@ -20,4 +19,4 @@
  13          return '; '.join(re.findall('<title>(.*)</title>', text))
  14  
  15  if __name__ == "__main__":
  16 -    print('After get_title')
  17 +    print(doctest.testmod())
  18 

Попробуем. Запускаем web_stuff.py:

   1 +>>> 
   2 +TestResults(failed=0, attempted=1)

Работает! Запускаем use_web.py:

+>>> 
+Но злой насмешкой будет твой ответ - Шекспир - сонет 2

Тоже работает!

Теперь интерфейсы.

Раньше мы делали интерфейсы скучные, в них всё было в одну стопку.

Чтобы стало повеселее, нам нужен всего лишь один виджет: Frame. Он никак не выглядит и ничего не делает. Единственная его роль: в него можно складывать другие виджеты. Тогда мы можем делать такие финты ушами:

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

Ещё для этого нам нужно вспомнить, что когда мы создаём виджет, первым аргументом мы ему всегда указывали, внутри чего ему расположиться. До сих пор мы всегда там ставили root, то есть предлагали ему расположиться внутри самого окна.

   1 +import web_stuff
   2 +import tkinter as tk
   3 +
   4 +root = tk.Tk()
   5 +url_bar = tk.Frame(root, background='red', padx=5, pady=5)
   6 +url_bar.pack(side='top', fill='x')
   7 +url_entry = tk.Entry(url_bar)
   8 +url_entry.pack(side='left', fill='x', expand='yes')
   9 +url_load = tk.Button(url_bar, text='Go')
  10 +url_load.pack(side='right')
  11 +titles = tk.Text(root)
  12 +titles.pack(side='bottom', fill='both')
  13 +root.mainloop()

Напоминаю, что у pack бывают аргументы:

Добавим кнопочке реакцию на нажатие: пусть при нажатии у нас в текстовое поле вписывается заголовок страницы:

   1  import web_stuff
   2  import tkinter as tk
   3  
   4 -print(web_stuff.get_title('http://www.stihi-rus.ru/World/Shekspir/2.htm'))
   5 +def add_title():
   6 +    url = url_entry.get()
   7 +    title = web_stuff.get_title(url)
   8 +    titles.insert('end', title)
   9  
  10  root = tk.Tk()
  11  url_bar = tk.Frame(root, background='red', padx=5, pady=5)
  12  url_bar.pack(side='top', fill='x')
  13  url_entry = tk.Entry(url_bar)
  14  url_entry.pack(side='left', fill='x', expand='yes')
  15 -url_load = tk.Button(url_bar, text='Go')
  16 +url_load = tk.Button(url_bar, text='Go', command=add_title)
  17  url_load.pack(side='right')
  18  titles = tk.Text(root)
  19  titles.pack(side='bottom', fill='both')

У виджета Text есть несколько методов, которые работают с его текстовым содержанием. Самые заметные из них:

Понятие где во всех из них выражаются небольшим симпатичным язычком, который позволяет говорить:

и много других приятностей. См. документацию c горой примеров. (Обращаю внимание, что в модуле tk есть куча констант вида tk.END = "end"; tk.INSERT = "insert", – и авторы этой документации этими константами пользуются. Большого преимущества в этих константах я не вижу).

Хочется ещё привязать функцию к нажатию на <Return>. Только мы же помним, что bind передаст функции лишний для нас сейчас аргумент event, в котором лежит всякая всячина в духе координат мыши, координат окна, виджета, у которого был курсор, и прочих полезных вещей. Но нам это всё сейчас не нужно.

Хочется, чтобы наша функция работала и если её вызвали без аргументов, и если её вызвали с аргументом. Это в питоне делается, и делается просто. Для этого нам нужно сказать, а что, собственно, положить в соответствующую переменную, если нам аргумента не дали. Пишется это так:

   1 +>>> def f(x=5):
   2 +       return x ** 2
   3 +
   4 +>>> f(10)
   5 +100
   6 +>>> f()
   7 +25

Итак, с этими новыми знаниями подправить наш код так, чтобы функция вызывалась и при нажатии на <Return> стало очень просто:

   1  import web_stuff
   2  import tkinter as tk
   3  
   4 -def add_title():
   5 +def add_title(event=None):
   6      url = url_entry.get()
   7      title = web_stuff.get_title(url)
   8 -    titles.insert('end', title)
   9 +    titles.insert('end', title + '\n')
  10  
  11  root = tk.Tk()
  12  url_bar = tk.Frame(root, background='red', padx=5, pady=5)
  13  url_bar.pack(side='top', fill='x')
  14  url_entry = tk.Entry(url_bar)
  15  url_entry.pack(side='left', fill='x', expand='yes')
  16 +url_entry.bind('<Return>', add_title)
  17  url_load = tk.Button(url_bar, text='Go', command=add_title)
  18  url_load.pack(side='right')
  19  titles = tk.Text(root)

Ещё мы можем запретить пользователю редактировать текст. Для этого мы можем перевести Text в состояние disabled. Только одна мелочь: мы и сами не сможем его править, пока он в этом состоянии, так что перед каждой правкой нам нужно будет обратно переводить его в состояние normal. Делается это ровно настолько же просто, насколько об этом говорится:

   1  def add_title(event=None):
   2      url = url_entry.get()
   3      title = web_stuff.get_title(url)
   4 +    titles.configure(state='normal')
   5      titles.insert('end', title + '\n')
   6 +    titles.configure(state='disabled')
   7  
   8  root = tk.Tk()
   9  url_bar = tk.Frame(root, background='red', padx=5, pady=5)
  10 @@ -17,8 +19,8 @@
  11  url_load.pack(side='right')
  12  
  13  tabs = ttk.Notebook(root)
  14 -tabs.pack(side='bottom', fill='both')
  15 -titles = tk.Text(tabs)
  16 +tabs.pack(side='bottom', fill='both', expand='yes')
  17 +titles = tk.Text(tabs, state='disabled')
  18  tabs.add(titles, text='URL titles')
  19  urls = tk.Text(tabs)
  20  tabs.add(urls, text='URLs themselves')

Хочется красивостей. Для всяких красивостей и готовых нетривиальных штук в tkinter есть модуль tkinter.ttk (это отдельный модуль, живущий внутри пакета tkinter, и его нужно импортировать отдельно).

Например, давайте сделаем окошко с вкладками. Для этого в tkinter.ttk есть класс виджетов Notebook:

   1  import web_stuff
   2  import tkinter as tk
   3 +import tkinter.ttk as ttk
   4  
   5  def add_title(event=None):
   6      url = url_entry.get()
   7 @@ -14,6 +15,12 @@
   8  url_entry.bind('<Return>', add_title)
   9  url_load = tk.Button(url_bar, text='Go', command=add_title)
  10  url_load.pack(side='right')
  11 -titles = tk.Text(root)
  12 -titles.pack(side='bottom', fill='both')
  13 +
  14 +tabs = ttk.Notebook(root)
  15 +tabs.pack(side='bottom', fill='both')
  16 +titles = tk.Text(tabs)
  17 +tabs.add(titles, text='URL titles')
  18 +urls = tk.Text(tabs)
  19 +tabs.add(urls, text='URLs themselves')
  20 +
  21  root.mainloop()

В первом приближении с ними работать очень просто:

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

Есть и прекрасный виджет tkinter.scrolledtext.ScrolledText, который ведёт себя так же, как Text, только у него появляются полосы прокрутки (scrollbar), когда в какую-то из сторон текста в нём становится слишком много.

Впрочем, нет глубокого смысла рассказывать обо всех виджетах, которые есть в tkinter или tkinter.ttk: они прекрасно описаны в документации, да и гугл умеет отвечать на вопросы о том, как делать всякие красивости. Я надеюсь, что я дал вам достаточно примеров того, что оно умеет, чтобы вы знали, чего можно от него просить, и у вас получалось узнавать необходимые новые кусочки знаний из документации.

Оффтопик. Ещё пара примеров про сортировку.

Питон может сортировать что угодно, что содержит много элементов: строки, списки, словари (в случае словарей он остортирует ключи словаря, и это очень удобно)...

   1 +>>> sorted("hello")
   2 +['e', 'h', 'l', 'l', 'o']
   3 +>>> sorted([3,4,13.4,5,31])
   4 +[3, 4, 5, 13.4, 31]
   5 +>>> sorted({'a':1, 'b':3})
   6 +['a', 'b']

Но не может сравнивать строки с числами:

   1 +>>> sorted(['a', 4])
   2 +Traceback (most recent call last):
   3 +  File "<pyshell#26>", line 1, in <module>
   4 +    sorted(['a', 4])
   5 +TypeError: unorderable types: int() < str()

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

   1 +>>> def sort_key(x):
   2 +       return int(x)
   3 +
   4 +>>> sorted([1, '2', 3])
   5 +Traceback (most recent call last):
   6 +  File "<pyshell#31>", line 1, in <module>
   7 +    sorted([1, '2', 3])
   8 +TypeError: unorderable types: str() < int()
   9 +>>> sorted([1, '2', 3], key=sort_key)
  10 +[1, '2', 3]

Вот мы так можем отсортировать список из строк и чисел при условии, что строки тоже изображают из себя числа. Заметьте, что в выдаче строка осталась строкой.

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

   1 +>>> f = {1:2, 3:4, 5:6}.get
   2 +>>> f(3)
   3 +4
   4 +>>> f(1)
   5 +2

Склеив эти два знания вместе мы получаем возможность сортировать словари аки по ключам, аки по значениям (аки ещё как хитрее, лишь бы мы смогли описать, по какому именно критерию):

   1 +>>> x={'z':1,'x':2,'y':3}
   2 +>>> sorted(x)
   3 +['x', 'y', 'z']
   4 +>>> sorted(x, key=x.get)
   5 +['z', 'x', 'y']