Menulis Subclass Properti

Class Property didesain untuk dijadikan subclass. Namun, biasanya lebih mudah untuk membuat subclass dari subclass Property yang sudah ada.

Semua atribut Property khusus, bahkan yang dianggap 'publik', memiliki nama yang diawali dengan garis bawah. Ini karena StructuredProperty menggunakan namespace atribut non-garis bawah untuk merujuk ke nama Property bertingkat; ini penting untuk menentukan kueri pada sub-properti.

Class Property dan subclass-nya yang telah ditetapkan memungkinkan subclassing menggunakan API validasi dan konversi composable (atau stackable). Tindakan ini memerlukan beberapa definisi terminologi:

  • Nilai pengguna adalah nilai yang akan ditetapkan dan diakses oleh kode aplikasi menggunakan atribut standar pada entity.
  • Nilai dasar adalah nilai yang akan diserialisasi ke dan dideserialisasi dari Datastore.

Subclass Property yang mengimplementasikan transformasi tertentu antara nilai pengguna dan nilai yang dapat diserialisasi harus mengimplementasikan dua metode, _to_base_type() dan _from_base_type(). Metode ini tidak boleh memanggil metode super(). Inilah yang dimaksud dengan API composable (atau stackable).

API mendukung class stacking dengan konversi basis pengguna yang lebih canggih: konversi pengguna ke basis berubah dari lebih canggih menjadi kurang canggih, sedangkan konversi basis ke pengguna berubah dari kurang canggih menjadi lebih canggih. Misalnya, lihat hubungan antara BlobProperty, TextProperty, dan StringProperty. Misalnya, TextProperty mewarisi dari BlobProperty; kodenya cukup sederhana karena mewarisi sebagian besar perilaku yang dibutuhkan.

Selain _to_base_type() dan _from_base_type(), metode _validate() juga merupakan API composable.

API validasi membedakan antara nilai pengguna lax dan strict. Kumpulan nilai lax adalah superset dari kumpulan nilai strict. Metode _validate() mengambil nilai lax dan jika perlu mengonversinya menjadi nilai strict. Ini berarti bahwa nilai lax diterima ketika menetapkan nilai properti, dan hanya nilai ketat yang akan ditampilkan ketika mendapatkan nilai properti. Jika tidak ada konversi yang diperlukan, _validate() dapat menampilkan Tidak ada. Jika argumen berada di luar kumpulan nilai lax yang diterima, _validate() harus memunculkan pengecualian, sebaiknya TypeError atau datastore_errors.BadValueError.

_validate(), _to_base_type(), dan _from_base_type() tidak perlu menangani:

  • None: Ketiganya tidak akan dipanggil dengan None (dan jika menampilkan Tidak ada, artinya nilai tidak memerlukan konversi).
  • Nilai berulang: Infrastruktur akan menangani pemanggilan _from_base_type() atau _to_base_type() untuk setiap item daftar dalam nilai berulang.
  • Membedakan nilai pengguna dari nilai dasar: Infrastruktur menangani hal ini dengan memanggil API composable.
  • Perbandingan: Operasi perbandingan memanggil _to_base_type() pada operand-nya.
  • Membedakan antara nilai pengguna dan nilai dasar: Infrastruktur menjamin bahwa _from_base_type() akan dipanggil dengan nilai dasar (tidak digabungkan), dan _to_base_type() tersebut akan dipanggil dengan nilai pengguna.

Misalnya, Anda perlu menyimpan bilangan bulat yang sangat panjang. IntegerProperty standar hanya mendukung bilangan bulat 64-bit (ditandatangani). Properti Anda mungkin menyimpan bilangan bulat yang lebih panjang sebagai string; ada baiknya jika class properti menangani konversi. Aplikasi yang menggunakan class properti Anda mungkin terlihat seperti ini

from datetime import date

import my_models
...
class MyModel(ndb.Model):
    name = ndb.StringProperty()
    abc = LongIntegerProperty(default=0)
    xyz = LongIntegerProperty(repeated=True)
...
# Create an entity and write it to the Datastore.
entity = my_models.MyModel(name='booh', xyz=[10**100, 6**666])
assert entity.abc == 0
key = entity.put()
...
# Read an entity back from the Datastore and update it.
entity = key.get()
entity.abc += 1
entity.xyz.append(entity.abc//3)
entity.put()
...
# Query for a MyModel entity whose xyz contains 6**666.
# (NOTE: using ordering operations don't work, but == does.)
results = my_models.MyModel.query(
    my_models.MyModel.xyz == 6**666).fetch(10)

Ini terlihat sederhana dan mudah. Contoh ini juga menunjukkan penggunaan beberapa opsi properti standar (default, berulang). Sebagai penulis LongIntegerProperty, Anda akan senang mengetahui bahwa Anda tidak perlu menulis "boilerplate" apa pun agar berfungsi. Lebih mudah untuk mendefinisikan subclass dari properti lain, misalnya:

class LongIntegerProperty(ndb.StringProperty):
    def _validate(self, value):
        if not isinstance(value, (int, long)):
            raise TypeError('expected an integer, got %s' % repr(value))

    def _to_base_type(self, value):
        return str(value)  # Doesn't matter if it's an int or a long

    def _from_base_type(self, value):
        return long(value)  # Always return a long

Saat Anda menetapkan nilai properti pada entity, misalnya ent.abc = 42, metode _validate() Anda akan dipanggil, dan (jika tidak memunculkan pengecualian) nilai tersebut disimpan di entity. Saat Anda menulis entity ke Datastore, metode _to_base_type() akan dipanggil, dengan mengonversi nilai menjadi string. Kemudian, nilai tersebut akan diserialisasi oleh class dasar, StringProperty. Rantai peristiwa terbalik terjadi saat entity dibaca kembali dari Datastore. Class StringProperty dan Property bersama-sama menangani detail lainnya, seperti melakukan serialisasi dan deserialisasi string, menetapkan default, serta menangani nilai properti berulang.

Dalam contoh ini, mendukung ketidaksetaraan (yaitu kueri yang menggunakan <, <=, >, >=) memerlukan upaya lebih. Contoh implementasi berikut menerapkan ukuran maksimum bilangan bulat dan menyimpan nilai sebagai string panjang tetap:

class BoundedLongIntegerProperty(ndb.StringProperty):
    def __init__(self, bits, **kwds):
        assert isinstance(bits, int)
        assert bits > 0 and bits % 4 == 0  # Make it simple to use hex
        super(BoundedLongIntegerProperty, self).__init__(**kwds)
        self._bits = bits

    def _validate(self, value):
        assert -(2 ** (self._bits - 1)) <= value < 2 ** (self._bits - 1)

    def _to_base_type(self, value):
        # convert from signed -> unsigned
        if value < 0:
            value += 2 ** self._bits
        assert 0 <= value < 2 ** self._bits
        # Return number as a zero-padded hex string with correct number of
        # digits:
        return '%0*x' % (self._bits // 4, value)

    def _from_base_type(self, value):
        value = int(value, 16)
        if value >= 2 ** (self._bits - 1):
            value -= 2 ** self._bits
        return value

Ini dapat digunakan dengan cara yang sama seperti LongIntegerProperty, kecuali bahwa Anda harus meneruskan jumlah bit ke konstruktor properti, misalnya BoundedLongIntegerProperty(1024).

Anda dapat membuat subclass jenis properti lain dengan cara yang sama.

Pendekatan ini juga berfungsi untuk menyimpan data terstruktur. Misalkan Anda memiliki class Python FuzzyDate yang mewakili rentang tanggal; metode ini menggunakan kolom first dan last untuk menyimpan awal dan akhir rentang tanggal:

from datetime import date

...
class FuzzyDate(object):
    def __init__(self, first, last=None):
        assert isinstance(first, date)
        assert last is None or isinstance(last, date)
        self.first = first
        self.last = last or first

Anda dapat membuat FuzzyDateProperty yang berasal dari StructuredProperty. Sayangnya, kode yang terakhir tidak berfungsi dengan class Python lama biasa; kode ini memerlukan subclass Model. Jadi, definisikan subclass Model sebagai representasi perantara;

class FuzzyDateModel(ndb.Model):
    first = ndb.DateProperty()
    last = ndb.DateProperty()

Selanjutnya, buat subclass StructuredProperty yang meng-hardcode argumen modelclass menjadi FuzzyDateModel, serta menentukan metode _to_base_type() dan _from_base_type() untuk mengonversi antara FuzzyDate dan FuzzyDateModel:

class FuzzyDateProperty(ndb.StructuredProperty):
    def __init__(self, **kwds):
        super(FuzzyDateProperty, self).__init__(FuzzyDateModel, **kwds)

    def _validate(self, value):
        assert isinstance(value, FuzzyDate)

    def _to_base_type(self, value):
        return FuzzyDateModel(first=value.first, last=value.last)

    def _from_base_type(self, value):
        return FuzzyDate(value.first, value.last)

Aplikasi dapat menggunakan class ini seperti berikut:

class HistoricPerson(ndb.Model):
    name = ndb.StringProperty()
    birth = FuzzyDateProperty()
    death = FuzzyDateProperty()
    # Parallel lists:
    event_dates = FuzzyDateProperty(repeated=True)
    event_names = ndb.StringProperty(repeated=True)
...
columbus = my_models.HistoricPerson(
    name='Christopher Columbus',
    birth=my_models.FuzzyDate(date(1451, 8, 22), date(1451, 10, 31)),
    death=my_models.FuzzyDate(date(1506, 5, 20)),
    event_dates=[my_models.FuzzyDate(
        date(1492, 1, 1), date(1492, 12, 31))],
    event_names=['Discovery of America'])
columbus.put()

# Query for historic people born no later than 1451.
results = my_models.HistoricPerson.query(
    my_models.HistoricPerson.birth.last <= date(1451, 12, 31)).fetch()

Misalkan Anda ingin menerima objek date biasa selain objek FuzzyDate sebagai nilai untuk FuzzyDateProperty. Untuk melakukannya, ubah metode _validate() sebagai berikut:

def _validate(self, value):
    if isinstance(value, date):
        return FuzzyDate(value)  # Must return the converted value!
    # Otherwise, return None and leave validation to the base class

Sebagai gantinya, Anda dapat membuat subclass FuzzyDateProperty sebagai berikut (dengan asumsi FuzzyDateProperty._validate() seperti yang ditampilkan di atas).

class MaybeFuzzyDateProperty(FuzzyDateProperty):
    def _validate(self, value):
        if isinstance(value, date):
            return FuzzyDate(value)  # Must return the converted value!
        # Otherwise, return None and leave validation to the base class

Saat Anda menetapkan nilai ke kolom MaybeFuzzyDateProperty, MaybeFuzzyDateProperty._validate() dan FuzzyDateProperty._validate() akan dipanggil dalam urutan tersebut. Hal yang sama berlaku untuk _to_base_type() dan _from_base_type(): metode di dalam superclass dan subclass digabungkan secara implisit. (Jangan gunakan super untuk mengontrol perilaku yang diwariskan. Untuk ketiga metode ini, interaksinya halus dan super tidak melakukan apa yang Anda inginkan.)