Sekarang hampir setiap developer sudah familiar dengan konsep "asynchrony" dalam pemrograman. Di era ketika produk informasi sangat diminati sehingga mereka dipaksa untuk secara bersamaan memproses sejumlah besar permintaan dan juga berinteraksi secara paralel dengan sejumlah besar layanan lain - tanpa pemrograman asynchronous - di mana pun. Kebutuhan itu ternyata begitu besar sehingga bahasa yang terpisah bahkan dibuat, fitur utamanya (selain minimalis) adalah pekerjaan yang sangat optimal dan nyaman dengan kode paralel / bersamaan, yaitu Golang . Terlepas dari kenyataan bahwa artikel tersebut sama sekali bukan tentang dia, saya akan sering membuat perbandingan dan merujuk. Tapi di sini dengan Python, yang akan dibahas dalam artikel ini - ada beberapa masalah yang akan saya jelaskan dan menawarkan solusi untuk salah satunya. Jika Anda tertarik dengan topik ini - silakan, di bawah kucing.
Kebetulan bahasa favorit saya, yang saya gunakan untuk bekerja, mengimplementasikan proyek hewan peliharaan, dan bahkan beristirahat dan bersantai, adalah Python . Saya tak habis-habisnya terpikat oleh keindahan dan kesederhanaannya, kejelasannya, di belakangnya, dengan bantuan berbagai jenis gula sintaksis, ada peluang besar untuk deskripsi singkat tentang hampir semua logika yang mampu dilakukan imajinasi manusia. Saya bahkan pernah membaca bahwa Python disebut bahasa tingkat tinggi, karena dapat digunakan untuk mendeskripsikan abstraksi yang akan sangat sulit untuk dijelaskan dalam bahasa lain.
Tetapi ada satu nuansa serius - Pythonsangat sulit untuk masuk ke dalam konsep bahasa modern dengan kemungkinan menerapkan logika paralel / konkuren. Bahasa, yang idenya berasal dari tahun 80-an dan yang seumuran dengan Java, hingga waktu tertentu tidak menyiratkan pelaksanaan kode apa pun secara kompetitif. Jika JavaScript awalnya memerlukan konkurensi untuk pekerjaan non-pemblokiran di browser, dan Golang adalah bahasa yang benar-benar baru dengan pemahaman nyata tentang kebutuhan modern, Python belum pernah menghadapi tugas-tugas seperti itu sebelumnya.
Ini, tentu saja, adalah pendapat pribadi saya, tetapi menurut saya Python sangat terlambat dengan penerapan asynchrony, karena munculnya pustaka asyncio bawaanadalah, lebih tepatnya, reaksi terhadap munculnya implementasi lain dari eksekusi kode bersamaan untuk Python. Pada dasarnya, asyncio dibangun untuk mendukung implementasi yang ada dan tidak hanya berisi implementasi event loop-nya sendiri, tetapi juga pembungkus untuk pustaka asinkron lainnya, sehingga menawarkan antarmuka umum untuk menulis kode asinkron. Dan Python , yang awalnya dibuat sebagai bahasa yang paling singkat dan mudah dibaca karena semua faktor yang tercantum di atas, saat menulis kode asinkron menjadi kumpulan dekorator, generator, dan fungsi. Situasi itu sedikit diperbaiki dengan penambahan arahan khusus async dan menunggu
Saya tidak akan mencantumkan semuanya dan akan fokus pada salah satu yang saya coba selesaikan: ini adalah deskripsi logika umum untuk eksekusi asinkron dan sinkron. Misalnya, jika saya ingin menjalankan fungsi secara paralel di Golang , maka saya hanya perlu memanggil fungsi tersebut dengan perintah go :
Pelaksanaan fungsi secara paralel di Golang
package main
import "fmt"
func function(index int) {
fmt.Println("function", index)
}
func main() {
for i := 0; i < 10; i++ {
go function(i)
}
fmt.Println("end")
}
Karena itu, di Golang, saya dapat menjalankan fungsi yang sama ini secara sinkron:
Pelaksanaan fungsi di Golang
package main
import "fmt"
func function(index int) {
fmt.Println("function", index)
}
func main() {
for i := 0; i < 10; i++ {
function(i)
}
fmt.Println("end")
}
Dalam Python, semua coroutine (fungsi asinkron) didasarkan pada generator dan peralihan di antaranya terjadi selama panggilan fungsi pemblokiran, mengembalikan kontrol ke loop peristiwa menggunakan direktif hasil . Sejujurnya, saya tidak tahu cara kerja konkurensi / konkurensi di Golang , tetapi saya tidak salah jika saya mengatakan bahwa konkurensi / konkurensi tidak berfungsi seperti di Python . Terlepas dari perbedaan internal implementasi kompiler Golang dan interpreter CPython dan ketidakmungkinan membandingkan paralelisme / konkurensi di dalamnya, saya masih akan melakukan ini dan tidak memperhatikan eksekusi itu sendiri, tetapi pada sintaksisnya. Dengan PythonSaya tidak dapat mengambil fungsi dan menjalankannya secara paralel / bersamaan dengan satu operator. Agar fungsi saya berfungsi secara asinkron, saya harus secara eksplisit menulisnya sebagai asinkron sebelum dideklarasikan, dan setelah itu tidak lagi hanya sebagai fungsi, tetapi sudah menjadi coroutine. Dan saya tidak dapat mencampur panggilan mereka dalam satu kode tanpa tindakan tambahan, karena fungsi dan coroutine di Python adalah hal yang sama sekali berbeda, terlepas dari kesamaan dalam deklarasi.
def func1(a, b):
func2(a + b)
await func3(a - b) # , await
Masalah utama saya adalah kebutuhan untuk mengembangkan logika yang dapat berjalan baik secara sinkron maupun asinkron. Contoh sederhananya adalah perpustakaan saya untuk interaksi dengan Instagram , yang sudah lama saya tinggalkan, tetapi sekarang saya mengambilnya lagi (yang mendorong saya untuk mencari solusi). Saya ingin menerapkan di dalamnya kemampuan untuk bekerja dengan API tidak hanya secara sinkron, tetapi juga secara asinkron, dan ini bukan hanya keinginan - saat mengumpulkan data di Internet, Anda dapat mengirim sejumlah besar permintaan secara asinkron dan mendapatkan jawaban untuk semuanya lebih cepat, tetapi pada saat yang sama, pengumpulan data besar-besaran tidak selalu dibutuhkan. Saat ini, perpustakaan mengimplementasikan yang berikut: untuk bekerja dengan Instagramada 2 kelas, satu untuk pekerjaan sinkron, yang lainnya untuk asinkron. Setiap kelas memiliki kumpulan metode yang sama, hanya di kelas pertama metode sinkron, dan kelas kedua asinkron. Setiap metode melakukan hal yang sama - kecuali bagaimana permintaan dikirim ke Internet. Dan hanya karena perbedaan dalam satu tindakan pemblokiran, saya harus menggandakan logika hampir sepenuhnya di setiap metode. Ini terlihat seperti ini:
class WebAgent:
def update(self, obj=None, settings=None):
...
response = self.get_request(path=path, **settings)
...
class AsyncWebAgent:
async def update(self, obj=None, settings=None):
...
response = await self.get_request(path=path, **settings)
...
Segala sesuatu yang lain dalam metode pembaruan dan dalam pembaruan coroutine benar-benar identik. Dan seperti yang diketahui banyak orang, duplikasi kode menambah banyak masalah, terutama dalam hal memperbaiki bug dan pengujian.
Saya menulis pustaka pySyncAsync saya sendiri untuk mengatasi masalah ini . Idenya adalah sebagai berikut - alih-alih fungsi dan coroutine biasa, generator diimplementasikan, di masa depan saya akan menyebutnya template. Untuk menjalankan template, Anda perlu membuatnya sebagai fungsi biasa atau coroutine. Template, ketika dijalankan pada saat ia perlu mengeksekusi kode asinkron atau sinkron di dalamnya, mengembalikan objek Call khusus menggunakan yield, yang menentukan apa yang harus dipanggil dan dengan argumen apa. Bergantung pada bagaimana template akan dibuat - sebagai fungsi atau sebagai coroutine - ini adalah bagaimana metode yang dijelaskan dalam objek Call akan dijalankan .
Saya akan menunjukkan contoh kecil template yang mengasumsikan kemampuan untuk membuat permintaan ke google :
Contoh permintaan ke google menggunakan pySyncAsync
import aiohttp
import requests
import pysyncasync as psa
# google
# Call
@psa.register("google_request")
def sync_google_request(query, start):
response = requests.get(
url="https://google.com/search",
params={"q": query, "start": start},
)
return response.status_code, dict(response.headers), response.text
# google
# Call
@psa.register("google_request")
async def async_google_request(query, start):
params = {"q": query, "start": start}
async with aiohttps.ClientSession() as session:
async with session.get(url="https://google.com/search", params=params) as response:
return response.status, dict(response.headers), await response.text()
# 100
def google_search(query):
start = 0
while start < 100:
# Call , google_request
call = Call("google_request", query, start=start)
yield call
status, headers, text = call.result
print(status)
start += 10
if __name__ == "__main__":
#
sync_google_search = psa.generate(google_search, psa.SYNC)
sync_google_search("Python sync")
#
async_google_search = psa.generate(google_search, psa.ASYNC)
loop = asyncio.get_event_loop()
loop.run_until_complete(async_google_search("Python async"))
Saya akan ceritakan sedikit tentang struktur internal perpustakaan. Ada kelas Manajer , di mana fungsi dan coroutine terdaftar untuk dipanggil menggunakan Panggilan . Dimungkinkan juga untuk mendaftarkan templat, tetapi ini opsional. Kelas Manajer memiliki metode register , generate, dan template . Metode yang sama dalam contoh di atas dipanggil langsung dari pysyncasync , hanya saja mereka menggunakan instance global kelas Manager , yang sudah dibuat di salah satu modul perpustakaan. Nyatanya, Anda dapat membuat instance Anda sendiri dan memanggil metode register , generate dan template darinya.sehingga mengisolasi manajer dari satu sama lain jika, misalnya, konflik nama dimungkinkan.
Metode register bertindak sebagai dekorator dan memungkinkan Anda untuk mendaftarkan fungsi atau coroutine untuk panggilan lebih lanjut dari template. Dekorator register menerima sebagai argumen nama di mana fungsi atau coroutine terdaftar di manajer. Jika namanya tidak ditentukan, maka fungsi atau coroutine didaftarkan dengan namanya sendiri.
Metode template memungkinkan Anda mendaftarkan generator sebagai template di pengelola. Ini diperlukan agar bisa mendapatkan templat berdasarkan nama. Hasilkan
metodememungkinkan Anda membuat fungsi atau coroutine berdasarkan template. Dibutuhkan dua argumen: yang pertama adalah nama template atau template itu sendiri, yang kedua adalah "sync" atau "async" - apa yang akan menghasilkan template - ke fungsi atau ke coroutine. Pada output, metode generate memberikan fungsi atau coroutine yang sudah jadi.
Saya akan memberikan contoh membuat template, misalnya di coroutine:
def _async_generate(self, template):
async def wrapper(*args, **kwargs):
...
for call in template(*args, **kwargs):
callback = self._callbacks.get(f"{call.name}:{ASYNC}")
call.result = await callback(*call.args, **call.kwargs)
...
return wrapper
Di dalam, sebuah coroutine dibuat, yang hanya melakukan iterasi di atas generator dan menerima objek dari kelas Call , kemudian mengambil coroutine yang terdaftar sebelumnya dengan nama (nama diambil dari panggilan ), memanggilnya dengan argumen (yang juga diambil dari panggilan ) dan hasil dari menjalankan coroutine ini juga disimpan dalam panggilan .
Objek dari kelas Panggilan hanyalah wadah untuk menyimpan informasi tentang apa dan bagaimana memanggil dan juga memungkinkan Anda untuk menyimpan hasilnya sendiri. wrapper juga dapat mengembalikan hasil eksekusi template; untuk ini, template dibungkus dalam kelas Generator khusus , yang tidak ditampilkan di sini.
Saya telah menghilangkan beberapa nuansanya, tetapi saya harap saya telah menyampaikan intinya secara umum.
Sejujurnya, artikel ini ditulis oleh saya untuk berbagi pemikiran saya tentang memecahkan masalah dengan kode asinkron dengan Python.dan, yang paling penting, mendengarkan pendapat warga Khabrav. Mungkin saya akan bertemu seseorang dengan solusi lain, mungkin seseorang akan tidak setuju dengan penerapan khusus ini dan akan memberi tahu Anda bagaimana Anda dapat membuatnya lebih baik, mungkin seseorang akan memberi tahu Anda mengapa solusi seperti itu tidak diperlukan sama sekali dan Anda tidak boleh mencampur sinkron dan kode asynchronous, pendapat Anda masing-masing sangat penting bagi saya. Juga, saya tidak berpura-pura benar dengan semua alasan saya di awal artikel. Saya berpikir secara ekstensif tentang topik YP lain dan bisa saja salah, ditambah lagi ada kemungkinan bahwa saya mungkin membingungkan konsepnya, tolong, jika Anda tiba-tiba melihat ada ketidakkonsistenan - jelaskan di komentar. Saya juga akan senang jika ada perubahan pada sintaks dan tanda baca.
Dan terima kasih atas perhatian Anda terhadap masalah ini dan khususnya artikel ini!