Cara menulis kode seperti Senior Data Engineer
Menulis kode berkinerja tinggi tanpa menimbulkan lebih banyak masalah bagi diri Anda sendiri di masa mendatang.
Selamat!
Jika Anda membaca ini, Anda mungkin ingin menjadi lebih baik dalam menulis kode untuk rekayasa data. Dalam panduan ini, saya akan menunjukkan kepada Anda bagaimana pendekatan saya menulis kode sebagai sarana pemecahan masalah.
Sedikit tentang saya
Saya seorang Insinyur Data Senior di Headspace Health dan telah mempraktikkan rekayasa data selama 5+ tahun. Lebih lanjut di situs web pribadi saya .
I. KONSEP LATAR BELAKANG
Sepanjang artikel ini, saya akan mengacu pada kode sebagai deklaratif vs. non-deklaratif (imperatif) :
- Kode imperatif (non-deklaratif) memberi tahu kompiler Anda apa yang dilakukan program, langkah demi langkah. Kompiler tidak dapat melewati langkah-langkah, karena setiap langkah sepenuhnya bergantung pada langkah sebelumnya.
- Kode deklaratif memberi tahu kompiler Anda apa yang seharusnya menjadi keadaan program yang diinginkan, mengabstraksi langkah-langkah cara mencapainya. Kompiler dapat melompati langkah-langkah atau menggabungkannya, karena semua itu dapat menentukan sebelumnya semua status.
Kompiler modern memiliki segala macam trik untuk membuat kode berjalan lebih cepat dan lebih efisien pada perangkat keras modern. Semakin kompiler dapat memprediksi keadaan program, semakin banyak "trik" yang dapat digunakannya, menghasilkan lebih sedikit instruksi dan manfaat kinerja yang signifikan.
Di bawah ini adalah diagram arsitektur antarmuka kompiler dengan unit pemrosesan (perangkat keras). Biarkan kompiler membantu Anda! Berikan instruksi yang lebih sederhana dan lebih mudah diprediksi.
Singkatnya: Kode deklaratif memanfaatkan kemampuan kompiler modern dan menghasilkan kinerja yang lebih tinggi.
Oke, mari kita menulis beberapa kode!
II. PERNYATAAN MASALAH
Anda memiliki daftar things, mungkin daftarnya kosong, mungkin ada jutaan item, dan Anda memerlukan nilai pertama non-null:
# A small list
things_small = [0, 1]
# An impossibly big list
things_big = list(range(1_000_000))
# A list with nulls and other stuff
things_with_nulls = [None, "", object()]
- Fungsi harus mengembalikan hasil yang akurat — dan tidak mendiskriminasi
0'satau mengosongkan string"". - Performa solusi tidak boleh lambat. Mungkin masuk akal untuk membidik
min = O(1), max = O(k), di manakukuran daftarnya
>> get_first_non_null([1, 2, 3])
1
>> get_first_non_null([None, 2, 3])
2
>> get_first_non_null([None, 0, 3])
0
>> get_first_non_null([None, None, None])
None
>> get_first_non_null([])
None
>> get_first_non_null([None, "", 1])
""
Solusi 1: Pemahaman Daftar [Solusi Sederhana]
Saya yakin setiap insinyur data telah memecahkan masalah ini puluhan kali. Kacang mudah! Ulangi daftar dan lihat item mana yang bukan nol, lalu kembalikan nilai pertama:
def get_first_non_null_list_comp(my_vals: list, default=None):
"""
Get first non-null value using
list comprehension.
"""
filtered_vals = [x for x in my_vals if x is not None]
if len(filtered_vals)>0:
return filtered_vals[0]
else:
return default
- Anda perlu mengoperasikan setiap elemen dalam daftar. Ini bisa lambat jika daftar Anda MASSIVE
- Pemahaman daftar pada dasarnya menyalin daftar tersebut, jadi ini mungkin membutuhkan banyak memori. Kecuali jika kami beroperasi di tempat (
my_vals = [x for x in my_vals]) yang dapat menimbulkan masalah dengan menimpa daftar asli. Jadi kita harus menghindari melakukan itu. - Mengakses elemen pertama dalam daftar
list[0]adalah non-deklaratif >> artinya, program Anda tidak memiliki jaminan akan seperti apa atributnya, sampai ia mendapatkannya. Ini OK untuk python, sebagian besar waktu. Tetapi ketika Anda mulai menulis lebih banyak "kode yang digunakan organisasi", Anda cenderung melihat contoh di mana mengakses elemen dalam daftar menjadi serba salah. Misalnya:customer_email = response["data"][0]["custom_attributes"][-1]["email"]<<EEEEEK! - Ada 2 pernyataan pengembalian — ini sebagian non-deklaratif dan meningkatkan kompleksitas kode (berpotensi membatasi perpanjangan).
Jadi kita dapat mengubah fungsi untuk beralih melalui daftar tanpa memproses semua nilai:
def get_first_non_null_loop(my_vals: list, default=None):
"""
Get first non-null value using
a loop.
"""
for x in my_vals:
if x is not None:
return x
# Otherwise, return the default value
return default
Tapi masih ada kekurangannya:
- Kodenya gila dan tidak akan mendapat manfaat dari vektorisasi
- Kode tidak bersifat deklaratif — kompiler kami sedih.
- Mirip dengan solusi 1 di atas, ada 2 pernyataan pengembalian. Saya ingin hanya ada 1.
Solusi 3: Filter menggunakan Generator [Hard Solution]
Memuat nilai secara dinamis menggunakan filterfungsi bawaan python yang membuat generator yang memungkinkan kita mengakses dan mengevaluasi setiap komponen secara dinamis:
from operator import is_not
from functools import partial
def get_first_non_null_generator(my_vals: list, default=None):
"""
Get first non-null value using
a generator (via filter).
"""
# Create a generator of values
filtered_vals = filter(partial(is_not, None), my_vals)
# Iterate and get the first not none value
return next(filtered_vals, default)
- Operator adalah generator/iterator
filter, artinya hanya mengevaluasi item yang diperlukan. Karena kita menggunakannextfungsi tersebut, pada dasarnya akan memuat dengan malas. - Fungsi ini memungkinkan kita untuk
partialsecara dinamis menerapkan evaluasi python tercepat ke nilai >>is not None>> jika tidak, jika kita menggunakan sesuatu seperti[x for x in my_list if x]itu maka0'sakan dikecualikan. - Fungsi mendapatkan item berikutnya dari iterator
next. Memori tidak meledak, karena kita hanya mendapatkan 1 nilai dalam satu waktu. Inidefaultdiatur secara eksplisit, jika tidak, ini akan memunculkan aStopIterationsetelah iterator habis. - Sifat deklaratif memungkinkan untuk vektorisasi (dan peningkatan kompilasi).
- Juga memungkinkan untuk kompilasi tepat waktu juga, jika kami ingin memperluas untuk pengoptimalan lebih lanjut.
Senang saya mendapatkan minat Anda! Saya akan menjelaskan secara rinci lain kali. Sementara itu, Anda dapat membaca sedikit tentangnya di sini: Vektorisasi: Alat Utama Untuk Meningkatkan Performa Pada CPU Modern
IV. MEMPERLUAS SOLUSI KAMI
Mendapatkan nilai tidak kosong pertama dari kamus.
Mendapatkan item pertama dari daftar agak mudah. Tapi bagaimana dengan mendapatkan nilai tidak kosong pertama dari kamus, berdasarkan sekumpulan kunci?
Misalnya, ambil dokumen berikut:
{
"key": {
"field_1": "one",
"field_2": "two"
}
}
Karena solusi ke-3 kami get_first_non_null_generator()menggunakan iterator apa pun, kami dapat membuat mapperyang mengikat dokumen kami ke kunci pencarian, dan menggunakan fungsi kami seperti ini:
my_doc = {
"field_1": "one",
"field_2": "two"
}
# Get the first non-empty value from a dictionary:
res = get_first_non_null_generator(
map(my_doc.get, ("field_1", "field_2"))
)
# We should get the first non-empty value
assert res == "one"
Berikut adalah contoh yang sedikit lebih panjang (yang lebih mirip dengan kasus penggunaan yang saya miliki untuk menulis kode ini):
# A dict of fields with default and example values
my_dict = {
"name": {
"example": "Willy Wonka"
},
"country": {
"default": "USA",
"example": "Wonka-land"
},
"n_wonka_bars": {
"default": 0,
"example": 11
},
"has_golden_ticket": {
"default": False
},
"is_an_oompa_loompa": {
"description": "Is this person an Oompa Loompa?"
}
}
# Now I want to get an example record, from default/example vals:
expected_result = {
"name": "Willy Wonka",
"country": "Wonka-land",
"n_wonka_bars": 11,
"has_golden_ticket": False,
"is_an_oompa_loompa": None
}
# Iterate through fields, though if we wanted to
# get crazy, we can compress to a single line (not shown)
example_record = {}
for key, value in my_dict.items():
# We want "examples" before "default", if any
example_record[key] = get_first_non_null_generator(
map(value.get, ("example", "default"))
)
# We should get the above expected result
assert example_record == expected_result
Inilah kasus penggunaan yang sangat canggih untuk mengakses atribut kelas menggunakan fungsi parsial dan pembuat peta:
from typing import Any, Optional
from operator import attrgetter
class FieldAttributes:
"""
Field attributes.
We will want to access these dynamically
"""
example: Any
default: Any
description: Optional[str]
def __init__(self, example=None, default=None, description=None):
self.example = example
self.default = default
self.description = description
class Field(FieldAttributes):
"""Class representing a field"""
name: str
attrs: FieldAttributes
def __init__(self, name, **kwargs):
self.name = name
self.attrs = FieldAttributes(**kwargs)
class UserData:
"""Class representing our user data"""
name = Field("user_name", example="Willy Wonka")
country = Field("country", default="USA", example="Wonka-land")
n_wonka_bars = Field("n_wonka_bars", default=0, example=11)
has_golden_ticket = Field("has_golden_ticket", default=False)
is_an_oompa_loompa = Field("is_an_oompa_loompa",
description="Is this person an Oompa Loompa?"
)
# Access all the fields here
fields = (
name,
country,
n_wonka_bars,
has_golden_ticket,
is_an_oompa_loompa
)
# ------------------------------------------------
# We could compress it all down to something even tighter:
example_record = {
k.name: get_first_non_null_generator(
map(k.attrs.__getattribute__,
("example", "default")
)
)
for k in UserData.fields
}
assert example_record == expected_result
"""
If we were concerned with high-performance (at the expense
of readibility), we could compress everything further
into a single context – which could translate
neatly within a vectorized library. But this is way overkill
"""
example_record = dict(
zip(
map(attrgetter('name'), UserData.fields),
map(
get_first_non_null_generator,
map(
attrgetter("attrs.example", "attrs.default"),
UserData.fields
)
)
)
)
assert example_record == expected_result
Pertimbangkan sebanyak mungkin faktor ini saat menulis kode Anda di muka, dan dokumentasikan asumsi Anda. (Tidak apa-apa untuk mengambil jalan pintas! Selama Anda memberi tahu diri Anda di masa depan dalam dokumentasi.)
V. BERPIKIR TENTANG SOLUSI
Sebagian besar menjadi pengembang senior adalah cara Anda memikirkan masalah. Sebagian besar masalah yang dihadapi tim data (dan tim perangkat lunak) adalah kombinasi dari masalah teknis dan organisasi.
Untuk menunjukkan, dalam kasus kami, kami menulis kode untuk menemukan nilai pertama non-nulldalam daftar. Namun seiring berjalannya waktu, solusi kami akan digunakan oleh tim lain yang akan menggunakan solusi kami dengan cara yang berbeda. Misalnya, seseorang mungkin mencoba menemukan nilai pertama non-nulldalam kamus, dengan memberikan daftar kunci. Ini belum tentu merupakan hal yang buruk. Tidak dapat dipungkiri bahwa pengembang akan menggunakan kode Anda dengan cara yang tidak Anda antisipasi saat pertama kali menulisnya.
Tanpa intervensi, kompleksitas basis kode dijamin akan meningkat seiring waktu. Jika kerumitannya terlalu parah, Anda akan membuat bola lumpur yang besar . Mengetahui hal ini, bagaimana Anda bisa melindungi status masa depan basis kode Anda?
Jika organisasi kami kecil: kami dapat memberikan komentar dalam kode yang berbunyi:#This code only works with flat lists. Contact YBressler if you have problems
Dengan kata lain, gunakan pemisahan masalah teknis dan organisasi dan selesaikan masing-masing secara terpisah. (Teknis = tulis kode. Organisasi = tinggalkan komentar.)
Sejujurnya, ini adalah solusi yang bagus jika tim Anda kecil. Namun begitu sebuah organisasi mencapai ukuran tertentu, atau orang-orang meninggalkan organisasi tersebut, solusi ini menjadi bermasalah.
Solusi yang lebih baik mempertimbangkan masalah siklus hidup pengembangan perangkat lunak. Ini biasanya berarti memastikan bahwa kode Anda lugas, mudah diuji, dan berperforma tinggi. Jenis kode ini akan memungkinkan pengembang masa depan untuk melakukan refactor dengan mudah, memungkinkan mereka untuk menggunakan kembali dan memperluas solusi asli Anda untuk kebutuhan selanjutnya tanpa meningkatkan kompleksitas basis kode.
Dengan kata lain, kode kita harus menyelesaikan masalah teknis dan organisasi. Teknis = kode berfungsi. Organizational = kode mudah dipahami dan dapat di-refactor dengan mudah.
Dalam contoh kode kami, saya tentu saja memperhatikan kinerja suatu solusi. Saya sama prihatinnya dengan bagaimana pengembang masa depan akan berinteraksi dengan solusi ini.
Jika solusi kode tidak mudah dipahami (tingkat kerumitan tinggi), orang akan takut untuk mengubahnya. Mereka akan menggunakannya dengan cara yang lebih kompleks, atau membuat lebih banyak kode, yang juga menambah kompleksitas basis kode.
VI. KESIMPULAN:
Kesimpulannya, insinyur data senior [mencoba] menulis kode yang mudah dipahami, berkinerja tinggi, tetapi yang terpenting, memecahkan masalah di masa depan dengan mengurangi kompleksitas basis kode.
Sebagai demonstrasi di atas, solusi cepatnya get_first_non_null_generator() cerdas, mudah dibaca, dan berkinerja. Yang terpenting, ini bertujuan untuk mengurangi kompleksitas dalam basis kode.
Referensi:
- Farley, D. (2022). Dalam Rekayasa Perangkat Lunak Modern: Melakukan apa yang berhasil untuk membangun perangkat lunak yang lebih baik dengan lebih cepat (p. 128), Addison-Wesley.

![Apa itu Linked List? [Bagian 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































