PEP 572 (Ekspresi Penugasan dalam python 3.8)

Halo Habr. Kali ini kita akan melihat PEP 572, yang berbicara tentang ekspresi penugasan. Jika Anda masih ragu dengan operator ": =" atau tidak sepenuhnya memahami aturan penggunaannya, maka artikel ini cocok untuk Anda. Di sini Anda akan menemukan banyak contoh dan jawaban untuk pertanyaan: "Mengapa demikian?" Artikel ini ternyata selengkap mungkin, dan jika Anda punya sedikit waktu, maka lihatlah bagian yang saya tulis. Pada awalnya, "tesis" utama dikumpulkan untuk pekerjaan yang nyaman dengan ekspresi penugasan. Maafkan saya sebelumnya jika Anda menemukan kesalahan (tulis tentang itu kepada saya, saya akan memperbaikinya). Ayo mulai:

PEP 572 - Ekspresi Penugasan

Semangat572
Judul:Ekspresi Penugasan
Penulis:Chris Angelico <rosuav at gmail.com>, Tim Peters <tim.peters di gmail.com>, Guido van Rossum <guido di python.org>
Diskusi:doc-sig di python.org
Status:Diterima
Sebuah tipe:Standar
Dibuat:28-Feb-2018
Versi python:3.8
Posting cerita:28-Feb-2018, 02-Mar-2018, 23-Mar-2018, 04-Apr-2018, 17-Apr-2018, 25-Apr-2018, 09-Jul-2018, 05-Agu-2019
Izin untuk mengadopsi standar:mail.python.org/pipermail/python-dev/2018-Juli/154601.html (dengan VPN untuk waktu yang lama, tetapi dimuat)
Kandungan


anotasi


Konvensi ini akan berbicara tentang kemungkinan penugasan di dalam ekspresi, menggunakan notasi baru NAME: = expr.

Sebagai bagian dari inovasi, prosedur untuk menghitung generator kamus (pemahaman kamus) telah diperbarui. Ini memastikan bahwa ekspresi kunci dievaluasi sebelum ekspresi nilai (ini memungkinkan Anda untuk mengikat kunci ke variabel dan kemudian menggunakan kembali variabel yang dibuat dalam perhitungan nilai yang sesuai dengan kunci).

Selama diskusi tentang PEP ini, operator ini secara tidak resmi dikenal sebagai operator walrus. Nama formal konstruk adalah "Ekspresi Penugasan" (menurut PEP: Penugasan Ekspresi Penugasan), tetapi dapat disebut sebagai "Ekspresi Bernama". Sebagai contoh, implementasi referensi di CPython menggunakan nama ini.

Pembenaran


Penamaan adalah bagian penting dari pemrograman yang memungkinkan Anda untuk menggunakan nama "deskriptif" alih-alih ekspresi yang lebih panjang, dan juga membuatnya mudah untuk menggunakan kembali nilai. Saat ini, ini hanya dapat dilakukan dalam bentuk instruksi, yang membuat operasi ini tidak tersedia saat membuat daftar (pemahaman daftar), serta dalam ekspresi lainnya.

Selain itu, penamaan bagian dari ekspresi besar dapat membantu dengan debugging interaktif dengan menyediakan alat untuk menampilkan prompt dan hasil antara. Tanpa kemampuan untuk menangkap hasil dari ekspresi bersarang, Anda perlu mengubah kode sumber, tetapi menggunakan ekspresi penetapan Anda hanya perlu memasukkan beberapa "penanda" dari bentuk "nama: = ekspresi". Ini menghilangkan refactoring yang tidak perlu, dan karena itu mengurangi kemungkinan perubahan kode yang tidak disengaja selama debugging (penyebab umum Heisenbugs adalah kesalahan yang mengubah sifat-sifat kode selama debugging dan mungkin secara tak terduga muncul dalam produksi]), dan kode ini akan lebih dimengerti oleh yang lain ke programmer.

Pentingnya Kode Nyata


Selama pengembangan PEP ini, banyak orang (baik pendukung dan kritik) terlalu fokus pada contoh mainan di satu sisi, dan contoh-contoh yang terlalu rumit di sisi lain.

Bahaya contoh mainan ada dua: mereka sering terlalu abstrak untuk membuat seseorang berkata "oh, ini tidak bisa ditolak", dan mereka juga mudah ditolak dengan kata-kata "Saya tidak akan pernah menulis itu." Bahaya dari contoh yang terlalu rumit adalah bahwa mereka menyediakan lingkungan yang nyaman bagi kritikus yang menyarankan bahwa fungsi ini dihapus ("Ini terlalu membingungkan," kata orang-orang semacam itu).

Namun, ada baiknya digunakan untuk contoh-contoh seperti: mereka membantu memperjelas semantik yang dimaksud. Oleh karena itu, kami akan memberikan beberapa di antaranya di bawah ini. Namun, untuk meyakinkan , contoh harus didasarkan padakode nyata yang ditulis tanpa memikirkan PEP ini. Yaitu, kode yang merupakan bagian dari aplikasi yang sangat berguna (tidak ada perbedaan: apakah itu besar atau kecil). Tim Peters banyak membantu kami dengan melihat repositori pribadinya dan memilih contoh kode yang ditulisnya, yang (menurut pendapatnya) akan lebih dimengerti jika ditulis ulang (tanpa fanatisme) menggunakan ungkapan penugasan. Kesimpulannya adalah ini: perubahan saat ini akan membawa perbaikan sederhana tapi jelas dalam beberapa bit dari kodenya.

Contoh lain dari kode nyata adalah pengamatan tidak langsung tentang bagaimana programmer menilai kekompakan. Guido van Rossum memeriksa basis kode Dropbox dan menemukan beberapa bukti bahwa programmer lebih suka menulis lebih sedikit baris kode daripada menggunakan beberapa ekspresi kecil.

Contoh kasus: Guido menemukan beberapa poin ilustratif ketika seorang programmer mengulangi subekspresi (dengan demikian memperlambat program), tetapi menyimpan satu baris kode tambahan. Misalnya, alih-alih menulis:

match = re.match(data)
group = match.group(1) if match else None

Programmer menyukai opsi ini:

group = re.match(data).group(1) if re.match(data) else None

Berikut adalah contoh lain yang menunjukkan bahwa programmer kadang-kadang bersedia melakukan lebih banyak pekerjaan untuk mempertahankan "tingkat sebelumnya" dari lekukan:

match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
    result = match1.group(1)
elif match2:
    result = match2.group(2)
else:
    result = None

Kode ini menghitung pattern2, bahkan jika pattern1 sudah cocok (dalam hal ini, sub-kondisi kedua tidak akan pernah terpenuhi). Oleh karena itu, solusi berikut ini lebih efektif, tetapi kurang menarik:

match1 = pattern1.match(data)
if match1:
    result = match1.group(1)
else:
    match2 = pattern2.match(data)
    if match2:
        result = match2.group(2)
    else:
        result = None

Sintaks dan semantik


Dalam kebanyakan kasus di mana Python menggunakan ekspresi sewenang-wenang, Anda sekarang dapat menggunakan ekspresi penetapan. Mereka memiliki bentuk NAME: = expr, di mana expr adalah ekspresi Python apa pun yang valid, kecuali untuk tuple yang tidak di-un-petak, dan NAME adalah pengenalnya. Nilai ekspresi seperti itu bertepatan dengan aslinya, tetapi efek tambahan adalah penugasan nilai ke objek target:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
   process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]

Kasus Luar Biasa


Ada beberapa tempat di mana ekspresi penugasan tidak diizinkan untuk menghindari ambiguitas atau kebingungan di antara pengguna:

  • Ekspresi penugasan yang tidak dimasukkan dalam tanda kurung dilarang di tingkat "atas":

    y := f(x)  # 
    (y := f(x))  # ,   

    Aturan ini akan memudahkan programmer untuk memilih antara operator penugasan dan ekspresi penugasan - tidak akan ada situasi sintaksis di mana kedua opsi tersebut setara.
  • . :

    y0 = y1 := f(x)  # 
    y0 = (y1 := f(x))  # ,   

    . :

    foo(x = y := f(x))  # 
    foo(x=(y := f(x)))  # ,     

    , .
  • . :

    def foo(answer = p := 42):  # 
        ...
    def foo(answer=(p := 42)):  # Valid, though not great style
        ...

    , (. , «» ).
  • , . :

    def foo(answer: p := 42 = 5):  # 
        ...
    def foo(answer: (p := 42) = 5):  # ,  
        ...

    : , "=" ":=" .
  • -. :

    (lambda: x := 1) # 
    lambda: (x := 1) # ,  
    (x := lambda: 1) # 
    lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid

    - , ":=". . , , () , .
  • f- . :

    >>> f'{(x:=10)}'  # ,  
    '10'
    >>> x = 10
    >>> f'{x:=10}'    # ,  ,  '=10'
    '        10'

    , , f-, . f- ":" . , f- . , .


Ekspresi penugasan tidak memperkenalkan cakupan baru. Dalam kebanyakan kasus, ruang lingkup di mana variabel akan dibuat tidak memerlukan penjelasan: itu akan menjadi saat ini. Jika variabel menggunakan kata kunci nonlokal atau global sebelumnya, maka ekspresi penugasan akan mempertimbangkan hal ini. Hanya lambda (menjadi definisi anonim dari suatu fungsi) dianggap sebagai ruang lingkup terpisah untuk tujuan ini.

Ada satu kasus khusus: ekspresi penugasan yang terjadi dalam generator daftar, set, kamus, atau dalam "ekspresi generator" sendiri (selanjutnya secara kolektif disebut sebagai "generator" (pemahaman)) mengikat variabel ke ruang lingkup yang dikandung generator, mengamati pengubah globab atau nonglobal, jika ada.

Alasan untuk kasus khusus ini ada dua. Pertama, ini memungkinkan kita untuk dengan mudah menangkap "anggota" dalam ekspresi any () dan all (), misalnya:

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

if all((nonblank := line).strip() == '' for line in lines):
    print("All lines are blank")
else:
    print("First non-blank line:", nonblank)

Kedua, ini menyediakan cara yang ringkas untuk memperbarui variabel dari generator, misalnya:

# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)

Namun, nama variabel dari ekspresi penugasan tidak bisa cocok dengan nama yang sudah digunakan dalam generator oleh for loop untuk beralih. Nama-nama terakhir adalah lokal untuk generator di mana mereka muncul. Akan menjadi tidak konsisten jika ekspresi penugasan juga merujuk pada ruang lingkup di dalam generator.

Misalnya, [i: = i +1 untuk i dalam rentang (5)] tidak valid: untuk loop menentukan bahwa saya lokal untuk generator, tetapi bagian "i: = i + 1" menegaskan bahwa saya adalah variabel dari eksternal cakupan Untuk alasan yang sama, contoh-contoh berikut tidak akan berfungsi:


[[(j := j) for i in range(5)] for j in range(5)] # 
[i := 0 for i, j in stuff]                       # 
[i+1 for i in (i := stuff)]                      # 

Meskipun secara teknis dimungkinkan untuk menetapkan semantik yang konsisten untuk kasus-kasus seperti itu, sulit untuk menentukan apakah cara kami memahami semantik ini akan bekerja dalam kode asli Anda. Itulah sebabnya implementasi referensi memastikan bahwa kasus seperti itu meningkatkan SyntaxError, daripada dieksekusi dengan perilaku yang tidak ditentukan, tergantung pada implementasi perangkat keras tertentu. Pembatasan ini berlaku bahkan jika ekspresi penugasan tidak pernah dieksekusi:

[False and (i := 0) for i, j in stuff]     # 
[i for i, j in stuff if True or (j := 1)]  # 

# [.  . - ""   
# ,       
# ,    ,   ]

Untuk badan generator (bagian sebelum kata kunci pertama "untuk") dan ekspresi filter (bagian setelah "jika" dan sebelum setiap "untuk" yang bersarang) pembatasan ini berlaku khusus untuk nama variabel yang secara bersamaan digunakan sebagai variabel berulang. Seperti yang telah kami katakan, ekspresi Lambda memperkenalkan ruang lingkup fungsi eksplisit baru dan oleh karena itu dapat digunakan dalam ekspresi generator tanpa batasan tambahan. [sekitar lagi, kecuali dalam kasus seperti itu: [i untuk i dalam kisaran (2, (lambda: (s: = 2) ()))]]

Karena keterbatasan desain dalam implementasi referensi (penganalisa tabel simbol tidak dapat mengenali apakah nama-nama dari bagian kiri generator digunakan di bagian yang tersisa di mana ekspresi iterable berada), oleh karena itu ekspresi penugasan sepenuhnya dilarang sebagai bagian dari iterable (di bagian setelah setiap "in" dan sebelum kata kunci berikutnya "jika" atau "untuk"). Artinya, semua kasus ini tidak dapat diterima:

[i+1 for i in (j := stuff)]                    # 
[i+1 for i in range(2) for j in (k := stuff)]  # 
[i+1 for i in [j for j in (k := stuff)]]       # 
[i+1 for i in (lambda: (j := stuff))()]        # 

Pengecualian lain terjadi ketika ekspresi penugasan digunakan dalam generator yang berada dalam ruang lingkup kelas. Jika, ketika menggunakan aturan di atas, kelas harus dibuat yang diukur dalam ruang lingkup, maka ekspresi penugasan seperti itu tidak valid dan akan menghasilkan SyntaxError:

class Example:
    [(j := i) for i in range(5)]  # 

(Alasan pengecualian terakhir adalah ruang lingkup implisit dari fungsi yang dibuat oleh generator - saat ini tidak ada mekanisme runtime untuk fungsi untuk merujuk ke variabel yang terletak di lingkup kelas, dan kami tidak ingin menambahkan mekanisme seperti itu. Jika masalah ini pernah dipecahkan, maka kasus khusus ini (mungkin) akan dihapus dari spesifikasi ekspresi penugasan. Harap dicatat bahwa masalah ini akan terjadi bahkan jika Anda membuat variabel lebih awal di dalam ruang kelas dan mencoba mengubahnya dengan ekspresi penugasan dari generator.)

Lihat Lampiran B untuk contoh bagaimana ekspresi penugasan yang ditemukan di generator dikonversi ke kode yang setara.

Prioritas Relatif: =


Operator: = dikelompokkan lebih kuat daripada koma di semua posisi sintaksis jika memungkinkan, tetapi lebih lemah dari semua operator lain, termasuk atau, dan, tidak, dan ekspresi kondisional (A jika C selain B). Sebagai berikut dari bagian "Kasus Luar Biasa" di atas, ekspresi penugasan tidak pernah bekerja pada "level" yang sama dengan penugasan klasik =. Jika diperlukan urutan operasi yang berbeda, gunakan tanda kurung.

Operator: = dapat digunakan secara langsung saat memanggil argumen posisi dari suatu fungsi. Namun, ini tidak akan berfungsi secara langsung dalam argumen. Beberapa contoh mengklarifikasi apa yang diizinkan secara teknis dan apa yang tidak mungkin:

x := 0 # 

(x := 0) #  

x = y := 0 # 

x = (y := 0) #  

len(lines := f.readlines()) # 

foo(x := 3, cat='vector') # 

foo(cat=category := 'vector') # 

foo(cat=(category := 'vector')) #  

Sebagian besar contoh "valid" di atas tidak direkomendasikan untuk digunakan dalam praktik, karena orang yang dengan cepat memindai kode sumber Anda mungkin tidak memahami artinya dengan benar. Tetapi dalam kasus sederhana ini diizinkan:

# Valid
if any(len(longline := line) >= 100 for line in lines):
    print("Extremely long line:", longline)

PEP ini merekomendasikan agar Anda selalu selalu memberikan ruang: =, mirip dengan rekomendasi PEP 8 untuk = untuk tugas klasik. (Perbedaan dari rekomendasi terakhir adalah bahwa ia melarang spasi di sekitar =, yang digunakan untuk meneruskan argumen kunci ke fungsi.)

Ubah urutan perhitungan.


Untuk memiliki semantik yang terdefinisi dengan baik, perjanjian ini mensyaratkan bahwa prosedur evaluasi harus didefinisikan dengan jelas. Secara teknis, ini bukan persyaratan baru. Python sudah memiliki aturan bahwa subekspresi biasanya dievaluasi dari kiri ke kanan. Namun, ekspresi penugasan membuat "efek samping" ini lebih terlihat, dan kami mengusulkan satu perubahan dalam urutan perhitungan saat ini:

  • Dalam kamus generator {X: Y untuk ...}, Y saat ini dievaluasi sebelum X. Kami menyarankan untuk mengubah ini sehingga X dihitung sebelum Y. (Dalam diktik klasik seperti {X: Y}, serta dalam dikt ((X, Y) untuk ...) ini telah diterapkan. Oleh karena itu, pembuat kamus harus mematuhi mekanisme ini)


Perbedaan antara ekspresi penugasan dan instruksi penugasan.


Yang paling penting, ": =" adalah ekspresi , yang artinya dapat digunakan dalam kasus di mana instruksi tidak valid, termasuk fungsi lambda dan generator. Sebaliknya, ekspresi penugasan tidak mendukung fungsionalitas diperluas yang dapat digunakan dalam instruksi penugasan:

  • Tugas Cascading tidak didukung secara langsung

    x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
  • "Target" yang terpisah, kecuali untuk nama variabel sederhana NAME, tidak didukung:

    # No equivalent
    a[i] = x
    self.rest = []
  • Fungsi dan prioritas "di sekitar" koma berbeda:

    x = 1, 2  # Sets x to (1, 2)
    (x := 1, 2)  # Sets x to 1
  • Nilai pembongkaran dan pengemasan tidak memiliki kesetaraan "murni" atau tidak didukung sama sekali

    # Equivalent needs extra parentheses
    loc = x, y  # Use (loc := (x, y))
    info = name, phone, *rest  # Use (info := (name, phone, *rest))
    
    # No equivalent
    px, py, pz = position
    name, phone, email, *other_info = contact
  • Anotasi jenis sebaris tidak didukung:

    # Closest equivalent is "p: Optional[int]" as a separate declaration
    p: Optional[int] = None
  • Tidak ada bentuk operasi yang dipersingkat:

    total += tax  # Equivalent: (total := total + tax)

Perubahan spesifikasi selama implementasi


Perubahan berikut dibuat berdasarkan pengalaman kami dan analisis tambahan setelah penulisan pertama PEP ini dan sebelum rilis Python 3.8:

  • Untuk memastikan konsistensi dengan pengecualian serupa lainnya, dan untuk tidak memperkenalkan nama baru yang mungkin tidak nyaman bagi pengguna akhir, subkelas yang awalnya diusulkan dari TargetScopeError untuk SyntaxError telah dihapus dan dikurangi menjadi SyntaxError biasa. [3]
  • Karena keterbatasan dalam mem-parsing tabel karakter CPython, implementasi referensi ekspresi penugasan memunculkan SyntaxError untuk semua penggunaan di dalam iterator. Sebelumnya, pengecualian ini hanya terjadi jika nama variabel yang dibuat bersamaan dengan yang sudah digunakan dalam ekspresi iteratif. Ini dapat direvisi jika ada contoh yang cukup meyakinkan, tetapi kompleksitas tambahan tampaknya tidak sesuai untuk kasus penggunaan yang murni "hipotetis".

Contohnya


Contoh Pustaka Standar Python


site.py


env_base hanya digunakan dalam suatu kondisi, sehingga penugasan dapat ditempatkan di if, sebagai "header" dari blok logis.

  • Kode Saat Ini:
    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base
  • Kode yang ditingkatkan:
    if env_base := os.environ.get("PYTHONUSERBASE", None):
        return env_base

_pydecimal.py


Anda dapat menghindari ifs bersarang, sehingga menghilangkan satu tingkat lekukan.

  • Kode Saat Ini:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • Kode yang ditingkatkan:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

copy.py


Kode terlihat lebih klasik, dan juga menghindari beberapa pernyataan bersyarat bersarang. (Lihat Lampiran A untuk mempelajari lebih lanjut tentang asal dari contoh ini.)

  • Kode Saat Ini:
    reductor = dispatch_table.get(cls)
    if reductor:
        rv = reductor(x)
    else:
        reductor = getattr(x, "__reduce_ex__", None)
        if reductor:
            rv = reductor(4)
        else:
            reductor = getattr(x, "__reduce__", None)
            if reductor:
                rv = reductor()
            else:
                raise Error(
                    "un(deep)copyable object of type %s" % cls)
  • Kode yang ditingkatkan:

    if reductor := dispatch_table.get(cls):
        rv = reductor(x)
    elif reductor := getattr(x, "__reduce_ex__", None):
        rv = reductor(4)
    elif reductor := getattr(x, "__reduce__", None):
        rv = reductor()
    else:
        raise Error("un(deep)copyable object of type %s" % cls)

datetime.py


tz hanya digunakan untuk s + = tz. Memindahkannya ke dalam jika membantu menunjukkan area penggunaannya yang logis.

  • Kode Saat Ini:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    tz = self._tzstr()
    if tz:
        s += tz
    return s
  • Kode yang ditingkatkan:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    if tz := self._tzstr():
        s += tz
    return s

sysconfig.py


Memanggil fp.readline () sebagai "kondisi" di loop while (serta memanggil metode .match ()) dalam kondisi if membuat kode lebih kompak tanpa menyulitkan pemahamannya.

  • Kode Saat Ini:

    while True:
        line = fp.readline()
        if not line:
            break
        m = define_rx.match(line)
        if m:
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        else:
            m = undef_rx.match(line)
            if m:
                vars[m.group(1)] = 0
  • Kode yang ditingkatkan:

    while line := fp.readline():
        if m := define_rx.match(line):
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        elif m := undef_rx.match(line):
            vars[m.group(1)] = 0

Sederhanakan Generator Daftar


Sekarang generator daftar dapat disaring secara efektif dengan "menangkap" kondisinya:

results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]

Setelah itu, variabel dapat digunakan kembali dalam ekspresi lain:

stuff = [[y := f(x), x/y] for x in range(5)]

Harap dicatat lagi bahwa dalam kedua kasus variabel y berada dalam cakupan yang sama dengan hasil variabel dan barang-barang.

Nilai Tangkap dalam Kondisi


Ekspresi penugasan dapat digunakan secara efektif dalam kondisi pernyataan if atau while:

# Loop-and-a-half
while (command := input("> ")) != "quit":
    print("You entered:", command)

# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
    print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

# Reading socket data until an empty string is returned
while data := sock.recv(8192):
    print("Received data:", data)

Secara khusus, pendekatan ini dapat menghilangkan kebutuhan untuk membuat loop tak terbatas, tugas, dan pengecekan kondisi. Ini juga memungkinkan Anda untuk menggambar paralel yang mulus antara siklus yang menggunakan pemanggilan fungsi sebagai kondisinya, serta siklus yang tidak hanya memeriksa kondisi, tetapi juga menggunakan nilai aktual yang dikembalikan oleh fungsi di masa mendatang.

Garpu


Contoh dari dunia UNIX tingkat rendah: [kira-kira. Fork () adalah panggilan sistem pada sistem operasi mirip Unix yang membuat sub-proses baru relatif terhadap induknya.]

if pid := os.fork():
    # Parent code
else:
    # Child code

Alternatif yang Ditolak


Secara umum, saran serupa cukup umum di komunitas python. Di bawah ini adalah sejumlah sintaks alternatif untuk ekspresi penugasan yang terlalu spesifik untuk dipahami dan telah ditolak untuk mendukung hal di atas.

Mengubah ruang lingkup untuk generator


Dalam versi PEP sebelumnya, diusulkan untuk membuat perubahan halus pada aturan ruang lingkup untuk generator agar lebih cocok untuk digunakan dalam ruang lingkup kelas. Namun, proposal ini akan mengarah ke ketidakcocokan mundur dan karenanya ditolak. Oleh karena itu, PEP ini hanya dapat sepenuhnya fokus pada ekspresi penugasan.

Ejaan alternatif


Secara umum, ekspresi penugasan yang diusulkan memiliki semantik yang sama, tetapi ditulis berbeda.

  1. EXPR sebagai NAME:

    stuff = [[f(x) as y, x/y] for x in range(5)]

    EXPR as NAME import, except with, (, ).

    ( , «with EXPR as VAR» EXPR VAR, EXPR.__enter__() VAR.)

    , ":=" :
    • , if f(x) as y , ​​ if f x blah-blah, if f(x) and y.
    • , as , , :
      • import foo as bar
      • except Exc as var
      • with ctxmgr() as var

      , as if while , as « » .
    • «»
      • NAME = EXPR
      • if NAME := EXPR

      .
  2. EXPR -> NAME

    stuff = [[f(x) -> y, x/y] for x in range(5)]

    , R Haskell, . ( , - y < — f (x) Python, - .) «as» , import, except with, . Python ( ), ":=" ( Algol-58) .
  3. «»

    stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as"
    stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="

    . Python, , .
  4. where: :

    value = x**2 + 2*x where:
        x = spam(1, 4, 7, q)

    ( , «»). , «» ( with:). . PEP 3150, ( given: ).
  5. TARGET from EXPR:

    stuff = [[y from f(x), x/y] for x in range(5)]

    Sintaks ini lebih kecil kemungkinannya untuk konflik dengan orang lain daripada sebagai (kecuali jika Anda menghitung kenaikan Exc dari konstruksi Exc), tetapi sebaliknya dapat dibandingkan dengan mereka. Alih-alih sejajar dengan expr sebagai target: (yang mungkin berguna, tetapi juga bisa membingungkan), opsi ini tidak memiliki paralel dengan apa pun, tetapi secara mengejutkan lebih baik diingat.


Kasus khusus dalam pernyataan bersyarat


Salah satu kasus penggunaan yang paling umum untuk ekspresi penugasan adalah pernyataan if dan while. Alih-alih solusi yang lebih umum, menggunakan sebagai meningkatkan sintaksis dua pernyataan ini dengan menambahkan cara menangkap nilai yang akan dibandingkan:

if re.search(pat, text) as match:
    print("Found:", match.group(0))

Ini berfungsi dengan baik, tetapi HANYA ketika kondisi yang diinginkan didasarkan pada "kebenaran" dari nilai pengembalian. Dengan demikian, metode ini efektif untuk kasus tertentu (memeriksa ekspresi reguler, membaca soket, mengembalikan string kosong ketika eksekusi berakhir), dan sama sekali tidak berguna dalam kasus yang lebih kompleks (misalnya, ketika kondisinya adalah f (x) <0, dan Anda ingin simpan nilai f (x)). Juga, ini tidak masuk akal dalam daftar generator.

Keuntungan : Tidak ada ambiguitas sintaksis. Kekurangan : bahkan jika Anda menggunakannya hanya dalam pernyataan if / while, itu hanya berfungsi dengan baik dalam beberapa kasus.

Kasus khusus dalam generator


Kasus penggunaan umum lainnya untuk ekspresi penugasan adalah generator (daftar / set / dict dan genexps). Seperti di atas, saran dibuat untuk solusi spesifik.

  1. di mana, biarkan, atau diberikan:

    stuff = [(y, x/y) where y = f(x) for x in range(5)]
    stuff = [(y, x/y) let y = f(x) for x in range(5)]
    stuff = [(y, x/y) given y = f(x) for x in range(5)]

    Metode ini menghasilkan subekspresi antara for for dan ekspresi utama. Itu juga memperkenalkan kata kunci bahasa tambahan, yang dapat menciptakan konflik. Dari tiga opsi, di mana adalah yang terbersih dan paling mudah dibaca, tetapi potensi konflik masih ada (misalnya, SQLAlchemy dan numpy memiliki metode di mana mereka, serta tkinter.dnd.Icon di perpustakaan standar).
  2. dengan NAME = EXPR:

    stuff = [(y, x/y) with y = f(x) for x in range(5)]

    , , with. . , «» for. C, , . : « «with NAME = EXPR:» , ?»
  3. with EXPR as NAME:

    stuff = [(y, x/y) with f(x) as y for x in range(5)]

    , as, . , for. with

Terlepas dari metode yang dipilih, perbedaan semantik yang tajam akan diperkenalkan antara generator dan versi yang digunakan melalui for for loop. Menjadi tidak mungkin untuk membungkus siklus dalam generator tanpa memproses tahap pembuatan variabel. Satu-satunya kata kunci yang dapat diorientasikan untuk tugas ini adalah kata dengan . Tetapi ini akan memberinya semantik berbeda di bagian kode yang berbeda, yang berarti Anda perlu membuat kata kunci baru, tetapi ini melibatkan banyak biaya.

Prioritas operator lebih rendah


Operator: = memiliki dua prioritas logis. Atau harus memiliki prioritas serendah mungkin (setara dengan operator penugasan). Atau harus diutamakan lebih besar daripada operator perbandingan. Menempatkan prioritasnya antara operator pembanding dan operasi aritmatika (lebih tepatnya: sedikit lebih rendah dari bitwise OR) akan memungkinkan Anda melakukannya tanpa tanda kurung dalam kebanyakan kasus kapan dan saat menggunakan, karena lebih mungkin Anda ingin menyimpan nilai sesuatu sebelum bagaimana perbandingan akan dilakukan di atasnya:

pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
    ...

Segera setelah find () mengembalikan -1, loop berakhir. Jika: = mengikat operan dengan bebas seperti =, maka hasil find () pertama-tama akan "ditangkap" ke dalam operator perbandingan dan biasanya akan mengembalikan Benar atau Salah, yang kurang bermanfaat.

Meskipun perilaku ini praktis dalam banyak situasi, akan lebih sulit untuk dijelaskan. Jadi kita dapat mengatakan bahwa "operator: = berperilaku sama dengan operator penugasan biasa." Artinya, prioritas untuk: = dipilih sedekat mungkin dengan operator = (kecuali bahwa: = memiliki prioritas lebih tinggi daripada koma).

Anda memberi koma di sebelah kanan


Beberapa kritikus berpendapat bahwa ekspresi penugasan harus mengenali tupel tanpa tambahan tanda kurung sehingga kedua entri tersebut setara:

(point := (x, y))
(point := x, y)

(Dalam versi standar saat ini, catatan terakhir akan setara dengan ekspresi ((titik: = x), y).)

Tetapi logis bahwa dalam skenario ini, ketika menggunakan ekspresi penugasan dalam panggilan fungsi, itu juga akan memiliki prioritas lebih rendah daripada koma, jadi kami mendapat akan menjadi kesetaraan membingungkan berikut:

foo (x: = 1, y)
foo (x: = (1, y))

Dan kita mendapatkan satu-satunya jalan keluar yang kurang membingungkan: menjadikan: = operator prioritas yang lebih rendah daripada koma.

Selalu membutuhkan tanda kurung


Itu selalu diusulkan untuk menandai ekspresi penugasan. Ini akan menyelamatkan kita dari banyak ambiguitas. Memang, tanda kurung sering dibutuhkan untuk mengekstraksi nilai yang diinginkan. Tetapi dalam kasus-kasus berikut, kehadiran kurung jelas tampak berlebihan bagi kami:

# Top level in if
if match := pattern.match(line):
    return match.group(1)

# Short call
len(lines := f.readlines())

Keberatan yang sering


Mengapa tidak mengubah pernyataan penugasan menjadi ekspresi?


Bahasa C dan yang serupa mendefinisikan operator = sebagai ekspresi, bukan instruksi, seperti yang dilakukan oleh Python. Ini memungkinkan penugasan dalam banyak situasi, termasuk tempat di mana variabel dibandingkan. Kesamaan sintaksis antara if (x == y) dan if (x = y) bertentangan dengan semantik mereka yang sangat berbeda. Dengan demikian, PEP ini memperkenalkan operator: = untuk mengklarifikasi perbedaan mereka.

Mengapa repot dengan ekspresi penugasan jika ada instruksi penugasan?


Kedua bentuk ini memiliki fleksibilitas yang berbeda. Operator: = dapat digunakan di dalam ekspresi yang lebih besar, dan di operator = dapat digunakan oleh "keluarga operator mini" dari jenis "+ =". Juga = memungkinkan Anda untuk menetapkan nilai berdasarkan atribut dan indeks.

Mengapa tidak menggunakan cakupan lokal dan mencegah polusi namespace?


Versi sebelumnya dari standar ini mencakup ruang lingkup lokal nyata (terbatas pada satu pernyataan) untuk ekspresi penugasan, mencegah kebocoran nama dan polusi namespace. Terlepas dari kenyataan bahwa dalam beberapa situasi ini memberikan keuntungan tertentu, dalam banyak situasi lain ini menyulitkan tugas, dan manfaatnya tidak dibenarkan oleh keunggulan dari pendekatan yang ada. Ini dilakukan demi kepentingan kesederhanaan bahasa. Anda tidak lagi membutuhkan variabel ini? Ada solusinya: hapus variabel menggunakan kata kunci del atau tambahkan garis bawah yang lebih rendah ke namanya.

(Penulis ingin mengucapkan terima kasih kepada Guido van Rossum dan Christophe Groth atas saran mereka untuk memajukan standar PEP ke arah ini. [2])

Rekomendasi Gaya


Karena ekspresi penugasan kadang-kadang dapat digunakan setara dengan operator penugasan, timbul pertanyaan, apa yang masih disukai? .. Sesuai dengan konvensi gaya lainnya (seperti PEP 8), ada dua rekomendasi:

  1. Jika Anda dapat menggunakan kedua opsi penugasan, maka berikan preferensi kepada operator. Mereka paling jelas menyatakan niat Anda.
  2. Jika penggunaan ekspresi penugasan mengarah ke ambiguitas dalam urutan eksekusi, maka tulis ulang kode menggunakan operator klasik.

Terima kasih


Para penulis standar ini ingin mengucapkan terima kasih kepada Nick Coghlan dan Steven D'Aprano atas kontribusi mereka yang signifikan terhadap PEP ini, serta anggota-anggota Pendampingan Python Core atas bantuan mereka dalam mengimplementasikan ini.

Lampiran A: Kesimpulan Tim Peters


Berikut adalah esai pendek yang ditulis Tim Peters tentang topik ini.

Saya tidak suka kode "bingung", dan juga tidak suka menempatkan logika yang tidak berhubungan secara konsep pada satu baris. Jadi, misalnya, alih-alih:

i = j = count = nerrors = 0

Saya lebih suka menulis:

i = j = 0
count = 0
nerrors = 0

Oleh karena itu, saya pikir saya akan menemukan beberapa tempat di mana saya ingin menggunakan ekspresi penugasan. Saya bahkan tidak ingin berbicara tentang penggunaannya dalam ekspresi yang sudah memanjang hingga setengah layar. Dalam kasus lain, perilaku seperti:

mylast = mylast[1]
yield mylast[0]

Secara signifikan lebih baik daripada ini:

yield (mylast := mylast[1])[0]

Kedua kode ini memiliki konsep yang sangat berbeda dan menggabungkannya akan menjadi gila. Dalam kasus lain, menggabungkan ekspresi logis mempersulit pemahaman kode. Misalnya, menulis ulang:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

Dalam bentuk yang lebih pendek, kita telah kehilangan "logika." Anda perlu memahami cara kerja kode ini. Otak saya tidak ingin melakukan ini:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

Tetapi kasus seperti itu jarang terjadi. Tugas mempertahankan hasilnya sangat umum, dan "jarang lebih baik daripada padat" tidak berarti bahwa "hampir kosong lebih baik daripada jarang" [kira-kira. referensi ke Zen Python]. Misalnya, saya memiliki banyak fungsi yang mengembalikan Tidak Ada atau 0 untuk mengatakan "Saya tidak memiliki hal yang berguna, tetapi karena ini sering terjadi, saya tidak ingin mengganggu Anda dengan pengecualian." Bahkan, mekanisme ini juga digunakan dalam ekspresi reguler yang mengembalikan Tidak ada ketika tidak ada kecocokan. Karena itu, dalam contoh ini, banyak kode:

result = solution(xs, n)
if result:
    # use result

Saya menemukan opsi berikut ini lebih mudah dimengerti, dan tentu saja lebih mudah dibaca:

if result := solution(xs, n):
    # use result

Pada awalnya saya tidak terlalu mementingkan hal ini, tetapi konstruksi sesingkat itu muncul begitu sering sehingga segera mulai mengganggu saya sehingga saya tidak dapat menggunakannya. Itu mengejutkan saya! [sekitar rupanya ini ditulis sebelum Python 3.8 secara resmi dirilis.]

Ada kasus-kasus lain di mana ekspresi penugasan benar-benar "menembak". Alih-alih mencari-cari dalam kode saya lagi, Kirill Balunov memberikan contoh yang baik dari fungsi copy () dari perpustakaan copy.py standar:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error("un(shallow)copyable object of type %s" % cls)

Lekukan yang terus meningkat menyesatkan: lagipula, logikanya datar: tes sukses pertama "menang":

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(shallow)copyable object of type %s" % cls)

Penggunaan ekspresi penugasan yang sederhana memungkinkan struktur visual kode untuk menekankan "bidang" logika. Tapi lekukan yang terus meningkat membuatnya tersirat.

Berikut ini adalah contoh kecil lain dari kode saya, yang membuat saya sangat senang karena memungkinkan saya untuk menempatkan logika terkait secara internal pada satu baris dan menghapus tingkat indentasi "buatan" yang mengganggu. Inilah yang saya inginkan dari pernyataan if dan itu membuat membaca lebih mudah. Kode berikut:

diff = x - x_base
if diff:
    g = gcd(diff, n)
    if g > 1:
        return g

Berubah menjadi:

if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
    return g

Jadi, di sebagian besar baris di mana penugasan variabel terjadi, saya tidak akan menggunakan ekspresi penugasan. Tetapi desain ini sangat sering sehingga masih banyak tempat di mana saya akan mengambil kesempatan ini. Dalam kebanyakan kasus terakhir, saya menang sedikit, karena sering muncul. Di sub-bagian yang tersisa, ini menyebabkan peningkatan sedang atau besar. Jadi, saya akan menggunakan ekspresi penugasan jauh lebih sering daripada triple jika, tetapi jauh lebih jarang daripada penugasan augmented [kira-kira. opsi pendek: * =, / =, + =, dll].

Contoh numerik


Saya punya contoh lain yang mengejutkan saya sebelumnya.

Jika semua variabel adalah bilangan bulat positif, dan variabel a lebih besar dari akar ke-x, maka algoritma ini mengembalikan pembulatan "ke bawah" dari akar ke-x (dan kira-kira menggandakan jumlah bit tepat per iterasi):

while a > (d := x // a**(n-1)):
    a = ((n-1)*a + d) // n
return a

Tidak jelas mengapa, tetapi varian dari algoritma seperti itu kurang jelas daripada loop tak terbatas dengan istirahat cabang bersyarat (loop setengah). Juga sulit untuk membuktikan kebenaran dari implementasi ini tanpa bergantung pada pernyataan matematis (“mean aritmatika - geometri ketidaksetaraan rata-rata”) dan tidak mengetahui beberapa hal non-sepele tentang bagaimana fungsi pembulatan bersarang berperilaku ke bawah. Tapi di sini masalahnya sudah ada di matematika, dan bukan di pemrograman.

Dan jika Anda mengetahui semua ini, maka opsi menggunakan ekspresi penugasan dibaca dengan sangat mudah, seperti kalimat sederhana: "Periksa" tebak "saat ini dan jika terlalu besar, kurangi itu" dan kondisi ini memungkinkan Anda untuk segera menyimpan nilai perantara dari kondisi loop. Menurut pendapat saya, bentuk klasik lebih sulit untuk dipahami:

while True:
    d = x // a**(n-1)
    if a <= d:
        break
    a = ((n-1)*a + d) // n
return a

Lampiran B: Juru Bahasa Kode Kasar untuk Generator


Apendiks ini berupaya mengklarifikasi (walaupun tidak menentukan) aturan yang dengannya suatu variabel harus dibuat dalam ekspresi generator. Untuk sejumlah contoh ilustratif, kami menunjukkan kode sumber tempat generator diganti dengan fungsi yang setara dalam kombinasi dengan beberapa "perancah".

Karena [x untuk ...] sama dengan daftar (x untuk ...), contohnya tidak kehilangan sifat umumnya. Dan karena contoh-contoh ini hanya dimaksudkan untuk memperjelas aturan umum, mereka tidak mengaku realistis.

Catatan: generator sekarang diimplementasikan melalui pembuatan fungsi generator bersarang (mirip dengan yang diberikan dalam lampiran ini). Contoh menunjukkan bagian baru, yang menambahkan fungsionalitas yang sesuai untuk bekerja dengan lingkup ekspresi penugasan (ruang lingkup seolah-olah penugasan itu dilakukan dalam blok yang berisi generator paling eksternal). Untuk menyederhanakan "inferensi tipe", contoh ilustratif ini tidak memperhitungkan bahwa penetapan ekspresi adalah opsional (tetapi memperhitungkan ruang lingkup variabel yang dibuat di dalam generator).

Pertama-tama mari kita ingat kode apa yang dihasilkan "di bawah kap" untuk generator tanpa ekspresi penugasan:

  • Kode sumber (EXPR paling sering menggunakan variabel VAR):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • Kode yang dikonversi (jangan khawatir tentang konflik nama):

    def f():
        def genexpr(iterator):
            for VAR in iterator:
                yield EXPR
        a = list(genexpr(iter(ITERABLE)))


Mari menambahkan ekspresi tugas sederhana.

  • Sumber:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Kode yang Dikonversi:

    def f():
        if False:
            TARGET = None  # Dead code to ensure TARGET is a local variable
        def genexpr(iterator):
            nonlocal TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

Sekarang mari kita tambahkan pernyataan TARGET global ke deklarasi fungsi f ().

  • Sumber:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Kode yang Dikonversi:

    def f():
        global TARGET
        def genexpr(iterator):
            global TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

Atau sebaliknya, mari kita tambahkan TARGET nonlokal ke deklarasi fungsi f ().

  • Sumber:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Kode yang Dikonversi:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            def genexpr(iterator):
                nonlocal TARGET
                for VAR in iterator:
                    TARGET = EXPR
                    yield TARGET
            a = list(genexpr(iter(ITERABLE)))

Akhirnya, mari kita masukkan dua generator.

  • Sumber:

    def f():
        a = [[TARGET := i for i in range(3)] for j in range(2)]
        # I.e., a = [[0, 1, 2], [0, 1, 2]]
        print(TARGET)  # prints 2
    
  • Kode yang Dikonversi:

    def f():
        if False:
            TARGET = None
        def outer_genexpr(outer_iterator):
            nonlocal TARGET
            def inner_generator(inner_iterator):
                nonlocal TARGET
                for i in inner_iterator:
                    TARGET = i
                    yield i
            for j in outer_iterator:
                yield list(inner_generator(range(3)))
        a = list(outer_genexpr(range(2)))
        print(TARGET)

Lampiran C: Tidak Ada Perubahan Semantik Lingkup


Perhatikan bahwa dalam Python semantik lingkup tidak berubah. Cakupan fungsi lokal masih ditentukan pada waktu kompilasi dan memiliki batas waktu tidak terbatas saat runtime (penutupan). Contoh:

a = 42
def f():
    # `a` is local to `f`, but remains unbound
    # until the caller executes this genexp:
    yield ((a := i) for i in range(3))
    yield lambda: a + 100
    print("done")
    try:
        print(f"`a` is bound to {a}")
        assert False
    except UnboundLocalError:
        print("`a` is not yet bound")

Kemudian:

>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42

Referensi


  1. Bukti implementasi konsep
  2. Diskusi semantik ekspresi penugasan (VPN ketat tapi dimuat)
  3. Diskusi TargetScopeError di PEP 572 (dimuat serupa dengan yang sebelumnya)

hak cipta


Dokumen ini telah tersedia untuk umum.

Sumber: github.com/python/peps/blob/master/pep-0572.rst

Bagianku


Untuk memulai, mari kita simpulkan:
  • Sehingga orang tidak mencoba untuk menghilangkan dualitas semantik, di banyak tempat "klasik" di mana orang dapat menggunakan "=" dan ": =" ada batasan, oleh karena itu operator :: = harus sering ditutup dalam tanda kurung. Kasus-kasus ini harus ditinjau dalam bagian yang menjelaskan penggunaan dasar.
  • Prioritas ekspresi penugasan sedikit lebih tinggi daripada koma. Karena itu, tupel tidak terbentuk selama penugasan. Itu juga memungkinkan untuk menggunakan: = operator ketika meneruskan argumen ke suatu fungsi.
  • , , , . . lambda , «» .
  • : ,
  • , .
  • / .
  • , .

Pada akhirnya, saya ingin mengatakan bahwa saya menyukai operator baru. Ini memungkinkan Anda untuk menulis kode yang lebih baik dalam kondisi, "filter" daftar, dan juga (akhirnya) menghapus "sama", baris kesepian sebelumnya jika. Jika orang menggunakan ekspresi penugasan untuk tujuan yang dimaksudkan, maka ini akan menjadi alat yang sangat nyaman yang akan meningkatkan keterbacaan dan keindahan kode (Meskipun, ini dapat dikatakan tentang bahasa fungsional apa pun ....)

All Articles