简体   繁体   English

运行 for 循环以在类内、在函数内使用 exec 创建函数

[英]Running a for loop to create functions with exec, inside a class, inside a function

I know it sounds complicated and it may not be possible, but I thought I'd try anyway.我知道这听起来很复杂,而且可能不可能,但我想我还是会尝试一下。

So I'm doing some web scraping with Selenium, and any time that I want to run some jQuery on a page, instead of having所以我正在用 Selenium 做一些网页抓取,并且任何时候我想在页面上运行一些 jQuery,而不是

driver.execute_script("$('button').parent().click()")

I want to tidy up my code and just have我想整理我的代码,只是有

j('button').parent().click()

I'm attempting to accomplish this by having a class j and then when you have a function like parent it just returns another instance of that class.我试图通过拥有一个类j来实现这一点,然后当你有一个像parent这样的函数时,它只返回该类的另一个实例。

In these examples I just have it printing the string that will get executed so that it can be tested more easily.在这些示例中,我只是让它打印将被执行的字符串,以便可以更轻松地对其进行测试。 And the way I have it the class exists inside a function.我的方式是类存在于函数中。

So in this example everything works fine when all the class functions are defined normally, but if you uncomment the for loop section and define the .parent() function with exec then you get the error NameError: name 'j' is not defined :因此,在此示例中,当所有类函数都正常定义时,一切正常,但是如果取消对for循环部分的注释并使用exec定义.parent()函数,则会收到错误NameError: name 'j' is not defined

def browser():

    class j:

        def __init__(self, str):
            if str[0] == '$':
                self.jq = str
            else:
                self.jq = f"$('{str}')"

        def click(self):
            output = f"{self.jq}.click()"
            print(output)
            return j(output)

        def parent(self):
            return j(f'{self.jq}.parent()')

        # functions = 'parent, next, prev'
        # for fn in functions.split(', '):
        #     exec(
        #         f"def {fn} (self):\n" +
        #         f"    return j(f'{{self.jq}}.{fn}()')"
        #     )

    j('button').parent().click()

browser()

However, if you move everything outside of the browser function and run everything then there's no error that way also, and that's if you define the functions either way.但是,如果您将所有内容移到browser功能之外并运行所有内容,那么这种方式也不会出错,而且如果您以任何一种方式定义功能。

I also tried doing exec(code, globals()) but that just gave me a different error message instead: AttributeError: 'j' object has no attribute 'parent'我也尝试过exec(code, globals())但这只是给了我一个不同的错误消息: AttributeError: 'j' object has no attribute 'parent'

Is there some way to define functions with exec in this way and do what I'm try to do?有没有办法以这种方式用exec定义函数并做我想做的事?

Edit: This is the entire error message:编辑:这是整个错误消息:

Traceback (most recent call last):
  File "C:\Users\Rob\Desktop\site\mysite\py\z.py", line 30, in <module>
    browser()
  File "C:\Users\Rob\Desktop\site\mysite\py\z.py", line 28, in browser
    j('button').parent().click()
  File "<string>", line 2, in parent
NameError: name 'j' is not defined

Using setattr and examples from Dynamic/runtime method creation (code generation) in Python I made: 在 Python 中使用setattr和来自动态/运行时方法创建(代码生成)的示例我做了:

def browser():

    class j:

        def __init__(self, str):
            if str[0] == '$':
                self.jq = str
            else:
                self.jq = f"$('{str}')"

        def click(self):
            output = f"{self.jq}.click()"
            print('click:', output)
            return j(output)

        def fun(self, name):
            return j(f'{self.jq}.{name}()')

        def fun2(self, name):
            self.jq += f'.{name}()'
            return self

    # - after class -

    functions = 'parent, next, prev'
    for fn in functions.split(', '):
         #setattr(j, fn, lambda cls, fun=fn: j(f'{cls.jq}.{fun}()'))
         #setattr(j, fn, lambda cls, fun=fn: j.fun(cls, fun))
         setattr(j, fn, lambda cls, fun=fn: cls.fun(fun))

    print('test:', j('button').parent().next().click().fun2('a').fun('b').jq)

browser()

But it is not ideal - it creates something like next = fun("next") and it may run it as next("x") and add .x() instead of .next()但这并不理想 - 它创建了类似next = fun("next") ,它可能将其作为next("x")并添加.x()而不是.next()


I tested also return self instead of creating new instance - and it also works.我测试也return self而不是创建新实例 - 它也可以工作。

Beacuse lambda is use in for -loop so it needs fun=fn to correctly get value from fn .因为lambda for循环for所以它需要fun=fn才能从fn正确获取值。 Without this it will take value from fn when it will be executed and then all function get the same value from fn - last value assigned to fn in loop.如果没有这个,它将在执行时从fn获取值,然后所有函数从fn获取相同的值 - 最后一个值分配给fn在循环中。


EDIT: Instead of running directly setattr with lambda I run function which runs setattr and use inner function instead of lambda - this way it can create function without second argument so now .next("x") raise error.编辑:而不是直接运行setattrlambda我运行它运行功能setattr并使用内部函数,而不是lambda -这样,所以现在可以创建没有第二个参数的功能.next("x")引发错误。

def browser():

    class j:

        def __init__(self, str):
            if str[0] == '$':
                self.jq = str
            else:
                self.jq = f"$('{str}')"

        def click(self):
            output = f"{self.jq}.click()"
            print('click:', output)
            return j(output)

        def fun(self, name):
            return j(f'{self.jq}.{name}()')

        def fun2(self, name):
            self.jq += f'.{name}()'
            return self

        @classmethod
        def add_function(cls, name):
            def func(cls):
                cls.jq += f'.{name}()'
                return cls
            func.__name__ = name
            setattr(j, name, func)

    # - after class -

    functions = 'parent, next, prev'
    for fn in functions.split(', '):
         #setattr(j, fn, lambda cls, fun=fn: j(f'{cls.jq}.{fun}()'))
         #setattr(j, fn, lambda cls, fun=fn: j.fun(cls, fun))
         #setattr(j, fn, lambda cls, fun=fn: cls.fun(fun))
         #setattr(j, fn, lambda cls: cls.fun(x))
         j.add_function(fn)

    item = j('button').parent().next().click().fun2('a').fun('b')
    print('test:', item.jq)

    j.add_function('other')

    item = item.other()
    print('test:', item.jq)

    item = j('input').other().click()
    print('test:', item.jq)

    print('name:', j.next.__name__) # gives `next`, without `func.__name__ = name` it gives `func`

browser()

I tested also __getattr__我也测试了__getattr__

def browser():

    class j:


        def __init__(self, str):
            if str[0] == '$':
                self.jq = str
            else:
                self.jq = f"$('{str}')"

        def click(self):
            return self.fun('click')
            #self.jq += f".click()"
            #return self

        def fun(self, name):
            self.jq += f'.{name}()'
            return self

        functions = ['parent', 'next', 'prev']

        #def __getattr__(self, name):
        #    '''acceptes every name'''
        #    return lambda: self.fun(name) # I use `lambda` because it needs function to use `()`

        #def __getattr__(self, name):
        #    '''___name__ doesn't change its name in error message and it shows `<lambda>`'''
        #    a = lambda: self.fun(name)
        #    a.__name__ = name
        #    return a

        def __getattr__(self, name):
            '''acceptes only name from list `functions`'''
            if name in self.functions:
                return lambda: self.fun(name) # I use `lambda` because it needs function to use `()`
            else:
                raise AttributeError(f"'j' object has no attribute '{name}'")

        #def __getattr__(self, name):
        #    '''test running without `fun()`'''
        #    self.jq += f'.{name}()'
        #    return lambda:self # I use `lambda` because it needs function to use `()`


    # - after class -

    item = j('button').parent().next().click().fun('a').fun('b')
    print('test:', item.jq)

    j.functions.append('other')
    item = item.other()
    print('test:', item.jq)

    item = j('input').other().click()
    print('test:', item.jq)

    #print('name:', j.next.__name__) # doesn't work

    print(j('a').tests().jq) 

browser()

Furas's second answer got me most of the way there so he deserves the upvotes. Furas 的第二个答案让我大获全胜,因此他应得的支持。 But I managed to improve on it and get it to do everything I was looking for.但我设法改进它并让它做我想要的一切。

So here is my version that can take arguments and checks type and puts quotes around strings but not numbers.所以这是我的版本,它可以接受参数并检查类型并在字符串周围加上引号而不是数字。

def browser():

    class j:

        def __init__(self, str):
            self.jq = f"$('{str}')"

        def click(self):
            self.jq += ".click()"
            print(self.jq)
            return self

        @classmethod
        def quotes(cls, arg):
            q = "'" * (type(arg) == str and len(arg) > 0)
            output = f'{q}{arg}{q}'
            return output

        @classmethod
        def add_function(cls, name):
            def func(cls, arg=''):
                cls.jq += f'.{name}({j.quotes(arg)})'
                return cls
            func.__name__ = name
            setattr(j, name, func)

    j_functions = 'closest, eq, find, next, parent, parents, prev'
    for fn in j_functions.split(', '):
        j.add_function(fn)

    j('button').eq(0).next('br').prev().click()

browser()


>> $('button').eq(0).next('br').prev().click()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM