PEP 661 – Nilai sentinel disetujui setelah 5 tahun
(peps.python.org)- PEP 661 mengusulkan objek callable bawaan Python
sentinel()dan C APIPySentinel_New()untuk membuat nilai sentinel yang bisa dibedakan secara terpisah saatNonemerupakan nilai yang valid - Idiom lama
_sentinel = object()memilikirepryang panjang dan tidak jelas dalam signature fungsi, serta dapat menimbulkan masalah pada signature tipe yang jelas, penyalinan, dan pickling - Pemanggilan
sentinel('MISSING')membuat objek unik baru denganreprsingkat, dan jika ingin berbagi sentinel yang sama, harus secara eksplisit digunakan ulang dengan menugaskannya ke variabel sepertiMISSING = sentinel('MISSING') - Sentinel direkomendasikan untuk dibandingkan dengan
isdan dievaluasi sebagai truthy,copy.copy()dancopy.deepcopy()mengembalikan objek yang sama, dan jika bisa diimpor berdasarkan nama dari modul, identitasnya tetap terjaga bahkan setelah pickling - Sistem tipe memungkinkan sentinel itu sendiri digunakan dalam ekspresi tipe seperti
int | MISSING, dan dokumentasi resmi terbaru tersedia di dokumen Python 3.15 untuksentinel")
Latar belakang
- Nilai sentinel (sentinel value), yaitu nilai placeholder yang unik, digunakan untuk nilai default saat argumen fungsi tidak diberikan, nilai kembalian yang menandakan pencarian gagal, atau nilai yang menunjukkan data hilang
- Python memang memiliki nilai khusus
Noneyang biasanya dipakai untuk tujuan ini, tetapi dalam konteks ketikaNonesendiri adalah nilai yang valid, diperlukan nilai sentinel terpisah yang bisa dibedakan dariNone - Pada Mei 2021, milis python-dev membahas cara yang lebih baik untuk mengimplementasikan nilai sentinel yang digunakan di
traceback.print_exception - Implementasi yang ada menggunakan idiom umum
_sentinel = object(), tetapirepr-nya terlalu panjang dan kurang informatif sehingga signature fungsi menjadi sulit dibaca>>> help(traceback.print_exception) Help on function print_exception in module traceback: print_exception(exc, /, value=<object object at 0x000002825DF09650>, tb=<object object at 0x000002825DF09650>, limit=None, file=None, chain=True) - Dalam proses diskusi, teridentifikasi juga masalah lain pada implementasi sentinel yang ada
- Beberapa sentinel tidak memiliki tipe yang unik, sehingga sulit mendefinisikan signature tipe yang jelas untuk fungsi yang menggunakan sentinel sebagai nilai default
- Setelah disalin, instance terpisah dapat terbentuk sehingga perbandingan
isgagal dan perilakunya tidak sesuai harapan - Beberapa idiom umum juga mengalami masalah serupa setelah di-pickle lalu di-unpickle
- Victor Stinner menyediakan daftar nilai sentinel yang digunakan di Python standard library, dan terkonfirmasi bahwa bahkan di dalam standard library sendiri ada beberapa cara implementasi berbeda, dengan banyak implementasi memiliki satu atau lebih masalah di atas
- Pemungutan suara di discuss.python.org tidak menghasilkan kesimpulan yang jelas berdasarkan 39 suara
- 40% memilih “keadaan saat ini sudah baik dan tidak perlu konsistensi”
- Mayoritas memilih satu atau lebih solusi yang distandardisasi
- 37% memilih opsi “menggunakan factory/class/metaclass sentinel khusus baru secara konsisten, dan menyediakannya secara publik di standard library”
- Karena hasilnya terpecah, PEP pun ditulis, dan menghasilkan kesimpulan bahwa implementasi standard library yang sederhana dan baik akan berguna baik di dalam maupun di luar standard library
- Mengganti semua sentinel yang ada di standard library dengan cara ini bukanlah syarat wajib, dan diserahkan pada kebijakan masing-masing maintainer
- Dokumen PEP adalah dokumen historis, dan dokumentasi resmi terbaru tersedia di dokumen Python 3.15 untuk
sentinel")
Kriteria desain
- Saat dibandingkan dengan operator
is, objek sentinel harus selalu identik dengan dirinya sendiri, dan tidak identik dengan objek lain mana pun - Pembuatan objek sentinel harus berupa satu baris kode yang sederhana dan intuitif
- Harus mudah mendefinisikan beberapa nilai sentinel berbeda sebanyak yang diperlukan
- Objek sentinel harus memiliki
repryang singkat dan jelas - Harus bisa menggunakan signature tipe yang jelas untuk sentinel
- Harus tetap berfungsi dengan benar setelah penyalinan, serta memiliki perilaku yang dapat diprediksi saat pickling dan unpickling
- Harus berjalan di CPython 3.x dan PyPy3, dan jika memungkinkan juga di implementasi Python lainnya
- Baik implementasi maupun penggunaannya harus sesederhana dan seintuitif mungkin, dan tidak menjadi satu lagi konsep khusus yang membebani saat mempelajari Python
- Standard library tidak bisa bergantung pada implementasi paket PyPI seperti
sentinelsatausentinel, sehingga dibutuhkan implementasi yang bisa digunakan di dalam standard library sendiri
Spesifikasi sentinel()
- Objek callable bawaan baru
sentinelditambahkan>>> MISSING = sentinel('MISSING') >>> MISSING MISSING sentinel()menerima satu argumen positional-only,name, dannameharus berupastr- Jika nilai non-string diberikan,
TypeErrorakan muncul namedigunakan sebagai nama sentinel danrepr-nya- Objek sentinel memiliki dua atribut publik
__name__: nama sentinel__module__: nama modul tempatsentinel()dipanggil
sentineltidak dapat di-subclass- Setiap pemanggilan
sentinel(name)mengembalikan objek sentinel baru - Jika sentinel yang sama perlu digunakan di beberapa tempat, objek itu harus ditetapkan ke variabel lalu digunakan ulang secara eksplisit, seperti idiom
MISSING = object()yang sudah adaMISSING = sentinel('MISSING') def read_value(default=MISSING): ... - Untuk memeriksa apakah nilai tertentu adalah sentinel, disarankan menggunakan operator
is, seperti padaNone - Perbandingan
==juga bekerja sesuai harapan dengan hanya mengembalikanTruesaat dibandingkan dengan dirinya sendiri - Pemeriksaan identitas seperti
if value is MISSING:biasanya lebih tepat daripada pemeriksaan boolean sepertiif value:atauif not value: - Objek sentinel bersifat truthy, dan evaluasi booleannya menghasilkan
True- Ini sama seperti perilaku default kelas arbitrer dan nilai boolean
Ellipsis - Ini berbeda dari
None, yang bersifat falsy
- Ini sama seperti perilaku default kelas arbitrer dan nilai boolean
- Jika objek sentinel disalin dengan
copy.copy()ataucopy.deepcopy(), objek yang sama akan dikembalikan - Sentinel yang dapat diimpor berdasarkan nama dari modul tempat ia didefinisikan mempertahankan identitasnya setelah pickling dan unpickling melalui mekanisme pickle standar
MISSING = sentinel('MISSING') assert pickle.loads(pickle.dumps(MISSING)) is MISSING sentinel()mencatat modul pemanggil ke atribut__module__saat sentinel dibuat- Pickling mencatat sentinel berdasarkan modul dan nama, dan unpickling mengimpor modul lalu mengambil sentinel berdasarkan nama
- Sentinel yang tidak dapat diimpor berdasarkan modul dan nama, seperti sentinel yang dibuat di scope lokal dan tidak ditetapkan ke nama yang cocok pada global modul atau atribut kelas, tidak dapat di-pickle
reprdari objek sentinel adalahnameyang diberikan kesentinel(), tanpa qualifier modul implisit- Jika
repryang terqualifikasi diperlukan, itu harus disertakan secara eksplisit dalam nama>>> MyClass_NotGiven = sentinel('MyClass.NotGiven') >>> MyClass_NotGiven MyClass.NotGiven - Perbandingan urutan untuk objek sentinel tidak didefinisikan
- Sentinel tidak mendukung weakref
Pengetikan
- Untuk membuat penggunaan sentinel dalam kode Python bertipe lebih jelas dan sederhana, penanganan khusus untuk objek sentinel ditambahkan ke sistem tipe
- Objek sentinel dapat digunakan sebagai nilai yang merepresentasikan dirinya sendiri di dalam ekspresi tipe")
- Ini serupa dengan cara
Noneditangani dalam sistem tipe yang adaMISSING = sentinel('MISSING') def foo(value: int | MISSING = MISSING) -> int: ... - Type checker harus mengenali pembuatan sentinel dalam bentuk
NAME = sentinel('NAME')sebagai pembuatan objek sentinel baru - Jika nama yang diberikan ke
sentinel()tidak cocok dengan nama target assignment, type checker harus menghasilkan error - Sentinel yang didefinisikan dengan sintaks ini dapat digunakan dalam ekspresi tipe")
- Tipe sentinel tersebut merepresentasikan tipe statis penuh") yang hanya memiliki satu anggota, yaitu objek sentinel itu sendiri
- Type checker harus mendukung narrowing union type yang menyertakan sentinel dengan menggunakan operator
isdanis notfrom typing import assert_type MISSING = sentinel('MISSING') def foo(value: int | MISSING) -> None: if value is MISSING: assert_type(value, MISSING) else: assert_type(value, int) - Implementasi runtime harus memiliki metode
__or__dan__ror__untuk mendukung penggunaan dalam ekspresi tipe, dan metode ini mengembalikan objektyping.Union") - Typing Council mendukung bagian terkait tipe dari proposal ini
C API
- Karena sentinel juga dapat berguna di ekstensi C, dua fungsi C API baru diusulkan
PyObject *PySentinel_New(const char *name, const char *module_name)membuat objek sentinel barubool PySentinel_Check(PyObject *obj)memeriksa apakah objek adalah sentinel- Kode C dapat menggunakan operator
==untuk memeriksa apakah suatu objek adalah sentinel tertentu
Kompatibilitas dan keamanan
- Menambahkan nama bawaan baru berarti kode yang saat ini mengasumsikan bare name
sentinelakan memunculkanNameErrortidak lagi melihat hasil yang sama - Ini adalah pertimbangan kompatibilitas yang umum saat menambahkan nama bawaan baru
- Nama lokal, global, atau impor
sentinelyang sudah ada tidak akan terpengaruh - Kode yang sudah menggunakan nama
sentinelmungkin perlu disesuaikan agar menggunakan objek bawaan baru, dan mungkin menerima peringatan baru dari linter yang memperingatkan konflik dengan nama bawaan - Docstring, dokumentasi library, dan bagian “What’s New” dianggap cukup sebagai cara dokumentasi umum untuk fitur bawaan baru
- Proposal ini dianggap tidak memiliki implikasi keamanan
Implementasi referensi dan backport
- Implementasi referensi disediakan sebagai pull request CPython [10]
- Implementasi referensi sebelumnya ada di repositori GitHub terpisah [7]
- Sketsa perilaku yang dimaksud adalah sebagai berikut
class sentinel: """Unique sentinel values.""" __slots__ = ("__name__", "_module_name") def __init_subclass__(cls): raise TypeError("type 'sentinel' is not an acceptable base type") def __init__(self, name, /): if not isinstance(name, str): raise TypeError("sentinel name must be a string") self.__name__ = name self._module_name = sys._getframemodulename(1) @property def __module__(self): return self._module_name def __repr__(self): return self.__name__ def __reduce__(self): return self.__name__ def __copy__(self): return self def __deepcopy__(self, memo): return self def __or__(self, other): return typing.Union[self, other] def __ror__(self, other): return typing.Union[other, self]- Modul typing-extensions memiliki backport, tetapi saat ini belum sepenuhnya cocok dengan perilaku dalam revisi PEP saat ini
Alternatif yang ditolak
-
Menggunakan
NotGiven = object()- Pendekatan ini memiliki semua kelemahan yang dibahas dalam kriteria desain PEP
repr-nya panjang dan tidak jelas, sulit memperjelas tanda tangan tipe, dan dapat menimbulkan masalah terkait penyalinan atau pickling
-
Menambahkan satu nilai sentinel baru tunggal seperti
MISSINGatauSentinel- Jika satu nilai dipakai di banyak tempat untuk banyak tujuan, pada beberapa kasus penggunaan sulit untuk selalu yakin bahwa nilai itu sendiri bukan nilai yang valid
- Nilai sentinel khusus yang berbeda-beda dapat digunakan dengan lebih percaya diri tanpa harus mempertimbangkan edge case potensial
- Nilai sentinel harus dapat memberikan nama dan
repryang bermakna sesuai konteks penggunaannya - Opsi ini sangat tidak populer, hanya dipilih oleh 12% suara dalam pemungutan suara
-
Menggunakan nilai sentinel
Ellipsisyang sudah adaEllipsispada dasarnya tidak dimaksudkan untuk tujuan seperti ini- Penggunaannya memang makin sering untuk mendefinisikan blok kelas atau fungsi kosong alih-alih
pass, tetapi tetap tidak bisa dipakai dengan keyakinan yang sama di semua kasus seperti nilai sentinel khusus yang berbeda
-
Menggunakan
Enumbernilai tunggal- Idiom yang diusulkan adalah sebagai berikut
class NotGivenType(Enum): NotGiven = 'NotGiven' NotGiven = NotGivenType.NotGiven - Terlalu berulang, dan
repr-nya terlalu panjang seperti<NotGivenType.NotGiven: 'NotGiven'> repryang lebih pendek memang bisa didefinisikan, tetapi kode dan pengulangannya menjadi lebih banyak- Ini menjadi opsi yang paling tidak populer karena satu-satunya dari 9 opsi dalam pemungutan suara yang tidak menerima suara sama sekali
-
Dekorator kelas sentinel
- Idiom yang diusulkan adalah sebagai berikut
@sentinel class NotGivenType: pass NotGiven = NotGivenType() - Implementasi dekorator itu sendiri bisa sederhana dan jelas, tetapi idiomnya terlalu bertele-tele, repetitif, dan sulit diingat
- Idiom yang diusulkan adalah sebagai berikut
-
Menggunakan objek kelas
- Karena kelas pada dasarnya adalah singleton, gagasan untuk memakainya sebagai nilai sentinel memang memungkinkan
- Bentuk paling sederhananya adalah sebagai berikut
class NotGiven: pass- Untuk mendapatkan
repryang jelas, diperlukan metaclass atau dekorator kelas
class NotGiven(metaclass=SentinelMeta): pass@Sentinel class NotGiven: pass - Untuk mendapatkan
- Menggunakan kelas dengan cara seperti ini tidak lazim sehingga bisa membingungkan
- Tanpa komentar, sulit memahami maksud kode tersebut, dan muncul perilaku tak terduga yang tidak diinginkan seperti sentinel menjadi dapat dipanggil
-
Mendefinisikan hanya idiom standar yang direkomendasikan tanpa implementasi
- Sebagian besar idiom lama yang umum memiliki kelemahan penting
- Sampai sekarang belum ditemukan idiom yang jelas dan ringkas sambil menghindari kelemahan-kelemahan tersebut
- Dalam pemungutan suara terkait, opsi rekomendasi idiom tidak populer, dan bahkan opsi dengan suara terbanyak pun hanya mencapai 25%
-
Menggunakan modul pustaka standar baru
- Draf awal mengusulkan penambahan kelas
Sentinelke modul barusentinelsatausentinellib - Menambahkan modul baru hanya untuk satu callable publik tidak diperlukan
- Penggunaan modul juga membuat fitur ini lebih tidak praktis dibanding idiom
object()yang sudah ada - Steering Council juga secara khusus merekomendasikan agar ini dijadikan fitur bawaan sehingga semudah
object()untuk digunakan - Nama
sentinelssudah bertabrakan dengan paket PyPI yang aktif digunakan, dan menjadikannya fitur bawaan menghindari masalah penamaan itu
- Draf awal mengusulkan penambahan kelas
-
Menggunakan registri nama sentinel per modul
- Draf awal mengusulkan agar nama sentinel dibuat unik di dalam modul
- Dalam desain ini, pemanggilan berulang
sentinel("MISSING")di modul yang sama akan mengembalikan objek yang sama melalui registri global proses dengan nama modul dan nama sentinel sebagai kunci - Perilaku ini ditolak karena terlalu implisit
- Jika sentinel bersama diperlukan, cukup definisikan satu secara eksplisit seperti
MISSING = object()yang sudah ada lalu gunakan kembali berdasarkan namanya - Dalam scope lokal, mungkin justru diinginkan sentinel baru pada setiap pemanggilan atau pengulangan, sehingga pemanggilan berulang
sentinel(name)harus menghasilkan objek yang berbeda, seperti pemanggilan berulangobject() - Dengan menghapus registri, implementasi dan model mentalnya menjadi lebih sederhana, dan yang tersisa hanyalah aturan bahwa
sentinel(name)membuat objek unik baru denganreprberupaname
-
Penemuan atau penerusan nama modul secara otomatis
- Draf awal mengusulkan argumen opsional
module_nameuntuk mendukung desain berbasis registri - Setelah registri dihapus, argumen publik
module_nametidak lagi diperlukan dalam usulan inti - Implementasinya secara internal tetap mencatat modul pemanggil agar pickling dapat menserialisasi sentinel yang dapat diimpor berdasarkan modul dan nama, mirip
TypeVar - Nama modul internal tidak memengaruhi
reprsentinel - Jika menginginkan
repryang menyertakan nama modul atau nama kelas, itu bisa dimasukkan secara eksplisit dalam satu argumenname, misalnyasentinel("mymodule.MISSING")
- Draf awal mengusulkan argumen opsional
-
Mengizinkan kustomisasi
repr- Ini punya kelebihan karena nilai sentinel yang sudah ada bisa dipindahkan ke pendekatan ini tanpa mengubah
repr - Namun, ini dikecualikan karena dianggap tidak sepadan dengan kompleksitas tambahan yang dibawanya
- Ini punya kelebihan karena nilai sentinel yang sudah ada bisa dipindahkan ke pendekatan ini tanpa mengubah
-
Mengizinkan kustomisasi evaluasi boolean
- Dalam diskusi, dipertimbangkan cara agar sentinel bisa secara eksplisit dibuat truthy, falsy, atau tidak dapat dikonversi ke
bool - Beberapa sentinel pihak ketiga memang menyediakan perilaku falsy sebagai bagian dari API publik mereka
- Sejumlah peserta berpendapat bahwa melempar pengecualian dalam konteks boolean lebih baik untuk memaksa pemeriksaan identitas
- PEP menyederhanakan usulan awal dengan mempertahankan perilaku truthy bawaan objek biasa dan merekomendasikan pemeriksaan identitas
- Perilaku boolean kustom dapat dipertimbangkan nanti jika dinilai layak dengan tambahan API dan kompleksitas pengetikan yang diperlukan
- Dalam diskusi, dipertimbangkan cara agar sentinel bisa secara eksplisit dibuat truthy, falsy, atau tidak dapat dikonversi ke
-
Menggunakan
typing.Literaldalam anotasi tipe- Beberapa orang mengusulkannya dalam diskusi, dan PEP juga awalnya mengadopsi pendekatan ini
- Namun, ini bisa menimbulkan kebingungan karena
Literal["MISSING"]merujuk ke nilai string"MISSING", bukan referensi maju ke nilai sentinelMISSING - Penggunaan bare name juga sering diusulkan dalam diskusi
- Pendekatan bare name mengikuti preseden yang dibuat oleh
Nonedan pola yang sudah dikenal, tidak memerlukan impor, dan jauh lebih singkat
Panduan penggunaan tambahan
- Saat mendefinisikan sentinel dalam scope kelas, untuk menghindari benturan nama, atau ketika
repryang terbatasi lebih jelas, nama terbatasi yang diinginkan harus diteruskan secara eksplisit>>> class MyClass: ... NotGiven = sentinel('MyClass.NotGiven') >>> MyClass.NotGiven MyClass.NotGiven - Membuat sentinel di dalam fungsi atau metode diperbolehkan
- Karena setiap pemanggilan
sentinel()menghasilkan objek yang berbeda, sentinel yang dibuat di scope lokal akan berperilaku seperti nilai yang dibuat dengan memanggilobject()di scope tersebut - Nilai boolean
NotImplementedadalahTrue, tetapi penggunaan ini telah deprecated sejak Python 3.9 dan akan memunculkan deprecation warning - Depresiasi ini disebabkan oleh masalah khusus
NotImplementedyang dijelaskan di bpo-35712 [8] - Jika perlu mendefinisikan beberapa nilai sentinel yang saling terkait atau menentukan urutan di antaranya, maka
Enumatau pendekatan serupa harus digunakan - Untuk pengetikan sentinel semacam ini, beberapa opsi dibahas di mailing list typing-sig [9]
1 komentar
Komentar Lobste.rs
Nama yang dipilih terasa aneh karena tampaknya maknanya terlalu sempit
Kalau hanya melihat namanya, rasanya simbol unik akan menjadi primitif yang lebih fleksibel. Memang pada praktiknya ini hampir akan berperilaku seperti simbol, jadi bisa dipakai seperti itu, tetapi menamainya “Sentinels” terasa janggal. Mungkin ini karena saya terbiasa dengan Lisp
SENTINEL_Amenjadi tipe yang berbeda dariSENTINEL_B, sehingga kita bisa menanyakan apakah suatu nilaiis_a SENTINEL_ASimbol di Ruby tidak bekerja seperti itu:
:beef.is_a? :droog.class #=> trueLiteraldan string literal untuk sebagian besar kasus penggunaan simbol LispAlasan ini disebut sentinel bernama adalah karena sentinel values merupakan konsep dan pola yang umum di Python, dan sentinel dimaksudkan untuk menyelesaikan secara sempit sebagian masalah yang muncul saat memakai pola itu. Persis seperti yang dijelaskan di bagian “Motivation” dan “Rationale”
Selain itu, sentinel tidak memiliki semantik nilai, jadi dua sentinel dengan nama yang sama pun tetap merupakan nilai yang berbeda dan tidak sama satu sama lain. Jadi ini juga tidak berperilaku seperti simbol, dan sebaiknya tidak dipakai seperti itu
Untuk masalah nilai default pada argumen bernama, di Typst cukup menambahkan nilai
autodi sampingnoneuntuk mengekspresikan hampir semua antarmuka argumen bernama yang diinginkannonesaja tidak cocok secara makna untuk sebagian besar nilai default argumen bernama.nonebagus sebagai nilai return default, tetapi saat masuk sebagai argumen fungsi sering kali tidak membawa makna yang tepat sebagai kata benda.matrix(axes=None)ambigu: apakah itu berarti menghapus sumbu, atau mempertahankannya seperti biasa? Juga tidak jelas apakah memberikannoneberbeda dengan tidak memberikan apa pun. Jika beralih ke multiple dispatch untuk membedakan ada tidaknya parameter, kita kehilangan satu tempat utama untuk mendokumentasikan perilaku parameter tersebutautoadalah nilai default yang sangat baik karena langsung bermakna “tangani dengan semestinya berdasarkan informasi yang ada”. Signatureauto | nonebisa dipakai seperti boolean yang lebih eksplisit, danT | auto | nonememberi cukup banyak informasi tentang bagaimana fungsi akan memakai nilainya. Misalnya jikaTadalahcolor, makaautokemungkinan berarti memilih default seperti putih/hitam atau mewarisi dari induk,Tberarti menetapkan warna secara eksplisit, dannonetergantung konteks bisa berarti tidak menetapkan warna sama sekali atau memperlakukannya sebagai transparanMenarik, dan saya penasaran bagaimana semantik beberapa paket akan berubah. Misalnya, alih-alih mengembalikan
Item | None, bisa menulis seperti iniTentu saja, beberapa sentinel juga bisa dipakai untuk membawa makna tambahan. Ini sebenarnya sudah mungkin dari dulu, tetapi belum ada cara yang “direkomendasikan secara resmi” dalam dokumentasi. Ini bisa saja mengarahkan penulis paket ke arah yang berbeda
Memang contoh yang agak dipaksakan, tetapi dalam kasus ini kita bisa membedakan antara kegagalan karena ID itu ada tetapi tidak punya nilai terkait, dan kegagalan karena ID semacam itu memang tidak ada. Cara yang mungkin lebih “Pythonic” barangkali memakai exception, tetapi ini terlihat lebih seperti pendekatan fungsional daripada gaya Python yang biasa ditulis
Sepertinya akan lebih baik kalau langsung mengadopsi API
Symbolmilik JavaScript. Itu berguna secara umum, dan juga menyelesaikan masalah yang ingin dipecahkan di sini