Python sebagai kasus akhir C ++. Bagian 2/2

Untuk dilanjutkan. Dimulai dengan Python sebagai Kasus Ultimate C ++. Bagian 1/2 ".


Variabel dan Tipe Data


Sekarang setelah kita akhirnya mengetahui matematika, mari kita putuskan variabel apa yang harus berarti dalam bahasa kita.


Dalam C ++, seorang programmer memiliki pilihan: menggunakan variabel otomatis yang ditempatkan di stack, atau menyimpan nilai dalam memori data program, menempatkan hanya pointer ke nilai-nilai ini di stack. Bagaimana jika kita memilih hanya satu dari opsi ini untuk Python?


Tentu saja, kita tidak selalu bisa hanya menggunakan nilai-nilai variabel, karena struktur data yang besar tidak akan muat di stack, atau gerakan konstan mereka di stack akan membuat masalah kinerja. Oleh karena itu, kami hanya akan menggunakan pointer dalam Python. Ini secara konseptual akan menyederhanakan bahasa.


Begitu ekspresinya


a = 3 

akan berarti bahwa kami membuat objek "3" dalam memori data program (yang disebut "tumpukan") dan menjadikan nama "a" referensi untuk itu. Dan ekspresinya


 b = a 

dalam hal ini, itu berarti bahwa kami memaksa variabel "b" untuk merujuk ke objek yang sama dalam memori yang "a" merujuk, dengan kata lain, kami menyalin pointer.


Jika semuanya adalah pointer, lalu berapa banyak tipe daftar yang perlu kita terapkan dalam bahasa kita? Tentu saja, hanya satu yang merupakan daftar petunjuk! Anda dapat menggunakannya untuk menyimpan bilangan bulat, string, daftar lain, apa pun - setelah semua, ini adalah pointer.


Berapa banyak jenis tabel hash yang perlu kita terapkan? (Dalam Python, jenis ini disebut "kamus" - dict .) Satu! Biarkan mengasosiasikan pointer ke kunci dengan pointer ke nilai.


Jadi, kita tidak perlu mengimplementasikan dalam bahasa kita bagian besar dari spesifikasi C ++ - templat, karena kita melakukan semua operasi pada objek, dan objek selalu dapat diakses oleh pointer. Tentu saja, program yang ditulis dengan Python tidak harus membatasi diri untuk bekerja dengan pointer: ada perpustakaan seperti NumPy yang membantu para ilmuwan bekerja dengan array data dalam memori, seperti yang mereka lakukan di Fortran. Tetapi dasar bahasa - ekspresi seperti "a = 3" - selalu bekerja dengan pointer.


Konsep "semuanya adalah sebuah penunjuk" juga menyederhanakan komposisi tipe hingga batasnya. Ingin daftar kamus? Cukup buat daftar dan letakkan kamus di sana! Anda tidak perlu meminta izin Python, Anda tidak perlu mendeklarasikan tipe tambahan, semuanya berfungsi di luar kotak.


Tetapi bagaimana jika kita ingin menggunakan objek majemuk sebagai kunci? Kunci dalam kamus harus memiliki nilai yang tidak dapat diubah, jika tidak, bagaimana cara mencari nilai dengan itu? Daftar dapat berubah, oleh karena itu mereka tidak dapat digunakan dalam kapasitas ini. Untuk situasi seperti itu, Python memiliki tipe data yang, seperti daftar, adalah urutan objek, tetapi, tidak seperti daftar, urutan ini tidak berubah. Jenis ini disebut tuple atau tuple (dilafalkan "tuple" atau "tuple").


Tuples di Python memecahkan masalah bahasa scripting yang sudah lama. Jika Anda tidak terkesan dengan fitur ini, maka Anda mungkin belum pernah mencoba menggunakan bahasa skrip untuk pekerjaan serius dengan data, di mana Anda hanya dapat menggunakan string atau hanya tipe primitif sebagai kunci dalam tabel hash.


Kemungkinan lain yang diberikan tuple kepada kami adalah mengembalikan beberapa nilai dari suatu fungsi tanpa harus mendeklarasikan tipe data tambahan untuk ini, seperti yang harus Anda lakukan dalam C dan C ++. Selain itu, untuk membuatnya lebih mudah menggunakan fitur ini, operator penugasan diberkahi dengan kemampuan untuk secara otomatis membongkar tuple menjadi variabel yang terpisah.


 def get_address(): ... return host, port host, port = get_address() 

Pembongkaran memiliki beberapa efek samping yang berguna, misalnya, pertukaran nilai variabel dapat ditulis sebagai berikut:


 x, y = y, x 

Semuanya adalah pointer, yang berarti fungsi dan tipe data dapat digunakan sebagai data. Jika Anda terbiasa dengan buku "Pola Desain" oleh "The Gang of Four", Anda harus ingat metode rumit dan membingungkan yang ditawarkannya untuk membuat parameter pilihan jenis objek yang dibuat oleh program Anda saat runtime. Memang, dalam banyak bahasa pemrograman ini sulit dilakukan! Dalam Python, semua kesulitan ini hilang, karena kita tahu bahwa suatu fungsi dapat mengembalikan tipe data, bahwa kedua fungsi dan tipe data hanyalah tautan, dan tautan dapat disimpan, misalnya, dalam kamus. Ini menyederhanakan tugas hingga batas.


David Wheeler berkata: "Semua masalah pemrograman diselesaikan dengan menciptakan tingkat tipuan tambahan." Menggunakan tautan dalam Python adalah tingkat tipuan yang secara tradisional telah digunakan untuk memecahkan banyak masalah dalam banyak bahasa, termasuk C ++. Tetapi jika digunakan secara eksplisit di sana, dan ini menyulitkan program, maka dalam Python digunakan secara implisit, seragam sehubungan dengan data dari semua jenis, dan ramah pengguna.


Tetapi jika semuanya adalah tautan, lalu apa yang dimaksud tautan-tautan ini? Bahasa seperti C ++ memiliki banyak jenis. Mari kita tinggalkan Python hanya satu tipe data - sebuah objek! Spesialis di bidang teori tipe menggelengkan kepala dengan tidak setuju, tapi saya percaya bahwa satu tipe sumber data, dari mana semua tipe lain dalam bahasa tersebut diturunkan, adalah ide bagus yang memastikan keseragaman bahasa dan kemudahan penggunaannya.


Untuk konten memori tertentu, berbagai implementasi Python (PyPy, Jython, atau MicroPython) dapat mengatur memori dengan berbagai cara. Tetapi untuk lebih memahami bagaimana kesederhanaan dan keseragaman Python diimplementasikan, untuk membentuk model mental yang benar, lebih baik beralih ke implementasi referensi Python di C yang disebut CPython, yang dapat kita unduh di python.org .


 struct { struct _typeobject *ob_type; /* followed by object's data */ } 

Apa yang akan kita lihat dalam kode sumber CPython adalah struktur yang terdiri dari pointer ke informasi tentang jenis variabel yang diberikan dan muatan yang menentukan nilai spesifik dari variabel.


Bagaimana cara kerja informasi tipe? Mari kita gali kode sumber CPython lagi.


 struct _typeobject { /* ... */ getattrfunc tp_getattr; setattrfunc tp_setattr; /* ... */ newfunc tp_new; freefunc tp_free; /* ... */ binaryfunc nb_add; binaryfunc nb_subtract; /* ... */ richcmpfunc tp_richcompare; /* ... */ } 

Kami melihat pointer ke fungsi yang menyediakan semua operasi yang mungkin untuk tipe tertentu: penambahan, pengurangan, perbandingan, akses ke atribut, pengindeksan, pengirisan, dll. Operasi ini tahu cara bekerja dengan muatan yang terletak di memori di bawah pointer untuk mengetik informasi, baik itu integer, string, atau objek dari tipe yang dibuat oleh pengguna.


Ini sangat berbeda dari C dan C ++, di mana informasi jenis dikaitkan dengan nama, bukan nilai variabel. Dalam Python, semua nama dikaitkan dengan tautan. Nilai berdasarkan referensi, pada gilirannya, adalah tipe. Ini adalah inti dari bahasa dinamis.


Untuk mewujudkan semua fitur bahasa, cukup bagi kami untuk mendefinisikan dua operasi pada tautan. Salah satu yang paling jelas adalah menyalin. Saat kami menetapkan nilai ke variabel, slot di kamus, atau atribut objek, kami menyalin tautan. Ini adalah operasi yang sederhana, cepat dan sepenuhnya aman: menyalin tautan tidak mengubah konten objek.


Operasi kedua adalah panggilan fungsi atau metode. Seperti yang kami tunjukkan di atas, program Python dapat berinteraksi dengan memori hanya melalui metode yang diimplementasikan pada objek bawaan. Oleh karena itu, itu tidak dapat menyebabkan kesalahan terkait dengan akses memori.


Anda mungkin memiliki pertanyaan: jika semua variabel berisi referensi, lalu bagaimana saya bisa melindungi nilai variabel dari perubahan dengan meneruskannya ke fungsi sebagai parameter?


 n = 3 some_function(n) # Q: I just passed a pointer! # Could some_function() have changed β€œ3”? 

Jawabannya adalah bahwa tipe sederhana dalam Python tidak dapat diubah: mereka tidak mengimplementasikan metode yang bertanggung jawab untuk mengubah nilainya. int , immatable (immutable) int , float , tuple atau str menyediakan dalam bahasa seperti "everything is a pointer" efek semantik yang sama yang diberikan variabel otomatis dalam C.


Jenis dan metode terpadu menyederhanakan penggunaan pemrograman umum, atau generik, sebanyak mungkin. Fungsi min() , max() , sum() dan sejenisnya adalah bawaan, tidak perlu mengimpornya. Dan mereka bekerja dengan tipe data apa pun di mana operasi perbandingan untuk min() dan max() diimplementasikan, penambahan untuk sum() , dll.


Buat Objek


Kami menemukan secara umum bagaimana benda harus berperilaku. Sekarang kita akan menentukan bagaimana kita akan membuatnya. Ini adalah masalah sintaksis bahasa. C ++ mendukung setidaknya tiga cara untuk membuat objek:


  1. Otomatis, dengan mendeklarasikan variabel kelas ini:
     my_class c(arg); 
  2. Menggunakan operator new :
     my_class *c = new my_class(arg); 
  3. Factory, dengan memanggil fungsi arbitrer yang mengembalikan pointer:
     my_class *c = my_factory(arg); 

Seperti yang mungkin sudah Anda duga, setelah mempelajari cara berpikir pembuat Python dalam contoh di atas, sekarang kita harus memilih salah satunya.


Dari buku yang sama, Geng Empat, kami belajar bahwa pabrik adalah cara paling fleksibel dan universal untuk membuat objek. Oleh karena itu, hanya metode ini yang diimplementasikan dalam Python.


Selain universalitas, metode ini bagus karena Anda tidak perlu membebani bahasa dengan sintaksis yang tidak perlu untuk memastikannya: pemanggilan fungsi sudah diterapkan dalam bahasa kami, dan pabrik tidak lebih dari fungsi.


Aturan lain untuk membuat objek dalam Python adalah ini: semua tipe data adalah pabriknya sendiri. Tentu saja, Anda dapat menulis sejumlah pabrik kustom tambahan (yang akan menjadi fungsi atau metode biasa, tentu saja), tetapi aturan umum akan tetap berlaku:


 # Let's make type objects # their own type's factories! c = MyClass() i = int('7') f = float(length) s = str(bytes) 

Semua tipe disebut objek, dan semuanya mengembalikan nilai tipenya, ditentukan oleh argumen yang diteruskan dalam panggilan.


Dengan demikian, hanya menggunakan sintaks dasar bahasa, setiap manipulasi saat membuat objek, seperti pola "Arena" atau "Adaptasi", dapat dienkapsulasi, karena ide bagus lain yang dipinjam dari C ++ adalah bahwa jenis itu sendiri menentukan bagaimana hal itu terjadi menelurkan objeknya, bagaimana operator new bekerja untuknya.


Bagaimana dengan NULL?


Menangani null pointer menambah kompleksitas pada program, jadi kami melarang NULL. Sintaksis python tidak memungkinkan untuk membuat pointer nol. Dua operasi dasar pada pointer, yang telah kita bicarakan sebelumnya, didefinisikan sedemikian rupa sehingga setiap variabel menunjuk ke suatu objek.


Sebagai akibatnya, pengguna tidak dapat menggunakan Python untuk membuat kesalahan yang terkait dengan akses memori, seperti kesalahan segmentasi atau di luar batas buffer. Dengan kata lain, program Python tidak terpengaruh oleh dua jenis kerentanan paling berbahaya yang mengancam keamanan Internet selama 20 tahun terakhir.


Anda mungkin bertanya: "Jika struktur operasi pada objek tidak berubah, seperti yang kita lihat sebelumnya, lalu bagaimana pengguna akan membuat kelas mereka sendiri, dengan metode dan atribut yang tidak tercantum dalam struktur ini?"


Ajaibnya terletak pada kenyataan bahwa untuk kelas khusus Python memiliki "persiapan" yang sangat sederhana dengan sejumlah kecil metode yang diterapkan. Inilah yang paling penting:


 struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; /* ... */ newfunc tp_new; /* ... */ } 

tp_new() membuat tabel hash untuk kelas pengguna, sama seperti untuk tipe dict . tp_getattr() mengekstrak sesuatu dari tabel hash ini, dan tp_setattr() , sebaliknya, meletakkan sesuatu di sana. Dengan demikian, kemampuan kelas arbitrer untuk menyimpan metode dan atribut apa pun disediakan tidak pada tingkat struktur bahasa C, tetapi pada tingkat yang lebih tinggi - tabel hash. (Tentu saja, dengan pengecualian beberapa kasus yang terkait dengan optimalisasi kinerja.)


Akses pengubah


Apa yang kita lakukan dengan semua aturan dan konsep yang dibangun di sekitar kata kunci C ++ yang private dan protected ? Python, sebagai bahasa scripting, tidak membutuhkannya. Kami sudah memiliki bagian "terlindungi" dari bahasa - ini adalah data tipe bawaan. Dalam situasi apa pun, Python tidak mengizinkan program, misalnya, untuk memanipulasi bit angka floating-point! Level enkapsulasi ini cukup untuk menjaga integritas bahasa itu sendiri. Kami, pencipta Python, percaya bahwa integritas bahasa adalah satu-satunya alasan yang baik untuk menyembunyikan informasi. Semua struktur dan data program pengguna lainnya dianggap publik.


Anda dapat menulis garis bawah ( _ ) di awal nama atribut kelas untuk memperingatkan kolega: Anda tidak harus bergantung pada atribut ini. Tetapi sisa Python belajar pelajaran dari awal 90-an: kemudian banyak yang percaya bahwa alasan utama kami menulis program kembung, tidak dapat dibaca, dan buggy adalah kurangnya variabel pribadi. Saya pikir 20 tahun ke depan telah meyakinkan semua orang di industri pemrograman: variabel pribadi bukan satu-satunya, dan jauh dari obat yang paling efektif untuk program kembung dan buggy. Oleh karena itu, pembuat Python memutuskan untuk tidak khawatir tentang variabel pribadi, dan, seperti yang Anda lihat, mereka tidak gagal.


Manajemen memori


Apa yang terjadi pada objek, angka, dan string kita di tingkat yang lebih rendah? Bagaimana tepatnya mereka disimpan dalam memori, bagaimana CPython menyediakan akses bersama kepada mereka, kapan dan dalam kondisi apa mereka dihancurkan?


Dan dalam hal ini, kami memilih cara yang paling umum, dapat diprediksi dan produktif untuk bekerja dengan memori: dari sisi program-C, semua objek kami adalah pointer bersama .


Dengan pengetahuan ini dalam pikiran, struktur data yang kami periksa sebelumnya di bagian "Variabel dan tipe data" harus dilengkapi sebagai berikut:


 struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; /* followed by object's data */ } } 

Jadi, setiap objek dalam Python (maksud kami implementasi CPython, tentu saja) memiliki counter referensi sendiri. Setelah menjadi nol, objek dapat dihapus.


Mekanisme penghitungan tautan tidak bergantung pada perhitungan tambahan atau proses latar belakang - suatu objek dapat dihancurkan secara instan. Selain itu, ia menyediakan lokalitas data tinggi: seringkali, memori mulai digunakan kembali segera setelah dibebaskan. Objek yang baru saja hancur kemungkinan besar digunakan baru-baru ini, yang berarti berada di cache prosesor. Oleh karena itu, objek yang baru dibuat akan tetap berada di cache. Dua faktor ini - kesederhanaan dan lokalitas - menjadikan penghitungan tautan cara pengumpulan sampah yang sangat produktif.


(Karena kenyataan bahwa objek dalam program nyata sering merujuk satu sama lain, penghitung referensi dalam kasus tertentu tidak dapat turun ke nol bahkan ketika objek tidak lagi digunakan dalam program. Oleh karena itu, CPython juga memiliki mekanisme pengumpulan sampah kedua - latar belakang, berdasarkan pada generasi objek. - kira - kira terjemahan. )


Kesalahan pengembang python


Kami mencoba mengembangkan bahasa yang cukup sederhana untuk pemula, tetapi juga cukup menarik bagi para profesional. Pada saat yang sama, kami tidak dapat menghindari kesalahan dalam memahami dan menggunakan alat yang kami buat sendiri.


Python 2, karena inersia pemikiran yang terkait dengan bahasa scripting, mencoba mengkonversi tipe string, seperti yang dilakukan oleh bahasa dengan pengetikan yang lemah. Jika Anda mencoba menggabungkan string byte dengan string di Unicode, interpreter secara implisit mengubah string byte ke Unicode menggunakan tabel kode yang tersedia pada sistem dan menyajikan hasilnya dalam Unicode:


 >>> 'byte string ' + u'unicode string' u'byte string unicode string' 

Akibatnya, beberapa situs web berfungsi dengan baik ketika pengguna mereka menggunakan bahasa Inggris, tetapi mereka menghasilkan kesalahan samar ketika menggunakan karakter dari huruf lain.


Bug desain bahasa ini telah diperbaiki dalam Python 3:


 >>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str 

Kesalahan serupa di Python 2 terkait dengan penyortiran daftar "naif" yang terdiri dari elemen-elemen yang tak tertandingi:


 >>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b'] 

Python 3 dalam hal ini menjelaskan kepada pengguna bahwa ia mencoba melakukan sesuatu yang tidak terlalu berarti:


 >>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str() 

Kekerasan


Pengguna sekarang dan kemudian kadang-kadang menyalahgunakan sifat dinamis dari bahasa Python, dan kemudian, di tahun 90-an, ketika praktik terbaik belum dikenal secara luas, ini sering terjadi:


 class Address(object): def __init__(self, host, port): self.host = host self.port = port 

"Tapi ini tidak optimal!" - Beberapa mengatakan, - β€œBagaimana jika port tidak berbeda dari nilai default? Bagaimanapun, kami menghabiskan seluruh atribut kelas pada penyimpanannya! ” Dan hasilnya adalah sesuatu seperti


 class Address(object): def __init__(self, host, port=None): self.host = host if port is not None: # so terrible self.port = port 

Jadi, objek dengan tipe yang sama muncul dalam program, yang, bagaimanapun, tidak dapat dioperasikan secara seragam, karena beberapa dari mereka memiliki atribut tertentu, sementara yang lain tidak! Dan kami tidak dapat menyentuh atribut ini tanpa memeriksa keberadaannya di muka:


 # code was forced to use introspection # (terrible!) if hasattr(addr, 'port'): print(addr.port) 

Saat ini, kelimpahan hasattr() , isinstance() dan introspeksi lainnya adalah tanda pasti dari kode yang buruk, dan itu dianggap praktik terbaik untuk membuat atribut selalu hadir dalam objek. Ini memberikan sintaksis yang lebih sederhana ketika mengaksesnya:


 # today's best practice: # every atribute always present if addr.port is not None: print(addr.port) 

Jadi, percobaan awal dengan atribut yang ditambahkan dan dihapus secara dinamis berakhir, dan sekarang kita melihat kelas dalam Python dengan cara yang sama seperti di C ++.


Kebiasaan buruk lain dari Python awal adalah penggunaan fungsi di mana argumen dapat memiliki tipe yang sama sekali berbeda. Misalnya, Anda mungkin berpikir bahwa mungkin terlalu sulit bagi pengguna untuk membuat daftar nama kolom setiap kali, dan Anda harus mengizinkannya untuk meneruskannya juga sebagai satu baris, di mana nama-nama kolom terpisah dipisahkan oleh, katakanlah, koma:


 class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns 

Namun pendekatan ini dapat menimbulkan masalah. Misalnya, bagaimana jika pengguna secara tidak sengaja memberi kami baris yang tidak dimaksudkan untuk digunakan sebagai daftar nama kolom? Atau jika nama kolom harus mengandung koma?


Selain itu, kode semacam itu lebih sulit untuk dipertahankan, didebug, dan terutama pengujian: dalam pengujian, dimungkinkan untuk memeriksa hanya satu dari dua jenis yang kami dukung, tetapi cakupannya masih 100%, dan kami tidak akan menguji jenis lainnya.


Sebagai hasilnya, kami sampai pada kesimpulan bahwa Python memungkinkan pengguna untuk meneruskan argumen dari jenis apa pun ke fungsi, tetapi sebagian besar dari mereka dalam sebagian besar situasi akan menggunakan fungsi dengan cara yang sama seperti di C: meneruskan argumen dengan tipe yang sama dengannya.


Kebutuhan untuk menggunakan eval() dalam suatu program dianggap salah perhitungan arsitektur eksplisit. Kemungkinan besar, Anda tidak tahu bagaimana melakukan hal yang sama dengan cara normal. βˆ’ , Jupyter notebook - βˆ’ eval() , Python ! , C++ .


, ( getattr() , hasattr() , isinstance() ) . , , , , : , , , !



: , . 20 , C++ Python. , , . .


, shared_ptr TensorFlow 2016 2018 .


TensorFlow βˆ’ C++-, Python- ( C++ βˆ’ TensorFlow, ).


gambar


TensorFlow, shared_ptr , . , .


C++? . , ? , , C++ Python!

Source: https://habr.com/ru/post/id464405/


All Articles