Penjelasan Beranotasi Simon Willison tentang Dataklasses David Beazley

Penjelasan Beranotasi Simon Willison tentang Dataklasses David Beazley

David Beazley di Twitter:

Jadi, saya baru saja menerbitkan putaran jahat yang nikmat ini di kelas data. Ini kecil dan kelas yang dihasilkan mengimpor sekitar 15-20 lebih cepat daripada kelas data. https://github.com/dabeaz/dataklasses

Saya memutuskan untuk membuat versi kode aslinya yang beranotasi berat untuk mencari tahu sendiri cara kerjanya.

Daftar Isi

Apa yang dilakukan kode ini

Dibutuhkan ini:

@dataklass kelas Koordinat : x: int

y: 

int

Dan mengubahnya menjadi ini:

kelas Koordinat : __match_args__ = (

"x", "y") def __init__ (diri sendiri, x, y): diri sendiri.x = x diri sendiri.y = y def __repr__( diri sendiri): kembali f"Koordinat( {diri sendiri.x!r}, {diri sendiri.y!R})" def __eq__(diri sendiri, lainnya): jika sendiri.__kelas__ adalah lainnya. __kelas__: kembali (diri sendiri,x, diri sendiri.y,) == (lainnya.x , lainnya.y,) kalau tidak: kembali Tidak Diimplementasikan

Trik kunci: pembuatan kode

Kunci untuk memahami cara kerja kode adalah dengan memahami bahwa itu menggunakan pembuatan kode. David mengintrospeksi properti kelas beranotasi, dari kelas itu sendiri dan superclass mana pun, lalu menghitung jumlahnya. Dia kemudian menghasilkan metode yang terlihat seperti ini:

Kemudian gunakan func.__code__.replace(co_names=repl_co_names, co_varnames=repl_co_varnames) metode untuk mengganti nama itu _0 dan _1 variabel kembali ke x dan y.

Saya belum mengerti mengapa dia melakukan ini sebagai lawan menghasilkan kode menggunakan x dan y secara langsung. Saya akan memperbarui ini dengan penjelasan setelah saya menemukannya!

Pembaruan: Jacob Kaplan-Moss menjelaskannya di sini!

Itu benar-benar liar trik inti dari ini, dan apa yang membuatnya cepat:

bytecode untuk

__init__(sendiri, x, y)

persis sama dengan bytecode untuk __init__(self, foo, bar)!

Jadi itu berarti Dave dapat men-cache kode yang dihasilkan untuk "any __init__ berfungsi dengan dua argumen" dan kemudian

re -gunakan kode yang sama persis untuk 2-arity selanjutnya __init__s!

Salinan beranotasi saya

 

Pertama, termasuk pesan hak cipta seperti yang dipersyaratkan oleh pesan hak cipta:

# dataklasses.py # # https://github.com/dabeaz/dataklasses # # Pengarang : David Beazley (@dabeaz). # http://www.dabeaz.com # # Hak cipta (C) 2021-2022. # # Izin diberikan untuk menggunakan , salin, dan ubah kode ini di any # cara selama pesan hak cipta dan disclaimer ini tetap ada # kode sumber. Tidak ada garansi. Coba gunakan kode untuk # lebih baik.

Anotasi saya dimulai di sini (Saya juga menjalankannya hingga Hitam).

mengurangi # Dekorator ini mengambil fungsi yang mengembalikan kode sumber Python dan mengkompilasinya # itu menjadi fungsi Python. Jadi jika func(fields) mengembalikan string berikut: # # def __init__(sendiri, a): # diri.a=a # # Nilai yang dikembalikan adalah itu fungsi yang dikompilasi. Dengan beberapa penyempurnaan. def kodegen(fungsi): # Ini menyimpan hasil jadi jika Anda melewati numfields yang sama tidak perlu # melakukan pekerjaan yang sama dua kali . # # Ini adalah pengoptimalan kinerja utama untuk kode ini – ini berarti # kode yang dihasilkan sama dapat digunakan kembali untuk setiap kelas yang memiliki kesamaan # jumlah argumen! @lru_cache def make_func_code(numfields) : # numfields di len(fields) nama = [f”_{n} for n in range(numfields)] # nama sekarang [“_0”, “_1”, “_2”] tergantung pada numfields # # Kami memanggil func() dengan daftar nama itu dan exec() the # mengembalikan kode sumber. Argumen ketiga untuk exec() adalah # locals() – ini adalah kamus yang fungsi exec() # akan diisi dengan simbol baru. # # Kami menggunakan d:={} operator walrus di sini sehingga kami dapat # lihat kamus itu dengan nama d sebentar lagi. eksekutif(

 func(

nama), global(), d := {}) # d.popitem() kembali pasangan (kunci, nilai) pertama di # kamus. Kita tahu bahwa kamus locals() # hanya akan memiliki satu simbol di dalamnya, karena # kita sekarang bahwa kode dikembalikan oleh fungsi(nama) saja # mendefinisikan satu fungsi. Jadi ini mengembalikan nilai # dari item pertama di locals(), function object kembali d

()[1] # Dekorasi ini() adalah nilai kembalian dari dekorator, # yang artinya kode berikut: # # @codegen # def make__init__(bidang): # # … # Mengubah make__init__ menjadi fungsi yang ditentukan oleh dekorasi() def dekorasi(bidang): # Seperti yang ditunjukkan di atas, ini menggunakan exec() untuk mengkompilasi dan mengembalikan # badan fungsi yang dibuat menggunakan kode sumber yang dihasilkan fungsi = make_func_code(

len(

bidang)) # Tapi ingat: karena kami men-cache dan menggunakan kembali badan metode, ini memiliki # jelek _0, _1 parameter dan variabel yang ingin kita buat lebih bagus. # co_names: tuple nama selain argumen dan fungsi lokal co_names = fungsi.__code__.co_name s # Untuk contoh Koordinat, nama_bersama=(“_0”, “_1”) # co_varnames: tuple nama argumen dan variabel lokal co_varnames = fungsi.__kode__.co_varnames # Untuk contoh Koordinat, co_varnames=(“self”, “_0”, “_1” ) # Kami akan mengganti co_names dan co_varnames dengan modifikasi # versi – jadi kita perlu membuat dua tupel pengganti untuk mereka. # # start :=co_names.index(“_0”) menggunakan operator walrus untuk keduanya # cari tahu indeks berbasis 0 dari simbol _0 dan menetapkannya ke s # # Kami menggunakan (*a, *b, *c) untuk membuat tupel baru yang merupakan hasil dari # menggabungkan ketiga tupel input tersebut. repl_co_names

= (

# Iris semuanya dalam co_names hingga simbol _0 pertama

*co_names[start + len(fields) :], 

# Kemudian masukkan bidang, yang merupakan properti kelas beranotasi *bidang, # Sekarang semuanya dalam co_names berikut *co_names

bidang, 
*

co_varnames[start + len(fields) :], ) jika “_0” di co_varnames else co_varnames # type(func) mengembalikan objek internal Python yang disebut “fungsi” # yang dapat dipanggil dan memiliki tanda tangan fungsi ini: # # fungsi(kode, global, nama=Tidak ada, argdefs=Tidak ada, penutupan=Tidak ada) #

repl_co_names

, co_varnames=repl_co_varnames ), fungsi # adalah kamus dengan hal-hal baru yang ditambahkan ke dalamnya. # # reversed(Coordinates.__mro__) loop melalui setiap superclass saat ini # kelas, dimulai dari “objek”. # # Jadi lambda dipanggil melawan setiap superclass secara bergantian, dan setiap kali itu # membaca bidang __annotations__, jika tersedia. # # Coordinates.__annotations__ mengembalikan {‘x’: int, ‘y’: int} # # dict1 | dict2 di Python mengembalikan dict baru yang menggabungkan dua sebelumnya # # Jadi fungsi ini mengembalikan kamus gabungan dari __annotations__ from # setiap kelas dalam hierarki superclass. kembali kurangi ( lambda x, y: x |

getattr

(y, “__annotations__”, {}),

terbalik

(cls.__mro__), {} ) # Berikutnya adalah fungsi yang menghasilkan metode yang berbeda. Ingat mereka lulus # bidang yang adalah {‘x’: , ‘y’:

} @codegen def make__init__(bidang): # Memanggil “,”.join(dict) bergabung saja kunci kamus itu kode = “def __init__(sendiri, “ + “,”.Ikuti(

bidang) 

+ “): n# Jadi di sini kita memiliki: # def __init__ (sendiri, x, y): kembali kode + n .Ikuti(f” diri sendiri.{nama}={nama}n untuk nama di bidang)

# Ini menambahkan:  

# self.x=x # self.y=y @kodegen def make__repr__(bidang): kembali ( “def __repr__(self): n # type(self).__name__ memberi kita nama kelas: “Koordinat” ‘ return f”{type(self).__name__}(‘ # Ini memberi kita {self.x!r}, {self.y! r} yang dalam f-string # memberi kami __repr__() versi properti objek tersebut + “, “.Ikuti(“{diri sendiri.” + nama + “!R}” untuk nama di bidang) + ‘)”n ) # Jadi ini menghasilkan: # def __repr__(self): # return f”Koordinat({self.x!r}, {self.y!r})” @kodegen def make__eq__(bidang): selfvals = “,”.Ikuti(f”diri sendiri.{nama} untuk nama di bidang ) lain-lain = “,,”.Ikuti(f”lainnya.{

nama }

untuk nama di bidang ) kembali ( “def __eq__(diri sendiri, orang lain): n ” jika self.__class__ adalah other.__class__: n f” kembali ({selfvals},)==({lainnya S},)n” kalau tidak: n ” return NotImplementedn ) # Ini menghasilkan: # def __eq__(diri sendiri, orang lain): # jika self.__class__ adalah yang lain.__class__: # return (self,x, self.y,)==(other.x, other.y) # kalau tidak: # return NotImplemented @kodegen def make__iter__(bidang): kembali “def __iter__(sendiri): n” + n.Ikuti( f” menghasilkan diri sendiri.{ nama} untuk nama di bidang ) @kodegen def make__hash__(bidang): self_tuple = “(“ + “,,”.Ikuti(f”diri.{nama}untuk nama di

bidang

) + “,)” kembali “def __hash__(diri): n f” mengembalikan hash({ self_tuple})ndef

dataklass(

cls): bidang = all_hints

# '__annotations__': {'x': , 'y': },

# ‘__dict__’: , # ‘__weakref__’: , # ‘__doc__’: Tidak ada} # # Tujuan dari fungsi ini terutama untuk menambahkan __init__ dan # __repr__ dan __eq__ metode, tetapi hanya jika belum didefinisikan jika bukan “__init__” di clsdict:

cls.__init__
 = make__init__(bidang) jika bukan "__repr__" di clsdict: cls.__repr__ = 
make__repr__

(bidang) jika tidak "__eq__" di clsdict: cls.__eq__ =

make__eq__

(bidang) # Tidak yakin mengapa ini dikomentari sekarang: # jika tidak ‘__iter__’ di clsdict: cls.__iter__=make__iter__(fields) # jika tidak ‘__hash__’ di clsdict: cls.__hash__=make__hash__(fields) cls.__match_args__ = bidang # Ini terkait dengan pengetikan struktural Python: # https://www.python.org/dev/peps/pep-0622/#special-attribute-match-args kembali cl s # Contoh penggunaan jika __nama__ == “__utama__”: @dataklass kelas Koordinat: x: int y: int

Dibuat 2021-12-19T20:30:57-08:00, diperbarui 2021-12-19T21:38:45-08:00 · Riwayat · Edit

Baca selengkapnya