Swift для дата-сайентиста: быстрое погружение за 2 часа
Так что отложите все свои дела, выбросьте Python и срочно учите Swift. Google объявил, что TensorFlow переезжает на Swift. А язык, надо сказать, местами довольно странный.
Для затравки посмотрите небольшую презентацию с объяснением, почему Swift и как с этим связан TensorFlow:
Хотя в нем можно будет писать python-подобный код. Разработчики TensorFlow, конечно, пока не забывают Python, но основой фреймворка станет именно Swift. Но исполняться он все равно будет интерпретатором Python, а это снова значит медленно, непараллельно, неэффективно по памяти, без контроля типов и все прочее.
Ну, не совсем с нуля: предполагается, что вы уже хорошо программируете на Python'е, и поэтому многие конструкции Swift далее будут описаны в сравнении с аналогичными Python'овскими конструкциями.
Статья ни в коем случае не претендует на подробное описание языка. Поэтому учим Swift с нуля. Это лишь первое весьма поверхностное знакомство с основными возможностями языка для тех, кто знает Python.
Это хорошо, потому что он основан на обширной базе более ранних языков. Swift — довольно новый язык. Поэтому язык очень быстро эволюционирует.
Несмотря на то, что в интернете полно статей и туториалов по Swift — все они уже устарели. Но в то же время и очень плохо, потому что он пока еще не лишен совсем “детских болезней”. Многочисленные рецепты со StackOverflow вам скорее всего тоже не подойдут, потому что они относятся к предыдущим версиям языка.
2, а в сентябре — уже “сильно другой” Swift 3, через год — “снова другой” Swift 4. Хронология последних событий: в марте 2016 года вышел Swift 2. 1, хотя Swift for Tensorflow — это уже 4. Текущая версия 4. До конца года выйдет Swift 5, в котором будет еще больше нововведений даже в самом языке, не говоря уже о библиотеках. 2-dev.
Поэтому я позволил себе потратить лишь два часа на ознакомление с языком в его текущем виде, чтобы через полгода легче было погружаться в Swift 5 с уже новой версией TensorFlow. В общем, TLDR: язык пока не готов к серьезной разработке для data science.
К счастью, компилятор умеет определять тип по начальному значению. Все переменные строго типизированы, поэтому при объявлении придется указывать тип данных.
let intConst = 5
let strConst = "strings should be in double quotes"
var nonInitVar: Int // для переменной без начального значения тип указывается явно
var intVar = 10
var floatVar = 10.0
var doubleVar: Double = 10.5
var strVar = "double quotes only"
В качестве хорошего тона и для оптимизации вычислений для всех значений, которые не будут в процессе исполнения программы меняться, рекомендуется именно let
. Как вы уже догадались, константы объявляются с помощью let
, а переменные через var
. В остальном, все просто и понятно.
В Python'е есть slice
, а в Swift'е целая пачка типов Range
: открытый, закрытый, неполный снизу и т.д.
Литералами они задаются довольно кратко (но можно было бы и еще короче):
1...5 // от 1 до 5 включительно
1..<5 // не включая 5
...5 // от начала до 5
2… // от 2 до конца
Для задания диапазонов с шагом, отличающимся от единицы, или с нецелыми числами можно использовать функцию stride
:
for i in stride(from: 0.1, to: 0.5, by: 0.1) { print(i)
}
или
for i in stride(from: 0.1, through: 0.5, by: 0.1) { print(i)
}
0. Догадаться невозможно, нужно просто знать, что в первом случае (с to
) диапазон открыт справа (т.е. 5 не включается).
Разработчики Swift'а решили, что у них это будут строки. В каждом языке есть своя чудовищная глупость. Они очень похожи, но отличаются лишь тем, что у Substring нет своей области памяти, и она всегда указывает на кусок памяти какой-то String. Во-первых, есть два строковых типа String и Substring. Идея понятная и правильная, но все эти нюансы можно было легко скрыть в реализации String.
Как получить Substring из String? Дальше хуже. Ничего подобного! Вы думаете, тут есть что-нибудь как в Python'е — str[1:10]
. А как надо? Нельзя индексировать строки целыми числами.
str[str.index(str.startIndex, offsetBy: 1) ..< str.index(str.startIndex, offsetBy: 10)]
Это официальный способ работы со строками. Я не шучу. Этот случай не стал исключением. У каждой дурацкой идеи есть длинное и бессмысленное объяснение.
В ней инвариантно все, кроме двух целых чисел. Обратите еще раз внимание на строку str[str.index(str.startIndex, offsetBy: 1) ..< str.index(str.startIndex, offsetBy: 10)]
. Иными словами, из 87 символов 84 лишние!
Чтобы было по-людски, пишем extension для стандартного типа String:
extension String public subscript(r: Range<Int>) -> Substring { var a = Array(r) let start = index(startIndex, offsetBy: a[0]) let end = index(startIndex, offsetBy: a[-1]) return s[start...end] }
}
Компилятор ругается: Запускаем… Не работает!
error: 'subscript' is unavailable: cannot subscript String with an integer range, see the documentation comment for discussion
Дело в том, что в Swift есть явный хардкод, запрещающий создавать метод subscript
, который принимает диапазон из целых чисел.
Ладно, пойдем другим путем, в 10 раз длиннее, видимо таков уж Swift-way:
extension String { public subscript(i: Int) -> Character { return self[index(startIndex, offsetBy: i)] } public subscript(bounds: Range<Int>) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) return self[start ..< end] } public subscript(bounds: ClosedRange<Int>) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) return self[start ... end] } public subscript(bounds: PartialRangeFrom<Int>) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(endIndex, offsetBy: -1) return self[start ... end] } public subscript(bounds: PartialRangeThrough<Int>) -> Substring { let end = index(startIndex, offsetBy: bounds.upperBound) return self[startIndex ... end] } public subscript(bounds: PartialRangeUpTo<Int>) -> Substring { let end = index(startIndex, offsetBy: bounds.upperBound) return self[startIndex ..< end] }
}
Язык не для краткости, это уже понятно. И в качестве упражнения скопируйте весь этот текст еще раз для типа Substring
.
Зато теперь можно нормально работать со строками:
var str = "Some long string"
let char = str[4]
var substr = str[3 …< 8]
let endSubstr = str[4…]
var startSubstr = str[...5]
let subSubStr = str[...8][2..][1..<4]
Здесь это скорее похоже на смесь tuple
и namedtuple
. Неизменяемая последовательность значений, или tuple
, в Swift устроена немного иначе, чем в Python'е.
let tuple = (100, "value", true) print(tuple.0) // 100
print(tuple.1) // "value"
print(tuple.2) // true var person = (name: "John", age: 24)
print(person.name, person.age) // а еще можно так
let tuple2 = (10, name: "john", age: 32, 115)
print(tuple2.1) // john
print(tuple2.name) // john
Раньше было можно. А вот распаковывать tuple
в аргументы функции нельзя. Возможно, в будущем обратно введут. Потом запретили.
С Set
ами та же история: как и в python'овском множестве можно менять состав элементов, но не тип. Массив (Array
) похож на python'овский list
тем, что его размер можно менять, однако все элементы массива должны иметь один тип данных. В словаре придется определить два типа: для ключей и для элементов.
let immutableArray = [5, 10, 15] var intArr = [10, 20, 30]
var nonInitIntArr: [Int]
var emptyArr: [Int] = []
var otherEmptyArr = [Int]() var names: [String] = ["John", "Anna"] var noninitSet: Set<String>
var emptySet: Set<Int> = []
var otherEmptySet = Set<Int>() var emptyDict: Dictionary<Int, String> = []
var strToArrDict: Dictionary<String, [Int]>
var fullDict: Dictionary<String, Int> = ["john": 24, "anna": 22] let allKeys = fullDict.keys
let allVals = fullDict.values
Кстати, если попробуете проитерироваться по словарю самым ожидаемым способом:
for k in fullDict.keys { print(k, fullDict[k])
}
Про Optional
поговорим отдельно, а итерироваться удобней tuple'ами: то неожиданно получите кучку предупреждений от компилятора, потому что тип значений в словаре fullDict
на самом деле не Int
, а Optional<Int>
(то есть может быть nil
или int
).
for (key, val) in fullDict { print(key, val)
}
Стандартный обход коллекции:
for item in collection { // ...
}
Удобно работать и с диапазонами:
for i in 0...10 { // ...
}
Если индексы нужны выборочно, что конструкция резко удлиняется:
for i in stride(from: 0, to: 10, by: 2) { // ...
}
Еще есть
while someBool { // ...
} repeat { // ...
} while otherBool
Все, как и ожидалось:
func myFunc(arg1: Int, arg2: String) -> Int { // do this // do that return someInt
}
Видимое отличие от Python можно заметить в том, что у аргумента может быть не только имя и тип, но и метка:
func fn1(a: Int, b: Int){ // обычные аргументы без меток
} // при вызове функции имена аргументов необходимо указывать fn1(a: 1, b: 10) func fn2(from a: Int, to b: Int){ // ...
} // теперь в качестве имени аргумента указывается метка
fn2(from: 1, to: 10)
fn2(a: 1, b: 10) // а так уже нельзя func fn3(_ a: Int, to b: Int){ // _ - не использовать имя при вызове
} // первый аргумент указывается без имени
fn3(1, to: 10)
Есть лямбды, здесь они называются closure
{ (arg1: Int, arg2: String) -> Bool in // ... return someBool
})
И вот тут открывается новая внезапность: Естественно, closure можно передавать в функции.
someFunc() { // do this // do that return someInt
}
Но на самом деле это вызов функции someFunc
, которой в качестве последнего аргумента передается closure
, заданная в фигурных скобках. Выглядит как определение функции, только без слова func
. Кстати, если closure
выступает единственным аргументом функции, то круглые скобки можно опустить.
let descArray = array.sorted { $0 > $1 }
let firstValue = array.sorted { $0 > $1 }.first
Для создания сложных типов данных предусмотрены классы и структуры:
struct MyStructure { public var attr1: Int private var count = 0 init(arg1: Int) { attr1 = arg1 } public func method1(arg1: Int, arg2: String) -> Float { // ... return 0.0 // этот метод обязательно должен вернуть значение типа Float }
} class MyClass { public var attr1: Int private var count = 0 init(arg1: Int) { attr1 = arg1 } public func method1(arg1: Int, arg2: String) -> Float { // … return 0.0 // этот метод обязательно должен вернуть значение типа Float }
}
С виду разницы никакой, но она все же есть:
- структуры по умолчанию являются неизменяемыми (immutable), поэтому методы, изменяющие значения атрибутов, следует предварять ключевым словом
mutating
; - структуры всегда передаются по значению, а классы по ссылке;
- классы можно наследовать.
Как вы уже знаете по строкам, у классов и структур есть удобный метод subscript
(аналог python'овских __getitem__
и __setitem__
), позволяющий индексировать данные, чтобы вместо:
let item = someClass.getItem(itemIndex)
let item = someClass.getSubsetOfItems(fromIndex: 0, toIndex: 10)
писать более компактное:
let item = someClass[itemIndex]
let aFewItems = someClass[0...10]
Реализуется он примерно так:
class MyClass { private var myData = [Int: Double]() public subscript(i: Int) -> Double { get { return myData[i]! } set { myData[i] = newValue } }
}
А это еще одна неявная конвенция — если для set
'а не заданы аргументы, то значение передается через переменную newValue
.
Так, а что означает восклицательный знак после myData[i]
? Вы, наверное, спросите: что за newValue
такое?
В Swift для этого есть Optional, который принимает значение определенного при декларации переменной типа или значение nil
. В строго типизированном языке нужен особый способ работы с отсутствующими значениями.
var opt: Optional<Int>
var short: Int?
var anOpt: Optional<Int> = Int(32)
var oneMore: Int? = nil
Как с этим работать?
if opt == nil { print("Значения нет")
} else { print("Значение =", opt!) // обратите внимание на восклицательный знак
}
“force unwrapping”) значения. Оператор !
предназначен для принудительной распаковки (англ. Если при этом opt
был nil
, вы получите runtime crash.
Другая, более рекомендуемая конструкция, — блок if-let
:
if let val = short { print("val - ‘настоящее’ значение short, чистый Int: ", val)
} else { print("short равен nil")
}
Для разворачивания опционала с присвоением ему значения по умолчанию существует оператор ??
, который незамысловато называется nil-coalescing operator.
print(oneMore ?? 0.0) // если значения нет, то будет выведен 0.0
Кроме того, Swift позволяет удобно получить значение атрибута, даже если он упакован в глубине сложной структуры:
if let cityCode = person?.contacts?.phone?.cityCode { // если человек определен, // и у него есть контактные данные, // в которых указан телефон, // в котором обозначен код города } else { // если хоть чего-нибудь нет print("Код города неизвестен")
}
В Swift for Tensorflow заявлена работа с Python'ом, чтобы можно было писать Python-подобный код:
import Python let np = Python.import("numpy")
let a = np.arange(15).reshape(3, 5)
let b = np.array([6, 7, 8])
Но сейчас так не работает и можно писать только вот так:
import Python let np = Python.import("numpy")
let a = np.arange.call(with: 15).reshape.call(with: 3, 5)
let b = np.array.call(with: [6, 7, 8])
Когда будет сделано по-нормальному, пока не известно, потому что нужны изменения в языке, а предложение на изменение еще даже не подано, хотя активно обсуждается (уже в третий раз и пока безрезультатно).
7, установленным в /usr/local/lib/python27 (опять хардкод). Кроме того, сейчас Swift умеет работать только с Python 2. В виду имеющейся разницы между Python 2 и 3 с точки зрения С-структур данных и C-вызовов в ближайшее время эта проблема также не разрешится. Ни с какими виртуальными средами тоже не совместим.
Наконец-то добрались до главного, ради чего все и затевалось.
Начнем с перемножения матриц:
import TensorFlow var tensor = Tensor([[1.0, 2.0], [2.0, 1.0]])
for _ in 0...100000 { tensor = tensor * tensor - tensor
}
Вот только пока медленнее, причем в разы. Выглядит точно красивее, короче и понятнее, чем на Python'е c tf.while_loop
вкупе с созданием сессии и инициализацией переменных. И GPU, конечно же, не поддерживается.
Попробуйте ввести его с клавиатуры. Кстати, для матричного умножения надо использовать не *
и не @
, а замечательный знак ⊗
.
Хотя не сделаем: документации нет, готовых слоев нет, оптимизаторов нет, — в общем, ничего еще нет. Давайте уже сделаем нейросеточку!.. вышеприведенное видео и единственный пример). Само собой, можно вручную перемножать тензоры, рассчитывать градиенты и изменение весов (см. Но этого мы делать, конечно, не будем.
Подождем. Язык интересный, вот только для реального применения в data science пока не готов.