Pencetus Cemerlang dan Cara Membuatnya

Gambar oleh John Matychuk di Unsplash

Masalah

Semasa belajar di Make School saya telah melihat fungsi rakan sebaya saya yang membuat senarai item.

s = 'baacabcaab'
p = 'a'
def find_char (string, character):
  indeks = senarai ()
  untuk indeks, str_char dalam menghitung (rentetan):
    jika str_char == aksara:
      indeks.append (indeks)
  indeks pulangan
cetak (find_char (s, p)) # [1, 2, 4, 7, 8]

Pelaksanaan ini berfungsi, tetapi ia menimbulkan beberapa masalah:

  • Bagaimana jika kita hanya mahu hasil pertama; adakah kita perlu membuat fungsi yang sepenuhnya baru?
  • Bagaimana jika semua yang kami lakukan adalah gelung ke atas hasil sekali, adakah kita perlu menyimpan semua elemen dalam ingatan?

Iterators adalah penyelesaian ideal untuk masalah ini. Mereka berfungsi seperti "senarai malas" di dalamnya bukannya mengembalikan senarai dengan setiap nilai yang dihasilkan dan mengembalikan setiap elemen satu demi satu.

Iterators malas mengembalikan nilai; menyimpan memori.

Jadi mari kita menyelidiki belajar tentang mereka!

Iterators terbina dalam

Penyahir yang paling kerap adalah menghitung (), dan zip (). Kedua-dua nilai ini malas kembali dengan seterusnya () dengan mereka.

julat (), bagaimanapun, bukan pemula, tetapi "malas iterable." - Penjelasan

Kita boleh menukarkan julat () ke dalam iterator dengan iter (), jadi kami akan melakukannya untuk contoh kami demi pembelajaran.

my_iter = iter (range (10))
cetak (seterusnya (my_iter)) # 0
cetak (seterusnya (my_iter)) # 1

Apabila setiap panggilan berikutnya () kami mendapat nilai seterusnya dalam julat kami; masuk akal? Sekiranya anda ingin menukar iterator kepada senarai anda, anda hanya memberikan pembuat senarai itu.

my_iter = iter (range (10))
cetak (senarai (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Jika kita meniru tingkah laku ini, kita akan mula memahami lebih lanjut mengenai cara kerja iterator.

my_iter = iter (range (10))
my_list = list ()
cuba:
  sementara Benar:
    my_list.append (next (my_iter))
kecuali StopIteration:
  lulus
cetak (my_list) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Anda dapat melihat bahawa kami perlu membungkusnya dalam pernyataan tangkapan cuba. Itulah sebab iterators meningkatkan StopIteration apabila mereka sudah letih.

Oleh itu, jika kita memanggil seterusnya pada pemalar pelbagai lelah, kita akan mendapat ralat itu.

seterusnya (my_iter) # Meningkat: StopIteration

Membuat pemasa

Mari kita cuba membuat pemula yang berkelakuan seperti julat dengan hanya hujah berhenti dengan menggunakan tiga jenis pemula: Kelas, Generator Fungsi (Hasil) dan Generator Expressions

Kelas

Cara lama membuat penyesuai adalah melalui kelas yang jelas. Untuk objek menjadi iterator ia mesti melaksanakan __iter __ () yang mengembalikan dirinya dan __next __ () yang mengembalikan nilai seterusnya.

my_range kelas:
  _current = -1
  def __init __ (diri, berhenti):
    self._stop = berhenti
  def __iter __ (diri):
    kembali diri
  def __next __ (diri):
    self._current + = 1
    jika self._current> = self._stop:
      tingkatkan StopIteration
    kembali self._current
r = my_range (10)
cetak (senarai (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Itu tidak terlalu sukar, tetapi malangnya, kita perlu mengesan pembolehubah antara panggilan seterusnya (). Secara peribadi, saya tidak suka boilerplate atau menukar bagaimana saya berfikir tentang gelung kerana ia bukan penyelesaian drop-in, jadi saya lebih suka penjana

Manfaat utama ialah kita boleh menambah fungsi tambahan yang mengubah suai pembolehubah dalaman seperti _stop atau membuat penyaharu baru.

Penyusun kelas mempunyai kelemahan memerlukan boilerplate, namun, mereka boleh mempunyai fungsi tambahan yang memodifikasi keadaan.

Penjana

PEP 255 memperkenalkan "penjana mudah" menggunakan kata kunci hasil.

Hari ini, penjana adalah pemula yang lebih mudah dibuat daripada rakan kelasnya.

Fungsi Generator

Fungsi Generator adalah apa yang akhirnya dibincangkan dalam PEP itu dan jenis penyesaran kegemaran saya, jadi mari bermula dengan itu.

def my_range (berhenti):
  indeks = 0
  sementara indeks 
r = my_range (10)
cetak (senarai (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Adakah anda melihat sejauh mana 4 baris kod yang indah itu? Ia agak jauh lebih pendek daripada pelaksanaan senarai kami untuk mengatasinya!

Generator berfungsi melintang dengan kurang boilerplate daripada kelas dengan aliran logik biasa.

Fungsi Generator secara automagically "jeda" pelaksanaan dan mengembalikan nilai yang ditentukan dengan setiap panggilan seterusnya (). Ini bermakna tiada kod yang dijalankan sehingga panggilan seterusnya () seterusnya.

Ini bermakna aliran seperti ini:

  1. seterusnya () dipanggil,
  2. Kod dilaksanakan sehingga penyata hasil seterusnya.
  3. Nilai di sebelah kanan hasil dikembalikan.
  4. Pelaksanaan dijeda.
  5. 1-5 berulang untuk setiap panggilan berikutnya () sehingga baris terakhir kod dipukul.
  6. StopIteration dibangkitkan.

Fungsi Generator juga membolehkan anda untuk menggunakan hasil dari kata kunci yang akan datang () berikutnya masa depan ke panggilan lain yang boleh dialihkan sehingga kata iterable telah habis.

def produce_range ():
  hasil dari my_range (10)
cetak (senarai (yielded_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Itu bukan contoh yang sangat kompleks. Tetapi anda juga boleh melakukan secara rekursif!

def my_range_recursive (stop, current = 0):
  jika semasa> = berhenti:
    kembali
  hasil semasa
  hasil dari my_range_recursive (stop, current + 1)
r = my_range_recursive (10)
cetak (senarai (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ungkapan Generator

Ungkapan generator membolehkan kita untuk membuat iterators sebagai satu-liner dan baik ketika kita tidak perlu memberikan fungsi luaran. Malangnya, kami tidak boleh membuat my_range lain menggunakan ungkapan, tetapi kami boleh bekerja pada peranti seperti fungsi my_range terakhir kami.

my_doubled_range_10 = (x * 2 untuk x dalam my_range (10))
cetak (senarai (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Perkara yang keren tentang perkara ini adalah bahawa ia melakukan perkara-perkara berikut:

  1. Senarai ini meminta my_doubled_range_10 untuk nilai seterusnya.
  2. my_doubled_range_10 meminta my_range untuk nilai seterusnya.
  3. my_doubled_range_10 mengembalikan nilai my_range didarab dengan 2.
  4. Senarai ini menambahkan nilai itu kepada dirinya sendiri.
  5. 1-5 ulangi sehingga my_doubled_range_10 menimbulkan StopIteration yang berlaku apabila my_range tidak.
  6. Senarai dikembalikan mengandungi setiap nilai yang dikembalikan oleh my_doubled_range.

Kami juga boleh melakukan penapisan menggunakan ungkapan penjana!

my_even_range_10 = (x untuk x dalam my_range (10) jika x% 2 == 0)
cetak (senarai (my_even_range_10)) # [0, 2, 4, 6, 8]

Ini sangat mirip dengan sebelumnya kecuali my_even_range_10 hanya mengembalikan nilai-nilai yang sepadan dengan keadaan yang diberikan, jadi hanya nilai-nilai di antara julat [0, 10].

Sepanjang semua ini, kami hanya membuat senarai kerana kami menyuruhnya.

Manfaat

Sumber

Kerana penjana adalah pemula, iterator boleh dilanjutkan, dan iterator malas mengembalikan nilai. Ini bermakna bahawa menggunakan pengetahuan ini kita boleh membuat objek yang hanya akan memberi kita objek apabila kita meminta mereka dan bagaimanapun banyak yang kita suka.

Ini bermakna kita boleh lulus penjana menjadi fungsi yang mengurangkan satu sama lain.

cetak (jumlah (my_range (10)) # #

Mengira jumlah dengan cara ini mengelakkan mencipta senarai apabila semua yang kita lakukan adalah menambahkannya bersama-sama dan kemudian membuang.

Kita boleh menulis semula contoh pertama untuk menjadi lebih baik dengan menggunakan fungsi penjana!

s = 'baacabcaab'
p = 'a'
def find_char (string, character):
  untuk indeks, str_char dalam menghitung (rentetan):
    jika str_char == aksara:
      indeks hasil
cetak (senarai (find_char (s, p)) # # 1, 2, 4, 7, 8]

Sekarang dengan serta-merta mungkin ada manfaat yang jelas, tapi mari kita pergi ke soalan pertama saya: "bagaimana jika kita hanya mahu hasil pertama; akankah kita perlu membuat fungsi yang sepenuhnya baru? "

Dengan fungsi penjana kita tidak perlu menuliskan banyak logik.
cetak (seterusnya (find_char (s, p)) # # 1

Sekarang kita boleh mendapatkan nilai pertama senarai yang diberikan oleh penyelesaian asal kami, tetapi dengan cara ini kita hanya mendapat perlawanan pertama dan berhenti melelongkan senarai. Penjana akan dibuang dan tiada lagi yang dibuat; memori simpanan secara besar-besaran.

Kesimpulannya

Sekiranya anda mencipta fungsi, nilai terkumpul dalam senarai seperti ini.

def foo (bar):
  nilai = []
  untuk x di bar:
    # beberapa logik
    values.append (x)
  nilai pulangan

Pertimbangkan untuk menjadikan ia mengembalikan pemula dengan kelas, fungsi penjana, atau ungkapan penjana seperti:

def foo (bar):
  untuk x di bar:
    # beberapa logik
    hasil x

Sumber dan Sumber

PEPs

  • Penjana
  • Generasi Expression PEP
  • Hasil Dari PEP

Artikel dan Thread

  • Pemula
  • Iterable vs Iterator
  • Dokumentasi Penjana
  • Iterators vs Generators
  • Generator Expression vs Function
  • Penjana Recrusive

Definisi

  • Iterable
  • Iterator
  • Penjana
  • Generator Generator
  • Ungkapan Generator

Originally diterbitkan di https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.