Varios conceptos diferentes de programación de Julian que desearía haber sabido cuando comencé
IInevitablemente, al comenzar con un lenguaje de programación, todos estamos obligados a encontrarnos con contratiempos educativos en el camino. Estos contratiempos pueden ser errores tontos, buscar toda la noche una solución a un error o simplemente no conocer alguna sintaxis que podría beneficiar enormemente a su código. Como científico de datos con la mayor parte de mi experiencia en el lado iterativo o orientado a objetos de la programación, este fue sin duda el caso cada vez que me presentaron originalmente el lenguaje Julia hace unos seis años. Sin embargo, he aprendido horas extras y la experiencia educativa me ha enseñado mucha información valiosa que ahora estoy feliz de tener.
¡Otra gran cosa que viene junto con esta experiencia en mi idioma favorito es que puedo eliminar este mismo tipo de barreras de entrada y programación inteligente al compartir esta información! Julia es un lenguaje de programación increíble, ¡especialmente cuando se trata del dominio de la ciencia de datos! Habiendo dicho eso, definitivamente vale la pena aprenderlo, por lo que hoy me gustaría hacer que sus viajes transcurran un poco sin problemas demostrando algunas de mis características favoritas que desearía haber tenido un control firme al ingresar al idioma y en mi humilde comienzo con el idioma.
increíble sintaxis de comprensión
Como programadores, probablemente seamos conscientes de la relación común de amor y odio que a menudo compartimos con los bucles for. Si bien los bucles for son una excelente manera de hacer el trabajo que es fácilmente legible y relativamente efectivo, puede ser un obstáculo grave para el rendimiento, especialmente en lenguajes de programación de un solo subproceso. Hay varias maneras diferentes de evitar esto, algunos ejemplos incluyen el map
función tanto en Julia como en Python y lambda en Python/funciones anónimas en Julia.
La desventaja de ambas técnicas es que a menudo pueden hacer que la sintaxis se vuelva confusa. Además, es posible que los programadores más nuevos no se den cuenta de este tipo de trucos de sintaxis de inmediato. En muchos sentidos, cuando elegimos estas técnicas, tenemos un alto potencial de sufrir en la métrica de legibilidad. Mientras que otros idiomas a menudo tienen algunas comprensiones bastante impresionantes que pueden ayudar con tales problemas, las comprensiones en Julia están en un nivel completamente diferente. Con Julia, podemos crear muy fácilmente comprensiones interpretables de varias líneas. Hay muchas razones diferentes para usar una comprensión de varias líneas en lugar de un bucle for tradicional.
En primer lugar, las comprensiones son más rápidas, no solo un poco más rápidas, sino mucho más rápidas. En segundo lugar, las comprensiones proporcionan un retorno, lo que significa que ya no se crea una estructura fuera de un ciclo y luego se agrega a ella en cada ciclo. La mayor deficiencia de la comprensión la mayor parte del tiempo es su legibilidad. Sin embargo, debido a que la sintaxis de comprensión de Julia funciona con begin
y end
podemos escribir muy fácilmente una comprensión que sea tan legible como un bucle for tradicional.
mycol = []
for x in 1:5
f = y -> y += 2
push!(mycol, f(x))
end
mycol = [begin
f = y -> y += 2
f(x)::Int64
end for x in 1:5]
funciones anónimas
Otra cosa que desearía haber entendido cuando comencé en el lenguaje de programación Julia son las funciones anónimas. Las funciones anónimas son lo que lambda es para Python para Julia, y se crean usando el operador lógico correcto, ->
. Hay algunas razones diferentes por las que esto hubiera sido genial saberlo de inmediato. En primer lugar, las funciones anónimas se usan por todas partes en la documentación de Julia, lo que definitivamente es bastante confuso si no tiene idea de lo que hace este operador. En segundo lugar, las funciones anónimas vienen muy bien… como todo el tiempo. También pueden ayudar a conservar las asignaciones y, en general, acelerar las cosas, ya que no es necesario almacenar el tipo de función, solo sus argumentos.
Estos son realmente fáciles de crear. En el lado izquierdo del operador, proporcionamos los argumentos para nuestra función. Esto puede ser un solo argumento o una tupla, y también podemos anotar esto. En el lado derecho, proporcionamos la función en sí misma; realmente es relativamente sencilla.
julia> f = x::Int64 -> x + 1
#3 (generic function with 1 method)julia> f(5)
6
julia> f = (x::Int64, y::Int64) -> x + y
#1 (generic function with 1 method)
julia> f(5, 5)
10
Además, también podemos utilizar el begin
end
sintaxis en este contexto:
julia> f = x::Int64 -> begin
x + 1
end
#5 (generic function with 1 method)julia> f(5)
6
redirigir E/S estándar
Una cosa que descubrí recientemente es lo fácil que puede ser redirigir la entrada, la salida y el error estándar de Julia durante la evaluación de una determinada parte del código o una aplicación. Esto se hace usando el redirect_stdout
, redirect_stderr
, redirect_stdin
y redirect_stdio
que se puede usar para redirigir múltiples al mismo tiempo con argumentos de palabras clave. Esto puede ser increíblemente conveniente, hay tantos ejemplos diferentes que se pueden pensar; redirect_stderr
podría usarse para arrojar una salida de error estándar en un archivo de registros en lugar de simplemente descargarlos en una terminal, redirect_stdin
podría usarse para capturar y escribir entradas, incluso en un HTTP.Stream
Por ejemplo.
La deficiencia significativa de esto, al menos en lo que he llegado a usar, es que los tipos con los que esto es posible son bastante limitados. Más específicamente, no parece que pueda hacer esto con cualquier cosa que esté en la memoria. Esto es un verdadero fastidio, porque significa que no podría, por ejemplo, ejecutar una función y escribir las salidas estándar en un IOBuffer
para poner en otro lugar, al menos no usando este método y esta técnica. En la aplicación particular para la que originalmente estaba interesado en esto, esto es lo que necesitaba, por lo que fue una gran decepción, sin embargo, ciertamente no resta valor a la amplia gama de posibilidades cuando se trata de escribir estos diferentes tipos de entrada. y salida a archivos o registros!
parámetros de subtipado
Si hay algo que realmente me faltaba en Julia durante el primer o segundo año de uso del lenguaje, sería la capacidad de subtipo de parámetros. Este título puede ser un poco engañoso, ya que puede parecer que estoy hablando de sub-escribir los parámetros de los constructores, pero en realidad me estoy refiriendo a los métodos. En Julia, por supuesto, puede proporcionar parámetros de tipo específicos para crear diferentes métodos para el mismo tipo con diferentes parámetros (que se convierte en un tipo diferente debido a los parámetros)… Por ejemplo, esta función solo funciona específicamente en un Vector
con Int64
como el parámetro:
julia> myfunc(v::Vector{Int64}) = beginend
myfunc (generic function with 1 method)
julia> myfunc(["hello"])
ERROR: MethodError: no method matching myfunc(::Vector{String})
Closest candidates are:
myfunc(::Vector{Int64}) at REPL[6]:1
Stacktrace:
Sin embargo, también podemos usar polimorfismo para aplicar esto a ciertos tipos de vectores basados en subtipos de lo que está contenido dentro del parámetro. Por ejemplo, Vector{<:Number}
:
julia> myfunc(v::Vector{<:Number}) = beginend
myfunc (generic function with 2 methods)
julia> myfunc([5.5, 4.4])
julia> myfunc([5.5 + 2.2im])
julia> myfunc([true])
julia>
Debería ser fácil ver dónde esto es útil, aún así proporcionaré un caso de uso. Digamos que estamos tratando de graficar diferentes vectores, y alguien proporciona un Vector{String}
como la X. Obviamente, vamos a querer manejar esto completamente diferente al espacio numérico. ¿Qué tal si fueran a proporcionar un Vector{Date}
? Bueno, podemos facilitar todas estas discrepancias de tipo increíblemente fácilmente, a pesar de que estos tipos son el tipo de los elementos, y también podemos denotar esas cosas por subtipo, por lo que tenemos que escribir la menor cantidad de métodos posible…
me encanta julia…
conjuntos
Es probable que este sea un poco más obvio para cualquiera que haya usado lenguajes de alto nivel antes; los conjuntos son un tipo de datos que contienen los valores únicos de un iterable dado. Era muy consciente de los conjuntos tanto matemáticamente como como un tipo de datos cuando me mudé a Julia, por supuesto, sin embargo, los estoy colocando en esta lista porque con mi trabajo en Julia realmente comencé a utilizar esos conjuntos mucho más. Por lo tanto, simplemente me gustaría compartir lo fácil que es crear un conjunto dentro de Julia y luego hablar sobre algunos casos de uso para Conjuntos. Para crear un Set, simplemente proyectamos Set sobre cualquier Vector
.
julia> Set(["hi", "hi", "hello"])
Set{String} with 2 elements:
"hi"
"hello"
Solo como ejemplo, digamos que cada nombre en esta lista tenía que ser único. Podríamos realizar rápidamente una verificación para asegurarnos de que todos los nombres sean únicos verificando si las longitudes son las mismas.
julia> s = ["hi", "hello", "hi"]
3-element Vector{String}:
"hi"
"hello"
"hi"julia> length(Set(s)) == length(s)
false
Marco de datos -> dictado
Los DataFrames son una gran estructura de datos, el paquete DataFrames.jl proporciona una infraestructura bastante robusta para trabajar con datos tabulares en memoria en Julia. Sin embargo, una cosa relativamente común que alguien podría querer hacer con un DataFrame
tampoco tiene un enlace inmediatamente obvio, y esto está convirtiendo un DataFrame
en un Dict
. Usted podría esperar que esto sería un simple Dict(DataFrame)
tipo de trato, pero en realidad necesitamos sacar cada columna como un conjunto de pares para lograr este objetivo.
julia> df = DataFrame(:a => [5, 10, 15], :b => [5, 10, 15])
3×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 5 5
2 │ 10 10
3 │ 15 15
Esto incluso se vuelve un poco más confuso; vocación eachcol
devolverá un DataFrameColumn
Bajo circunstancias normales:
julia> eachcol(df)
3×2 DataFrameColumns
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 5 5
2 │ 10 10
3 │ 15 15
Sin embargo, si tuviéramos que usar eachcol
dentro de una comprensión, en cambio obtendríamos una Vector
de Vector
:
julia> [col for col in eachcol(df)]
2-element Vector{Vector{Int64}}:
[5, 10, 15]
[5, 10, 15]
Sin embargo, si llamamos a la pairs
método en nuestro DataFrameColumn
entonces obtendríamos el retorno apropiado de un montón de pairs
que finalmente podemos proporcionar a la Dict
constructor para convertir nuestro DataFrame
en un Dict
:
julia> Dict(pairs(eachcol(df)))
Dict{Symbol, AbstractVector} with 2 entries:
:a => [5, 10, 15]
:b => [5, 10, 15]
Eso no es tan malo, pero definitivamente es bastante fácil ver cómo esto podría ser un poco desafiante para alguien que no esté familiarizado con estos métodos, ¡por eso quería compartir esto!
introspección del método
Otra cosa realmente genial de Julia, que mucha menos gente sabe cómo manejar, es la introspección del método. La introspección de métodos se puede utilizar para darnos información sobre los diferentes métodos de una función. Esto abre una ventana de oportunidad para todo tipo de trucos ingeniosos. Podríamos hacer una introspección de estos parámetros dentro de una función, llamar a cada versión de una función determinada, así como a todo lo demás. También podemos obtener más información sobre diferentes funciones, incluidas las proporcionadas como argumento, lo que sin duda es una característica útil. La introspección de los métodos también es increíblemente fácil, ya que todo lo que tenemos que hacer es llamar al methods
método:
julia> function example_function(i::Int64)end
example_function (generic function with 1 method)
julia> methods(example_function)
# 1 method for generic function "example_function":
[1] example_function(i::Int64) in Main at REPL[27]:1
julia> methods(example_function)[1]
example_function(i::Int64) in Main at REPL[27]:1
julia> methods(example_function)[1].sig
Tuple{typeof(example_function), Int64}
julia> methods(example_function)[1].sig.parameters
svec(typeof(example_function), Int64)
julia> methods(example_function)[1].sig.parameters[1]
typeof(example_function) (singleton type of function example_function, subtype of Function)
julia> methods(example_function)[1].sig.parameters[2]
Int64
No
Otra función realmente genial en Julia es la Not
función. El Not
función nos permite crear lo que se llama un InvertedIndex
. Básicamente, esto significa que podemos indexar un determinado Vector
y elegir cualquier elemento que queramos omitir.
julia> x = [1, 2, 3, 4, 5]
5-element Vector{Int64}:
1
2
3
4
5
julia> x[Not(1:2)]
3-element Vector{Int64}:
3
4
5
campos ambiguos malos
Como probablemente haya escuchado, Julia es un lenguaje de programación realmente rápido, especialmente cuando se compara con otros lenguajes de alto nivel con el tipo de sintaxis que tiene Julia, como Python. Sin embargo, como cualquier otro lenguaje, el rendimiento no dependerá únicamente del compilador, sino también del código que se introduce en el compilador. Julia tiene una cosa particularmente desastrosa que se puede hacer para obstaculizar el rendimiento de manera bastante dramática, y esto es tipeo ambiguo.
La escritura ambigua esencialmente significa que el tipo de algo puede ser cualquier cosa, y el compilador debe inferirlo o resolverlo. No juzgues esto mal; usar un campo ambiguo dentro de una estructura porque hay poca o ninguna otra opción no es necesariamente la preocupación aquí. La preocupación es principalmente cuando esto se usa como normal en lugar de como excepción. Idealmente, los campos ambiguos deben usarse lo menos posible. Si el tipo de su campo debe facilitar diferentes tipos, la mejor técnica para trabajar con algo así es proporcionar un parámetro que proporcione el tipo de este campo. Esto hará que todos los objetos de ese tipo, el tipo que contiene el parámetro, aún tengan campos del mismo tipo, lo que hace que este tipo de cosas sean mucho más predecibles.
mutable struct BadFields
n::Number
end
mutable struct BetterFields{T <: Number}
n::T
end
encontrar funciones
La última característica de Julia con la que desearía haberme cruzado antes es la find
funciones; al menos así es como me gusta llamarlos. Estas funciones se usan comúnmente en algoritmos, y serán una necesidad absoluta si uno quisiera analizar un String
, Por ejemplo. Encontrar casi cualquier cosa por algún campo o valor dentro de él se hará usando funciones de búsqueda. Estos son findall
, findlast
, findfirst
, findnext
y findprev
. Deberían ser bastante obvias las diferencias entre ellos, así que pasemos rápidamente al uso.
Estas funciones toman un Function
como su primer argumento posicional, lo que significa que también puede escribirlos usando el do
sintaxis, y luego tome una colección como segundo argumento. Si estás usando findnext
o findprev
también deberá proporcionar un índice: un Int64
que será el tercer y último argumento posicional sobre esos métodos. Cabe señalar que todos estos devuelven un valor singular a excepción de findall
que devolverá un Vector{Int64}
que contiene los diferentes índices… A menos que, es decir, proporcionemos un String
como el segundo argumento posicional en lugar de un Vector
. Esto convertirá nuestro Int64
en un UnitRange
en todos estos casos. Este rango, por supuesto, representa el índice en el que se encontró. Al usar esto en un String
también podemos reemplazar el primer argumento posicional con un String
:
julia> findall("emmy", "hello, I am emmy")
1-element Vector{UnitRange{Int64}}:
13:16
julia> findfirst(x -> x == 5, 1:10)
5
Afortunadamente, he escrito un artículo completo que profundiza mucho más en estas diferentes funciones, lo que podría ayudar a comprender mejor estas funciones tan importantes en Julia.