10. Call stack. Изключения, част 2. Структура на skeptic
10 ноември 2014
Днес
- Call stack
- Общ преглед на изключения
catch
и throw
- Структурата на една Ruby библиотека (skeptic)
- Накратко за четвърто предизвикателство
Сбирки след лекцията в сряда
- Миналата сряда ходихме в Торонто на по бира
- Ще опитаме да го превърнем в традиция – тази сряда пак
- Споделихте ни много ценни неща, ще се радваме да си поговорим пак
План за неделя, 16 ноември
традиционната планина
Новини за работни позиции с Ruby
Помощници за седмични учебни групи
- Участвам в организацията на учебни групи, където учим начинаещи на уеб и програмиране
- Групите се срещат всяка седмица, с различни хора в четвъртък и петък
- Няколко човека сме инструктори там и помагаме на участниците да напредват
- Неангажираща и същевременно изключително интересна и предизвикателна дейност
- Търся помощник-инструктори; достатъчни са (някакви) познания по HTML, CSS и основи в Ruby
- Не забравяйте – начинаещи са
Ориентировъчен план за следващите лекции
- 12 ноември – Интроспекция и метапрограмиране, част 1
- 17 ноември – Интроспекция и метапрограмиране, част 2
- 19 ноември – Интроспекция и метапрограмиране, част 3
- 24 ноември – Регулярни изрази
- 26 ноември – Git
- 3 декември – първи тест
- Code jam session – декември
- Web с Ruby, Sinatra, Ruby core & stdlib, конкурентност
- Каквото ви е интересно
Въпрос 1
Кой е родителският клас на класа StandardError
?
Въпрос 2
Кой клас стои най-горе в йерархията на всички изключения (като не броим Object
)?
Въпрос 3
От какъв тип са изключенията, хвърлени така: raise 'foo'
?
Въпрос 4
Как ще се оцени следният израз?
begin
raise 'oh noes!'
rescue Exception
'A general exception has occurred.'
rescue RuntimeError
'A standard error has occurred.'
end
На низа "A general exception has occurred."
. Изключението се хваща от първия rescue
, който споменава клас exception_class
, за който е вярно, че exception.is_a?(exception_class)
.
Call stack
- Нещо, което практически всеки език за програмиране осигурява и всяка програма използва
- Пази реда на извикване на определени вложени методи и "указател" към техните аргументи и локални променливи
- Бездънната рекурсия – извикване на нови и нови методи, без старите извиквания да са приключили – го препълват
- Има място за N на брой вложени извиквания (например, 65к)
- Всеки ред код в Ruby се намира някъде в този стек
Достъп до call стека
- Методът
caller
връща списък с низове, които показват къде сме в текущия call стек
- Показват имената на методите, които са извиквани, в обратен на извикването ред
- В кой файл е станало извикването
- На кой ред във файла
puts с много аргументи
лирическо отклонение
- Може да извикате
puts
с нула или повече аргумента
- Извикване с нула аргумента ще изведе на екрана нов ред
- Извикване с три аргумента ще изведе трите неща на отделен ред
- Ако някой от аргументите е списък, елементите на списъка ще бъдат изведени на отделни редове
puts :foo, [:bar, :baz]
ще изведе foo
, bar
и baz
на три отделни реда
Пример за call stack
# inception.rb:
def roll_the_ball() go_deep end
def go_deep() we_need_to_go_deeper end
def we_need_to_go_deeper() even_deeper_than_that end
def even_deeper_than_that() puts(caller) end
roll_the_ball
Изпълняваме го с ruby inception.rb
.
Пример за call stack - резултат
Примерът от предния слайд ще продуцира:
inception.rb:3:in `we_need_to_go_deeper'
inception.rb:2:in `go_deep'
inception.rb:1:in `roll_the_ball'
inception.rb:6:in `<main>'
Call стекът обикновено е по-дълбок
(съкратен) пример от irb
> puts caller
.../irb/workspace.rb:86:in `eval'
.../irb/workspace.rb:86:in `evaluate'
.../irb/context.rb:380:in `evaluate'
.../irb.rb:492:in `block (2 levels) in eval_input'
.../irb.rb:624:in `signal_status'
.../irb.rb:489:in `block in eval_input'
.../irb/ruby-lex.rb:247:in `block (2 levels) in each_top_level_statement'
.../irb/ruby-lex.rb:233:in `loop'
.../irb/ruby-lex.rb:233:in `block in each_top_level_statement'
.../irb/ruby-lex.rb:232:in `catch'
.../irb/ruby-lex.rb:232:in `each_top_level_statement'
.../irb.rb:488:in `eval_input'
.../irb.rb:397:in `block in start'
.../irb.rb:396:in `catch'
.../irb.rb:396:in `start'
.../bin/irb:11:in `'
=> nil
Изключения като цяло
- Изключенията са механизъм за обработка на грешки
- Придвижвате се нагоре (bubble-up-ват) по call стека от мястото на възникване, докато бъдат хванати или стекът свърши
- Затова позволяват разделяне обработката на грешктите от мястото, където са възникнали
- Ако никой не ги хване нейде по стека, програмата ви умира
Основни атрибути
Изключенията в Ruby са обекти като всичко останало – инстанции на клас Exception
или негов наследник.
Имат три основни атрибута:
- Tип (клас) –
KeyError
, RuntimeError
, NoFriendsException
– за автоматична обработка
- Tекстово съобщение – "undefined local variable or method `foo' for main:Object" – за хора
- Backtrace (call stack trace) – отпечатване на мястото в call стека, където се е случила грешката – отново за хора
Основни атрибути (2)
Нека имаме инстанция на изключение в променлива error
. Тогава:
- Tипът (класът) достъпваме с
error.class
(както и всеки друг Ruby обект)
- Съобщението с
error.message
- Backtrace (call stack trace) –
error.backtrace
- Има и още няколко метода, вижте документацията на
Exception
Някои вградени изключения
foo # NameError: undefined local variable or method `foo' for main:Object
1 / 0 # ZeroDivisionError: divided by 1
File.open # ArgumentError: wrong number of arguments (0 for 1..3)
File.open('/Ah?') # Errno::ENOENT: No such file or directory @ rb_sysopen - /Ah?
Между другото, Errno::ENOENT
си е нормално изключение:
Errno::ENOENT.ancestors.take_while { |kind| kind != Object }
# => [Errno::ENOENT, SystemCallError, StandardError, Exception]
Наши собствени изключения
За да си направим клас-изключение, обикновено наследяваме от RuntimeError
или StandardError
:
class NoFriendsError < StandardError
end
- Обикновено тези класове са празни
- Нужната функционалност я получаваме наготово от
Exception
- Съществуват само за разграничение по тип
Как да ползваме изключения
Може да разделим изключенията на два основни вида.
- Непредвидими грешки, причинени от "околната среда".
- Програмистки грешки, причинени от неправилна употреба на парче код.
Непредвидими грешки
- Изчерпано дисково пространство, мрежови грешки, неправилни входни данни от потребител и прочее
- Обикновено се прихващат с
rescue
от нас или от ползвателите на нашия код
- Програмата реагира по някакъв начин – информира потребителя, пише в лог файл, прави повторни опити след време...
Програмистки грешки
- Не трябва да се хващат – трябва програмата да гръмне
- Неправилна употреба на даден код – грешни/липсващи аргументи и прочее
- Спазва се принципът "fail early"
Какво да хващаме?
- Възможно по-малко, от конкретен тип
- Точно поради горепосочените причини, е много вредно да хващате
Exception
или да обгръщате огромни части от програмата си с begin ... rescue Exception
Изключения в библиотеки
It is recommended that a library should have one subclass of StandardError or RuntimeError and have specific exception types inherit from it. This allows the user to rescue a generic exception type to catch all exceptions the library may raise even if future versions of the library add new exception subclasses.
Изключения в библиотеки (2)
- Правите си клас, наследяващ от, да кажем,
RuntimeError
class Skeptic::Error < RuntimeError; end
- Всички останали изключения във вашата библиотека наследяват от този клас
- За "програмистки" грешки е окей да ползвате вградените класове (
ArgumentError
, NotImplementedError
и прочее)
- Възникването на такива грешки е знак за грешна употреба на вашия код и е важно да се видят рано (по време на разработка и тестване, а не в production)
catch и throw
- Сходни на
raise
и rescue
- Не ползват изключения вътрешно
- Служат за предаване на информация по стека (control flow)
catch и throw
def iterate_pairs(hash)
hash.values.each { |array| iterate_values array }
end
def iterate_values(array)
array.each do |item|
if item == 'Nemo'
puts 'Found Nemo!'
throw :done
end
end
end
animals = {cats: %w[Simba], fish: %w[Crispy Nemo], boars: %w[Pumba]}
catch(:done) { iterate_pairs(animals) }
Този пример е доста синтетичен.
catch и throw
накратко
catch
приема символ и блок.
- Нещо в блока може да извика
throw
със същия символ.
- Когато това стане, Ruby започва да търси обратно по стека до съответния
catch
.
- Когато бъде намерен, изпълнението продължава след
catch
-а.
throw
взема допълнителен аргумент, който е върнатата стойност на catch
.
- Ако няма
throw
, стойността на catch
е последния оценен израз.
Всички възможни аргументи на метод
лирическо отклонение с елементи на преговор
def an_example_of_great_method_design(
a,
_,
_,
b,
c = :something,
*splat,
d,
(e, f),
keyword:,
with_default: 'value',
**other_keyword_args,
&some_block
)
end
Всички възможни аргументи на метод (2)
- Не може да имате аргумент със стойност по подразбиране след splat
- Може да ползвате скоби за "деструктивно" присвояване (разпадане на списък)
- Такъв метод е очевидно лоша идея
- Едно от правилата на Sandi Metz е "не повече от 4 аргумента на метод"
- В горното се броят и keyword аргументи, както и ключовете в аргументи тип
options = {}
Sandi Metz
Като споменахме Санди...
I'm the author of Practical Object-Oriented Design in Ruby (POODR). I believe in simple code and straightforward explanations. I want to help you transform your code and make the pain go away.
- Тя е едно от най-известните имена в Ruby community-то
- Погледнете сайта ѝ
- Следвайте я
Code spelunking: skeptic
Гмуркане в дълбините на skeptic, за да разберем как работи и как да правим това с други библиотеки.
Четвърто предизвикателство
тъжната действителност
Искахме от вас нещо просто:
remove_duplicates [-1, 4, -1, 33, 33, 42, 4] # [-1, 4, 33, 42]
- Google търсене за "ruby unique numbers in array" вади един StackOverflow въпрос с едно от възможните решения
- Това не е лошо – пак сте научили нещо :)
- Само едно решение не взима точка; дълго е 44 реда и се чупи в един от четирите теста
Четвърто предизвикателство - добри решения (1)
Едно от възможните добри решения:
def remove_duplicates(integers)
integers.each_with_object([]) do |integer, uniques|
uniques << integer unless uniques.include? integer
end
end
- Обърнете внимание на именуването –
integers
Четвърто предизвикателство - добри решения (2)
Още един вариант:
def remove_duplicates(integers)
integers & integers
end
Някои вариации на горното:
def remove_duplicates(integers) integers | integers end
def remove_duplicates(integers) [] | integers end
Четвърто предизвикателство - добри решения (3)
Частично приемливо:
def remove_duplicates(integers)
integers.to_set.to_a
end
Проблеми с това решение:
- Изисква
require 'set'
- Разчита, че
Set
използва вътрешно Hash
, като Hash
пази наредбата си
- Проблемът е, че
Hash
пази тази наредба по спецификация, докато за Set
не пише такова нещо в документацията
Четвърто предизвикателство - добри решения (4)
Както и това:
def remove_duplicates(integers)
integers.group_by { |integer| integer }.keys
end
Четвърто предизвикателство - добри решения (5)
Приемливо, защото е прост, праволинеен и очакван начин за решение на проблема:
def remove_duplicates(integers)
uniques = []
integers.each { |integer| uniques << integer unless uniques.include?(integer) }
uniques
end
Четвърто предизвикателство - проблеми
- Всичко останало, което сте правили, извън тези варианти, е излишно
- Може да се приеме и за грешно
- Твърде много грешки, просто няма да има време да разгледаме всички
- Прегледайте решенията и вижте какво не трябва да се прави
Четвърто предизвикателство - проблеми
Най-общо:
- За съжаление, все още проблеми с идентация и конвенции
- Лошо именуване (на места много лошо)
- Излишни неща (
hash[:foo] += 1
вместо hash[:foo] = true
)
- Невладеене на
Enumerable