__getattr__ pada modul

127

Bagaimana mengimplementasikan yang setara dengan a __getattr__di kelas, di modul?

Contoh

Saat memanggil fungsi yang tidak ada dalam atribut modul yang didefinisikan secara statis, saya ingin membuat instance kelas dalam modul itu, dan memanggil metode di atasnya dengan nama yang sama seperti yang gagal dalam pencarian atribut pada modul.

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

Pemberian yang mana:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined
Matt Joiner
sumber
2
Saya kemungkinan akan memilih jawaban berduka, karena ini berfungsi dalam semua keadaan (meskipun agak berantakan dan bisa dilakukan lebih baik). Harvard S dan S Lott memiliki jawaban bersih yang bagus tetapi itu bukan solusi praktis.
Matt Joiner
1
Anda tidak dalam kasus Anda bahkan membuat akses atribut, jadi Anda meminta dua hal yang berbeda sekaligus. Jadi pertanyaan utamanya adalah yang mana yang Anda inginkan. Apakah Anda ingin salutationada di namespace global atau lokal (yang coba dilakukan oleh kode di atas) atau Anda ingin pencarian nama yang dinamis saat membuat akses titik pada modul? Itu dua hal yang berbeda.
Lennart Regebro
Pertanyaan menarik, bagaimana Anda mendapatkan ini?
Chris Wesseling
1
kemungkinan duplikat Autoload dengan Python
Piotr Dobrogost
1
__getattr__pada modul didukung dari Python 3.7
Fumito Hamamura

Jawaban:

42

Beberapa waktu lalu, Guido mendeklarasikan bahwa semua pencarian metode khusus pada kelas gaya baru melewati __getattr__dan__getattribute__ . Metode Dunder sebelumnya bekerja pada modul - Anda dapat, misalnya, menggunakan modul sebagai pengelola konteks hanya dengan mendefinisikan __enter__dan __exit__, sebelum trik tersebut rusak .

Baru-baru ini beberapa fitur historis telah kembali, modul __getattr__di antaranya, dan peretasan yang ada (modul yang menggantikan dirinya dengan kelas pada sys.modulessaat impor) seharusnya tidak lagi diperlukan.

Di Python 3.7+, Anda cukup menggunakan satu cara yang jelas. Untuk menyesuaikan akses atribut pada modul, tentukan __getattr__fungsi pada tingkat modul yang harus menerima satu argumen (nama atribut), dan mengembalikan nilai yang dihitung atau meningkatkan AttributeError:

# my_module.py

def __getattr__(name: str) -> Any:
    ...

Ini juga akan memungkinkan kait ke impor "dari", yaitu Anda dapat mengembalikan objek yang dibuat secara dinamis untuk pernyataan seperti from my_module import whatever.

Pada catatan terkait, bersama dengan getattr modul, Anda juga dapat menentukan __dir__fungsi di tingkat modul untuk merespons dir(my_module). Lihat PEP 562 untuk detailnya.

wim
sumber
1
Jika saya secara dinamis membuat modul melalui m = ModuleType("mod")dan set m.__getattr__ = lambda attr: return "foo"; Namun, ketika saya lari from mod import foo, saya mengerti TypeError: 'module' object is not iterable.
weberc2
@ weberc2: Buat itu m.__getattr__ = lambda attr: "foo", ditambah Anda perlu mendefinisikan entri untuk modul dengan sys.modules['mod'] = m. Setelah itu, tidak ada kesalahan dengan from mod import foo.
martineau
wim: Anda juga bisa mendapatkan nilai yang dihitung secara dinamis — seperti memiliki properti tingkat modul — memungkinkan seseorang untuk menulis my_module.whateveruntuk memanggilnya (setelah sebuah import my_module).
martineau
115

Ada dua masalah dasar yang Anda hadapi di sini:

  1. __xxx__ metode hanya dicari di kelas
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) berarti solusi apa pun juga harus melacak modul mana yang sedang diperiksa, jika tidak, setiap modul akan memiliki perilaku substitusi-instance; dan (2) berarti bahwa (1) bahkan tidak mungkin ... setidaknya tidak secara langsung.

Untungnya, sys.modules tidak pilih-pilih tentang apa yang ada di sana sehingga pembungkus akan berfungsi, tetapi hanya untuk akses modul (yaitu import somemodule; somemodule.salutation('world'); untuk akses modul yang sama Anda harus mencabut metode dari kelas substitusi dan menambahkannya ke globals()eiher dengan metode kustom di kelas (saya suka menggunakan .export()) atau dengan fungsi umum (seperti yang sudah terdaftar sebagai jawaban). Satu hal yang perlu diingat: jika pembungkus selalu membuat instance baru, dan solusi global tidak, Anda berakhir dengan perilaku yang agak berbeda. Oh, dan Anda tidak dapat menggunakan keduanya pada saat yang sama - itu salah satu.


Memperbarui

Dari Guido van Rossum :

Sebenarnya ada peretasan yang kadang-kadang digunakan dan direkomendasikan: modul dapat mendefinisikan kelas dengan fungsionalitas yang diinginkan, dan kemudian pada akhirnya, mengganti dirinya sendiri di sys.modules dengan instance kelas itu (atau dengan kelas, jika Anda bersikeras , tapi itu umumnya kurang berguna). Misalnya:

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

Ini berfungsi karena mesin impor secara aktif mengaktifkan peretasan ini, dan sebagai langkah terakhirnya menarik modul aktual dari sys.modules, setelah memuatnya. (Ini bukan kebetulan. Peretasan sudah lama diusulkan dan kami memutuskan bahwa kami cukup menyukainya untuk mendukungnya di mesin impor.)

Jadi cara yang mapan untuk mencapai apa yang Anda inginkan adalah dengan membuat satu kelas dalam modul Anda, dan sebagai tindakan terakhir modul ganti sys.modules[__name__]dengan instance kelas Anda - dan sekarang Anda dapat bermain dengan __getattr__/ __setattr__/ __getattribute__sesuai kebutuhan.


Catatan 1 : Jika Anda menggunakan fungsionalitas ini, apa pun yang ada di modul, seperti global, fungsi lain, dll., Akan hilang saat sys.modulestugas dibuat - jadi pastikan semua yang diperlukan ada di dalam kelas pengganti.

Catatan 2 : Untuk mendukung from module import *Anda harus __all__didefinisikan di kelas; sebagai contoh:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

Bergantung pada versi Python Anda, mungkin ada nama lain untuk dihilangkan __all__. The set()dapat dihilangkan jika Python 2 kompatibilitas tidak diperlukan.

Ethan Furman
sumber
3
Ini berfungsi karena mesin impor secara aktif mengaktifkan peretasan ini, dan sebagai langkah terakhirnya menarik modul aktual keluar dari sys.modules, setelah memuatnya Apakah disebutkan di suatu tempat di dokumen?
Piotr Dobrogost
4
Sekarang saya merasa lebih nyaman menggunakan peretasan ini, mengingatnya "semi-sanksi" :)
Mark Nunberg
3
Ini melakukan hal-hal edan, seperti membuat import sysmemberi Noneuntuk sys. Saya menduga peretasan ini tidak dikenai sanksi dalam Python 2.
asmeurer
3
@asmeurer: Untuk memahami alasan untuk itu (dan solusinya) lihat pertanyaan Mengapa nilai __name__ berubah setelah penugasan ke sys.modules [__ name__]? .
martineau
1
@qarma: Sepertinya saya ingat beberapa penyempurnaan yang dibicarakan yang akan memungkinkan modul python untuk lebih langsung berpartisipasi dalam model pewarisan kelas, tetapi meskipun demikian metode ini masih berfungsi dan didukung.
Ethan Furman
48

Ini adalah hack, tetapi Anda dapat membungkus modul dengan kelas:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])
Håvard S
sumber
10
bagus dan kotor: D
Matt Joiner
Itu mungkin berhasil tetapi mungkin itu bukan solusi untuk masalah sebenarnya dari penulis.
DasIch
12
"Mungkin berhasil" dan "mungkin tidak" tidak terlalu membantu. Ini adalah retasan / trik, tetapi berfungsi, dan memecahkan masalah yang ditimbulkan oleh pertanyaan tersebut.
Håvard S
6
Meskipun ini akan berfungsi di modul lain yang mengimpor modul Anda dan mengakses atribut yang tidak ada di dalamnya, ini tidak akan berfungsi untuk contoh kode sebenarnya di sini. Mengakses global () tidak melalui sys.modules.
Marius Gedminas
5
Sayangnya ini tidak berfungsi untuk modul saat ini, atau mungkin untuk hal-hal yang diakses setelah import *.
Matt Joiner
19

Kami biasanya tidak melakukannya seperti itu.

Yang kami lakukan adalah ini.

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

Mengapa? Sehingga contoh global implisit terlihat.

Misalnya, lihat randommodul, yang membuat instance global implisit untuk sedikit menyederhanakan kasus penggunaan di mana Anda menginginkan generator nomor acak "sederhana".

S. Lott
sumber
Jika Anda benar - benar ambisius, Anda dapat membuat kelas, dan mengulangi semua metodenya dan membuat fungsi tingkat modul untuk setiap metode.
Paul Fisher
@ Paul Fisher: Sesuai masalah, kelas sudah ada. Mengekspos semua metode kelas mungkin bukan ide yang bagus. Biasanya metode yang diekspos ini adalah metode "kenyamanan". Tidak semua sesuai untuk contoh global implisit.
S. Lott
13

Mirip dengan apa yang @ Håvard S usulkan, dalam kasus di mana saya perlu menerapkan beberapa sihir pada modul (seperti __getattr__), saya akan mendefinisikan kelas baru yang mewarisi types.ModuleTypedan memasukkannya sys.modules(mungkin mengganti modul tempat kebiasaan saya ModuleTypedidefinisikan).

Lihat __init__.pyfile utama Werkzeug untuk implementasi yang cukup kuat dari ini.

Matt Anderson
sumber
8

Ini hackish, tapi ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

Ini bekerja dengan mengulang semua objek di namespace global. Jika item tersebut adalah kelas, item tersebut diulangi atas atribut kelas. Jika atribut dapat dipanggil, ia menambahkannya ke namespace global sebagai fungsi.

Ini mengabaikan semua atribut yang berisi "__".

Saya tidak akan menggunakan ini dalam kode produksi, tetapi ini akan membantu Anda memulai.

bersedih
sumber
2
Saya lebih suka jawaban Håvard S daripada saya, karena kelihatannya jauh lebih rapi, tetapi ini langsung menjawab pertanyaan seperti yang ditanyakan.
berduka
Ini jauh lebih dekat dengan apa yang akhirnya saya gunakan. Ini sedikit berantakan, tetapi berfungsi dengan global () dengan benar dalam modul yang sama.
Matt Joiner
1
Sepertinya saya seperti jawaban ini tidak cukup apa yang diminta yaitu "Saat memanggil fungsi yang tidak ada dalam atribut yang didefinisikan secara statis modul" karena melakukan itu bekerja tanpa syarat dan menambahkan setiap metode kelas yang mungkin. Itu bisa diperbaiki dengan menggunakan pembungkus modul yang hanya melakukan AddGlobalAttribute()ketika ada level modul AttributeError- jenis kebalikan dari logika @ Håvard S. Jika saya memiliki kesempatan, saya akan menguji ini dan menambahkan jawaban hybrid saya sendiri meskipun OP telah menerima jawaban ini.
martineau
Perbarui ke komentar saya sebelumnya. Sekarang saya mengerti bahwa sangat sulit (impssoble?) Untuk mencegat NameErrorpengecualian untuk namespace (modul) global - yang menjelaskan mengapa jawaban ini menambahkan callable untuk setiap kemungkinan yang ditemukannya ke namespace global saat ini untuk mencakup setiap kemungkinan kasus sebelumnya.
martineau
5

Inilah kontribusi saya yang sederhana - sedikit hiasan dari jawaban @ Håvard S yang berperingkat tinggi, tetapi sedikit lebih eksplisit (jadi mungkin dapat diterima oleh @ S. Lott, meskipun mungkin tidak cukup baik untuk OP):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")
martineau.dll
sumber
-3

Buat file modul Anda yang memiliki kelas Anda. Impor modul. Jalankan getattrpada modul yang baru saja Anda impor. Anda dapat melakukan impor dinamis menggunakan__import__ dan menarik modul dari sys.modules.

Ini modul Anda some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

Dan di modul lain:

import some_module

Foo = getattr(some_module, 'Foo')

Melakukan ini secara dinamis:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')
drr
sumber
1
Anda menjawab pertanyaan lain di sini.
Marius Gedminas