Dapatkah modul memiliki properti dengan cara yang sama seperti objek?

100

Dengan properti python, saya bisa membuatnya seperti itu

obj.y 

memanggil fungsi, bukan hanya mengembalikan nilai.

Apakah ada cara untuk melakukan ini dengan modul? Saya memiliki kasus yang saya inginkan

module.y 

untuk memanggil fungsi, bukan hanya mengembalikan nilai yang disimpan di sana.

Josh Gibson
sumber
2
Lihat __getattr__modul untuk solusi yang lebih modern.
wim

Jawaban:

56

Hanya instance dari kelas gaya baru yang dapat memiliki properti. Anda dapat membuat Python percaya bahwa instance seperti itu adalah modul dengan menyimpannya sys.modules[thename] = theinstance. Jadi, misalnya, file modul m.py Anda bisa jadi:

import sys

class _M(object):
    def __init__(self):
        self.c = 0
    def afunction(self):
        self.c += 1
        return self.c
    y = property(afunction)

sys.modules[__name__] = _M()
Alex Martelli
sumber
2
Apakah ada orang lain yang mencoba ini? Ketika saya meletakkan kode ini dalam satu file x.py dan mengimpornya dari yang lain, kemudian memanggil hasil xy di AttributeError: objek 'NoneType' tidak memiliki atribut 'c', karena _M entah bagaimana memiliki nilai Tidak ada ...
Stephan202
3
Memang kode tersebut bekerja pada interpreter. Tetapi ketika saya memasukkannya ke dalam sebuah file (katakanlah, bowwow.py) dan saya mengimpornya dari file lain (otherfile.py), maka itu tidak lagi berfungsi ...
Stephan202
4
T: Apakah akan ada keuntungan tertentu untuk mendapatkan kelas instance dari types.ModuleTypeseperti yang ditunjukkan dalam jawaban @ Tidak diketahui yang sebaliknya sangat mirip?
martineau
11
Hanya instance dari kelas gaya baru yang dapat memiliki properti. Ini bukan alasannya: modul adalah turunan dari kelas gaya baru, di builtins.moduledalamnya mereka adalah contoh , yang dengan sendirinya merupakan turunan type(yang merupakan definisi dari kelas gaya baru). Masalahnya adalah bahwa sifat harus berada di kelas, tidak contoh: jika Anda melakukannya f = Foo(), f.some_property = property(...)itu akan gagal dengan cara yang sama seperti jika Anda naif memasukkannya ke dalam modul. Solusinya adalah dengan meletakkannya di kelas, tetapi karena Anda tidak ingin semua modul memiliki properti, Anda membuat subkelas (lihat jawaban tidak diketahui).
Thanatos
3
@Joe, perubahan globals()(menjaga kunci tetap utuh tetapi mengatur ulang nilainya ke None) pada rebinding nama sys.modulesadalah masalah Python 2 - Python 3.4 berfungsi sebagaimana mestinya. Jika Anda memerlukan akses ke objek kelas di Py2, tambahkan misalnya _M._cls = _Mtepat setelah classpernyataan (atau simpan dengan setara di beberapa namespace lain) dan akses seperti self._clsdalam metode yang memerlukannya ( type(self)mungkin OK, tetapi tidak jika Anda juga melakukan subkelas apa pun _M) .
Alex Martelli
54

Saya akan melakukan ini untuk mewarisi semua atribut modul dengan benar, dan diidentifikasi dengan benar oleh isinstance ()

import types

class MyModule(types.ModuleType):
    @property
    def y(self):
        return 5


>>> a=MyModule("test")
>>> a
<module 'test' (built-in)>
>>> a.y
5

Dan kemudian Anda dapat memasukkan ini ke dalam sys.modules:

sys.modules[__name__] = MyModule(__name__)  # remember to instantiate the class
Tidak diketahui
sumber
Ini tampaknya hanya berfungsi untuk kasus yang paling sederhana. Masalah yang mungkin terjadi adalah: (1) beberapa importir pembantu mungkin juga mengharapkan atribut lain seperti __file__yang harus ditentukan secara manual, (2) impor yang dibuat dalam modul yang berisi kelas tidak akan "terlihat" selama run time, dll ...
tutuDajuju
1
Ini tidak perlu untuk menurunkan subclass dari types.ModuleType, setiap (gaya baru) kelas yang akan dilakukan. Atribut modul khusus apa yang ingin Anda warisi?
martineau
Bagaimana jika modul asli adalah sebuah paket dan saya ingin mengakses modul di bawah modul asli?
kawing-chiu
2
@martineau Anda akan memiliki repr modul, Anda dapat menentukan nama modul saat __init__sebuah instance, dan Anda akan mendapatkan perilaku yang benar saat menggunakan isinstance.
wim
@wim: Poin yang diambil, meskipun sejujurnya tidak ada yang tampaknya menjadi IMO yang penting.
martineau
35

Karena PEP 562 telah diimplementasikan dengan Python> = 3.7, sekarang kita bisa melakukan ini

file: module.py

def __getattr__(name):
    if name == 'y':
        return 3
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

other = 4

pemakaian:

>>> import module
>>> module.y
3
>>> module.other
4
>>> module.nosuch
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "module.py", line 4, in __getattr__
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
AttributeError: module 'module' has no attribute 'nosuch'

Perhatikan bahwa jika Anda menghilangkan raise AttributeErrorin the __getattr__function, itu berarti fungsi diakhiri dengan return None, maka module.nosuchakan mendapatkan nilai None.

John Lin
sumber
2
Berdasarkan ini, saya menambahkan jawaban lain: stackoverflow.com/a/58526852/2124834
3
Ini hanya setengah dari sebuah properti. Tidak ada penyetel.
wim
Sayangnya tampaknya tidak sulit untuk membuat tooling mengetahui atribut seperti itu (?) ( Getattr hanya dipanggil jika tidak ada anggota reguler yang ditemukan)
olejorgenb
9

Berdasarkan jawaban John Lin :

def module_property(func):
    """Decorator to turn module functions into properties.
    Function names must be prefixed with an underscore."""
    module = sys.modules[func.__module__]

    def base_getattr(name):
        raise AttributeError(
            f"module '{module.__name__}' has no attribute '{name}'")

    old_getattr = getattr(module, '__getattr__', base_getattr)

    def new_getattr(name):
        if f'_{name}' == func.__name__:
            return func()
        else:
            return old_getattr(name)

    module.__getattr__ = new_getattr
    return func

Penggunaan (perhatikan garis bawah di depan), di the_module.py:

@module_property
def _thing():
    return 'hello'

Kemudian:

import the_module

print(the_module.thing)  # prints 'hello'

Garis bawah di depan diperlukan untuk membedakan fungsi yang di-properti-kan dari fungsi aslinya. Saya tidak dapat memikirkan cara untuk menetapkan ulang pengenal, karena selama waktu eksekusi dekorator, pengenal tersebut belum ditetapkan.

Perhatikan bahwa IDE tidak akan mengetahui bahwa properti itu ada dan akan menampilkan gelombang merah.


sumber
Bagus! Dibandingkan dengan properti kelas @property def x(self): return self._xmenurut saya def thing()tanpa garis bawah lebih konvensional. Dan dapatkah Anda membuat dekorator "penyetel properti modul" dalam jawaban Anda juga?
John Lin
2
@ JohnLin, saya mencoba menerapkan def thing()saran Anda . Masalahnya adalah bahwa __getattr__hanya dipanggil untuk atribut yang hilang . Tapi setelah @module_property def thing(): …berjalan, the_module.thingditentukan, jadi getattr tidak akan pernah dipanggil. Kita perlu mendaftar thingdi dekorator dan kemudian menghapusnya dari namespace modul. Saya mencoba kembali Nonedari dekorator, tetapi kemudian thingdidefinisikan sebagai None. Seseorang bisa melakukannya @module_property def thing(): … del thingtetapi saya menemukan itu lebih buruk daripada menggunakan thing()sebagai fungsi
Ben Mares
Oke, saya melihat tidak ada "penyetel properti modul", atau "modul __getattribute__". Terima kasih.
John Lin
5

Kasus penggunaan tipikal adalah: memperkaya modul (besar) yang ada dengan beberapa (beberapa) atribut dinamis - tanpa mengubah semua materi modul menjadi tata letak kelas. Sayangnya patch kelas modul yang paling sederhana seperti sys.modules[__name__].__class__ = MyPropertyModulegagal dengan TypeError: __class__ assignment: only for heap types. Jadi pembuatan modul perlu kabel.

Pendekatan ini melakukannya tanpa kait impor Python, hanya dengan memiliki beberapa prolog di atas kode modul:

# propertymodule.py
""" Module property example """

if '__orgmod__' not in globals():

    # constant prolog for having module properties / supports reload()

    print "PropertyModule stub execution", __name__
    import sys, types
    class PropertyModule(types.ModuleType):
        def __str__(self):
            return "<PropertyModule %r from %r>" % (self.__name__, self.__file__)
    modnew = PropertyModule(__name__, __doc__)
    modnew.__modclass__ = PropertyModule        
    modnew.__file__ = __file__
    modnew.__orgmod__ = sys.modules[__name__]
    sys.modules[__name__] = modnew
    exec sys._getframe().f_code in modnew.__dict__

else:

    # normal module code (usually vast) ..

    print "regular module execution"
    a = 7

    def get_dynval(module):
        return "property function returns %s in module %r" % (a * 4, module.__name__)    
    __modclass__.dynval = property(get_dynval)

Pemakaian:

>>> import propertymodule
PropertyModule stub execution propertymodule
regular module execution
>>> propertymodule.dynval
"property function returns 28 in module 'propertymodule'"
>>> reload(propertymodule)   # AFTER EDITS
regular module execution
<module 'propertymodule' from 'propertymodule.pyc'>
>>> propertymodule.dynval
"property function returns 36 in module 'propertymodule'"

Catatan: Sesuatu seperti from propertymodule import dynvalakan menghasilkan salinan beku tentu saja - sesuai dengandynval = someobject.dynval

kxr
sumber
1

Jawaban singkatnya: gunakan proxy_tools

The proxy_toolspaket mencoba untuk memberikan @module_propertyfungsionalitas.

Ini menginstal dengan

pip install proxy_tools

Menggunakan sedikit modifikasi dari contoh @ Marein, the_module.pykami masukkan

from proxy_tools import module_property

@module_property
def thing():
    print(". ", end='')  # Prints ". " on each invocation
    return 'hello'

Sekarang dari skrip lain, saya bisa melakukannya

import the_module

print(the_module.thing)
# . hello

Perilaku tidak terduga

Solusi ini bukannya tanpa peringatan. Yaitu, the_module.thingadalah tidak string ! Ini adalah proxy_tools.Proxyobjek yang metode khususnya telah diganti sehingga meniru string. Berikut adalah beberapa tes dasar yang menggambarkan hal tersebut:

res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]

print(type(res))
# <class 'proxy_tools.Proxy'>

print(isinstance(res, str))
# False

print(res)
# . hello

print(res + " there")
# . hello there

print(isinstance(res + "", str))
# . True

print(res.split('e'))
# . ['h', 'llo']

Secara internal, fungsi asli disimpan ke the_module.thing._Proxy__local:

print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>

Pikiran lebih lanjut

Sejujurnya, saya bingung tentang mengapa modul tidak memiliki fungsi bawaan ini. Saya pikir inti masalahnya adalah itu the_moduleadalah turunan dari types.ModuleTypekelas. Menyetel "properti modul" berarti menyetel properti pada instance kelas ini, bukan pada types.ModuleTypekelas itu sendiri. Untuk lebih jelasnya, lihat jawaban ini .

Kami sebenarnya dapat mengimplementasikan properti pada types.ModuleTypesebagai berikut, meskipun hasilnya tidak bagus. Kami tidak dapat langsung mengubah tipe bawaan, tetapi kami dapat mengutuknya :

# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))

Ini memberi kita properti yang ada di semua modul. Ini agak sulit, karena kami merusak perilaku pengaturan di semua modul:

import sys

print(sys.thing2)
# hi from sys

sys.thing2 = 5
# AttributeError: can't set attribute
Ben Mares
sumber
1
Bagaimana ini lebih baik daripada hanya menjadikan modul sebagai instance dari kelas nyata seperti yang ditunjukkan dalam jawaban @Alex Martelli?
martineau
1
Anda mengatakan hal lain yang tidak masuk akal bagi saya. Ambillah bisnis ini tentang memiliki @module_propertydekorator. Secara umum, @propertydekorator built-in digunakan ketika kelas ditentukan, bukan setelah sebuah instance dibuat, jadi saya akan berasumsi hal yang sama akan berlaku untuk properti modul dan dengan jawaban Alex - ingat pertanyaan ini bertanya "Dapatkah modul memiliki properti dengan cara yang sama seperti objek?". Namun adalah mungkin untuk menambahkan mereka setelah itu dan saya sudah dimodifikasi saya sebelumnya potongan untuk menggambarkan cara yang bisa dilakukan.
martineau
1
Ben: Setelah melihat kode dalam contoh konkret Anda, saya rasa saya mengerti apa yang Anda dapatkan sekarang. Saya juga berpikir saya baru-baru ini menemukan teknik untuk mengimplementasikan sesuatu yang mirip dengan properti modul yang tidak memerlukan modul untuk diganti dengan instance kelas seperti dalam jawaban Alex, meskipun saya tidak yakin pada titik ini apakah ada cara untuk melakukannya. melalui dekorator - akan menghubungi Anda kembali jika saya membuat kemajuan.
martineau
1
Oke, ini tautan ke jawaban untuk pertanyaan lain yang berisi gagasan inti.
martineau
1
Setidaknya dalam kasus a cached_module_property, fakta yang __getattr__()tidak akan dipanggil lagi jika atributnya didefinisikan sangat membantu. (mirip dengan apa yang functools.cached_propertydicapai).
martineau